型なしDataTableから型付きDataTableにコピーする方法

最近だと、LINQ to SQL や LINQ to Entities があるので、DataSet/DataTable はあまり使わないのですが、ADO.NET と云えば、DataAdapter と DataSet の組み合わせでした。
その頃は、データベースのテーブルを型付で取ってこれる、型付きDataSetの存在が結構大きかったのです。

どういうことかというと、DataTable を直接扱ってしまった場合、

foreach ( DataRow row in dt.Rows ) {
	int id = (int)row["id"];
	...
}

のように、DataRow から値を取ってくる場合は、列名を指定しないと駄目かつキャストをしなければならず、という2重苦が待っています。これが、文字列なので、ええ、ちょっと間違うとえらいことになってしまうのです。PHP だと、こんな風に書くのが普通なので、特に気にしないのですが、ASP.NET で規模の大きいデータベースだと、ちょっとデバッグが大変なことに、っていう具合です。これ、実行時のエラーでしか取れないので、難しいんですよね。

なので、C# の厳密な型、というのを利用して、

foreach ( MyDataRow row in dt.Rows ) {
	int id = row.id ;
	...
}

のように、プロパティで値を安全に取得できるようにするのが、型付の意味なのです。

で、この型付DataSetですが、Visual Studio 上で作成すると膨大な自動生成のソースコードがあって整理が大変ッ!!! ということもあり、さらに、自前で型付DataTableを使いたいときは、あのデザイナでちまちま列を作らなきゃならない、という問題がありまして。なんとなく避けてしまって、とりあえず、DataSet や DataTable のまま扱っているのが普通なのではないかなぁと。

そこで、本題ですが、これをコンバーターのクラスを用意して、型なしから型付にコピーできるようにしてしまおう、というものです。で、MyDataTable, MyDataRow の作成はなるべく手間をかけたくないという訳で。

それを実現してみたのが次のコードです。

/// <summary>
/// リフレクションを簡単にするための準備
/// </summary>
public interface IDataTable  
{
    Type GetRowType();
    object CreateRow();
}
/// <summary>
/// 型付DataTable用のテンプレート
/// </summary>
/// <typeparam name="DRType"></typeparam>
public class DTTemplate<DRType> : IDataTable, IListSource
{
    /// <summary>
    /// DataRow の型を返す
    /// </summary>
    /// <returns></returns>
    public Type GetRowType() {
        return typeof(DRType);
    }
    /// <summary>
    /// DataRow のリスト
    /// </summary>
    protected List<DRType> _rows = new List<DRType>();
    public List<DRType> Rows {
        get { return _rows; }
    }

    /// <summary>
    /// 新しい DataRow を作成
    /// </summary>
    /// <returns></returns>
    public DRType NewRow() {
        return (DRType)Activator.CreateInstance(typeof(DRType));
    }
    /// <summary>
    /// 新しい DataRow を作成(object型)
    /// </summary>
    /// <returns></returns>
    public object CreateRow() {
        return NewRow();
    }
    
    // インターフェースの実装
    /// <summary>
    /// 内部リストから IList を使う
    /// </summary>
    public bool  ContainsListCollection
    {
        get { return true; }
    }
    /// <summary>
    /// IListのコレクションを返す
    /// </summary>
    /// <returns></returns>
    public System.Collections.IList GetList()
    {
        return this.Rows;
    }
}

/// <summary>
/// いわゆる型付DataRow
/// </summary>
public class ProductRow  
{
    protected string _id;
    protected string _name;
    protected int _price;

    // プロパティアクセスは、C# 3.0 ならば、
    // public string id { get; set; }
    // で書ける.
    // 今回は v2.0 なので、この形式で。
    public string id
    {
        get { return _id; }
        set { _id = value; }
    }
    public string name
    {
        get { return _name; }
        set { _name = value; }
    }
    public int price
    {
        get { return _price; }
        set { _price = value; }
    }
}
/// <summary>
/// 型付DataSet
/// </summary>
public class Prodcut : DTTemplate<ProductRow>
{
}
/// <summary>
/// 形無しDataSetを型付DataSetにコンバートするクラス
/// </summary>
public class DataBind
{
    public static void Conv(DataTable src, IDataTable dest)
    {
        Type rowType = dest.GetRowType();
        System.Collections.IList rows = ((IListSource)dest).GetList();
        foreach (DataRow row in src.Rows)
        {
            object item = dest.CreateRow();
            // リフレクションを使って、列ごとにコピーする
            foreach (DataColumn column in src.Columns)
            {
                // この部分はキャッシュすると高速化する
                string key = column.ColumnName;
                PropertyInfo pi = rowType.GetProperty(key);
                if (pi != null)
                {
                    // object型 -> 元の型のキャストでエラーになるため、
                    // 一度元の型に ChangeType してから代入する。
                    pi.SetValue(item, 
                        Convert.ChangeType(row[key], pi.PropertyType), 
                        null);
                }
            }
            rows.Add(item);
        }
    }
}

いやぁ、リフレクションを使っているんですが、結構実現するのは長いコードになってしまいました。型付の ProductRow は、v2.0 なのでget/set を別に書く必要がありますが、v3.0 以降ならば、次のように短く書けます。

public class ProductRow  
{
    public string id { get; set; }
    public string name { get; set; }
    public int price { get; set; }
}

そして、型付DataTable に関しては、こんな風に更に短く。

public class Prodcut : DTTemplate<ProductRow> {}

実質、1行で書くことができます。
コンバート関数は、DataBind.Conv() だけで使えるので、非常に簡単。

データベースに接続して、DataGrid に表示するコードがこのように短く書けます。

private void button2_Click(object sender, EventArgs e)
{
    // データベース接続
    SqlConnection cn = new SqlConnection(@"Data Source=.\sqlexpress;Initial Catalog=mvcdb;Integrated Security=True;MultipleActiveResultSets=True");
    cn.Open();
    DataTable dt = new DataTable();
    SqlDataAdapter da = new SqlDataAdapter(
        "SELECT * FROM TProduct", cn);
    da.Fill(dt);
    cn.Close();

    Prodcut product =new Prodcut();
    DataBind.Conv(dt, product);

    dataGridView1.DataSource = product;

}

まあ、単純に DataGridView の DataSource プロパティを使ってバインドする場合は、DataTable のままでいいんですけどね。列名なんかで条件分岐をするときなんか、dt.id のようなプロパティで指定できると間違いが少なくなりますよねぇ。という話でした。

ええと、業務で使うコードは、もうちょっと高速化していきます。といいますか、DataBind.Conv のように static 関数にせず、実は ProductRow の行テーブルへのマッピング方式にしています。ま、これは別の機会に紹介します。値を加工して ProductRow に詰め込むようなことをするために、ProductRow の内部メソッドとして用意しています。

カテゴリー: C# パーマリンク