2013年6月9日日曜日

DbCommandからのレコード取得をLINQableにする拡張メソッド

RDBMS利用時のアーキテクチャの選択方針」では、バッチ処理では生ADO.netが相性が良いと書きました。ただこのクラス群はさすがに古くて、あまり積極的に使いたくないなー。と思ってしまうところがあります。

特に、SQLを実行して結果を取得しない「DbCommand.ExecuteNonQuery」や単一の値を求める「DbCommand.ExecuteScalar」であればともかく、SELECTして複数行にわたって処理する場合、「DbCommand.ExecuteReader」を使う必要がありますが、どう頑張ったってこんなコードになっちゃう。
using (var conn = new SqlConnection(connectionString))
using (var cmd = conn.CreateCommand())
{
  conn.Open();
  cmd.CommandText = selectCmd;

  using (var reader = cmd.ExecuteReader())
  {
    while (reader.Read())
    {
      xxx = reader[columnName];
      // いろいろ処理
    }
  }
}
んむー。せめてwhileじゃなくてforeachを使いたい。できればLINQに繋げたい。そのためには、SELECTの結果のレコードを、IEnumerable<T>で取得できるようなメソッドを用意しておきたい。そこで、DbCommandクラスにこんな拡張メソッドを用意してみます。なお、わかりやすくするためにパラメータのNULLチェックはなしで。
public static IEnumerable<IDataRecord> ExecuteQuery(this DbCommand command)
{
  return ExecuteQuery(command, dr => dr);
}

public static IEnumerable<T> ExecuteQuery<T>(
    this DbCommand command, Func<IDataRecord, T> mapper)
{
  using (var reader = command.ExecuteReader())
  {
    while (reader.Read())
    {
       yield return mapper(reader);
    }
  }
}
「ExecuteQuery」という名前の2つの拡張メソッドを作ります。一つはIEnumerable<IDataRecord>を返し、もう一方は任意の変換関数を用意して、IEnumerbale<T>を返します。ちなみに「IDataRecord」はDbCommand.ExecuteReaderメソッドが返す、DbDataReader抽象クラスの派生クラスで実装されます。

これを使うと多少ましになります。例としてなんか適当なバッチ処理を考えてみましょうか。

SQLServerのあるテーブルをCSVファイルにエクスポートします。その際に「exported」カラムが「0」のレコードのみを対象とします。また、エクスポート実行後、対象レコードの「exported」カラムを「1」に、「exportAt」カラムに現在日時をセットします。

こんなありがちな処理を先のDbCommand拡張メソッドを使って書いてみると、こんなコードになります。
const string connectionString =
  @"Data Source=.\SQLEXPRESS;Initial Catalog=Test;Integrated Security=True";
const string selectCmd =
  "select id, uid, insertAt, qty from TestTable_1 where exported=0";
const string updateCmd =
  "update TestTable_1 set exported=1, exportAt=getdate() where id in ({0})";

static void Main(string[] args)
{
  using (var conn = new SqlConnection(connectionString))
  {
    var ids = new List<int>();

    using (var cmd = conn.CreateCommand())
    using (var csv = new StreamWriter("output.csv"))
    {
      conn.Open();
      cmd.CommandText = selectCmd;

      foreach (var r in cmd.ExecuteQuery())
      {
        ids.Add(r.GetInt32(0));
        csv.WriteLine(string.Format("{0},{1},{2}", r[1], r[2], r[3]));
      }
    }

    if (ids.Count > 0)
    {
      using (var cmd = conn.CreateCommand())
      {
         cmd.CommandText = string.Format(updateCmd, string.Join(",", ids));
         cmd.ExecuteNonQuery();
      }
    }
  }
}
意外と見やすいコードが書けるんじゃないかと思ってるんですが、どうでしょ?

0 件のコメント:

コメントを投稿