データベースアクセスのパフォーマンスチューニング例 | 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> と同じ仕組みですね。
