データベースアクセスのパフォーマンスチューニング例 | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/2423
キャッシュを作ってDataTableのアクセスを高速にする | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/2780
の続きで、複数キー対応のDictonaryを作成してキャッシュを有効に使う例をば。
つまりは、「Dictonary」のような形で、2つの int型のキーを持っていて、それから ProductRow オブジェクト(Productテーブルの列オブジェクト)を取ってくる。
■利用方法
まずは、使い方を。
/// <summary> /// Product のキャッシュ /// </summary> private ProductCache pcache = null; /// <summary> /// 複数キーのキャッシュ実行 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void button2_Click(object sender, EventArgs e) { if (cache == null) { SqlConnection cn = new SqlConnection(CNSTR); SqlDataAdapter da = new SqlDataAdapter("SELECT * FROM PRODUCT", cn); DataTable dt = new DataTable(); da.Fill(dt); // キャッシュに設定 Product product = new Product(); DataBind.Conv(dt, product); this.pcache.Fill(product.Rows); } int id = int.Parse(textBox1.Text); int subid = int.Parse(textBox2.Text); // 2つのキーで検索 ProductRow row = this.pcache[id, subid]; textBox2.Text = row.name; }
初回だけデータベースに接続して、キャッシュを作成します。
DataRow から独自の Product テーブルクラスにコピーした後に、キャッシュ pcache に保存します。
キャッシュへのアクセスは、pcache[ id, subid ] のように配列でアクセスできて、戻り値は ProductRow オブジェクトという具合。DataRow よりも型付の ProductRow のほうが、row.id とか row.name とかのプロパティアクセスができるので、コードの品質はあがるはずです。インテリセンスが使ええるし、コンパイル時に列名が間違っているとエラーになるし。
■内部実装
キャッシュクラスはこんな感じ。
/// <summary> /// 複数キーに対応したDictonary /// キーが2つの場合 /// </summary> /// <typeparam name="K1"></typeparam> /// <typeparam name="K2"></typeparam> /// <typeparam name="V"></typeparam> public abstract class Dictonary<K1, K2, V> : Dictionary<string, V> where V : new() { public V Empty { get; set; } /// <summary> /// コンストラクタ /// </summary> public Dictonary() { this.Empty = new V(); } // protected override protected void Add(string key) { } protected new V this[string key] { get { return Empty; } set { } } protected new bool ContainsKey(string key) { return false; } /// <summary> /// キーと値を追加 /// </summary> /// <param name="key1"></param> /// <param name="key2"></param> /// <param name="value"></param> public void Add(K1 key1, K2 key2, V value) { this.Add(MakeKey(key1, key2), value); } /// <summary> /// キーを作成 /// </summary> /// <param name="key1"></param> /// <param name="key2"></param> /// <returns></returns> public string MakeKey(K1 key1, K2 key2) { return key1.ToString() + "_" + key2.ToString(); } public abstract string MakeKey(V value); /// <summary> /// []演算子でアクセス /// </summary> /// <param name="key1"></param> /// <param name="key2"></param> /// <returns></returns> public V this[K1 key1, K2 key2] { get { string key = MakeKey(key1, key2); return this.ContainsKey(key) ? this[key] : Empty; } set { string key = MakeKey(key1, key2); if (this.ContainsKey(key)) { this[key] = value; } else { this.Add(key, value); } } } /// <summary> /// 指定したキーを含むか /// </summary> /// <param name="key1"></param> /// <param name="key2"></param> /// <returns></returns> public bool ContainsKey(K1 key1, K2 key2) { string key = MakeKey(key1, key2); return base.ContainsKey(key); } /// <summary> /// データを設定 /// </summary> /// <param name="table"></param> public void Fill(IList<V> items) { foreach (V it in items) { this[MakeKey(it)] = it; } } }
抽象クラスになっているのは、DataRow にあたる V から主キーは直接取り出せないので、一度継承するという方式です。データエンティティに「主キーの属性」みたいなのをつければ、ここは解決するかも。
もうひとつは、DataRow から型付のDataRowクラスにコンバートするためのクラスが必要と思います。毎回コンバートだけコードを書くのは面倒なので、テンプレートとリフレクションを使って自動化します。
/// <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> /// <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); } } }
これを作成しておいて、自前のエンティティのクラスやキャッシュクラスを作成。
/// <summary> /// 複数キーを持つ Product エンティティクラス /// </summary> public class ProductRow { public int id { get; set; } // PK public int subid { get; set; } // PK public string name { get; set; } public int parent_id { get; set; } public string desc { get; set; } public DateTime updatedate { get; set; } } /// <summary> /// 型付DataTable /// </summary> public class Product : DTTemplate<ProductRow> { } /// <summary> /// Product キャッシュクラスを作成 /// </summary> public class ProductCache : Dictonary<int, int, ProductRow> { /// <summary> /// ProductRow からキー文字列を取得 /// </summary> /// <param name="value"></param> /// <returns></returns> public override string MakeKey(ProductRow value) { return MakeKey(value.id, value.subid); } }
テンプレートのまま使ってもいいけど、もう一度のエンティティクラスの名前を付け直すとコーディングが楽です。テーブル名と揃えておくと、データベース仕様書とのマッチングが楽とか。
ちなみに、キーが3つある場合は、先の Dictonary のコードをコピーして、
public abstract class Dictonary<K1, K2, K3, V> : Dictionary<string, V> where V : new()
のように倍増させておきます。あらかじめ、6個ぐらい作っておけば大抵のテーブルは大丈夫かと。デリゲートの <Action> と同じ仕組みですね。