キャッシュを作ってDataTableのアクセスを高速にする(2)

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

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