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

データベースアクセスのパフォーマンスチューニング例 | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/2423

の続きを作ろうと思って「3分間クッキング」ならぬ「30分間プログラミング」になってしまいました。テンプレートを使ったものと、DataRow をそのまま使ったものとを 2 つ作ってしまったかですね。DataTable や List<> の場合には、Select メソッドを使えるのではないか?というのもありますが、Dictionary を使っているので速度的には断然高速なはずです。少なくとも、20万件ぐらいでも、Dictionary の場合は、一瞬(0.001秒以下)で返ってきます。

■利用の仕方

private string CNSTR = "";
private AnkenCache cache = null;

private void button1_Click(object sender, EventArgs e)
{
	if (cache == null)
	{
		SqlConnection cn = new SqlConnection(CNSTR);
		SqlDataAdapter da = new SqlDataAdapter("SELECT * FROM ANKEN", cn);
		DataTable dt = new DataTable();
		da.Fill(dt);
		// キャッシュに設定
		this.cache.SetData(dt);
	}
	int id = int.Parse(textBox1.Text);
	DataRow row = cache.Get(id.ToString());
	textBox2.Text = (string)row["name"];
}

こんな風にキャッシュ利用します。1度目はデータベースから検索するけど、2度目以降はキャッシュを使うってことで。
キー情報は、string 型なので、適当な MakeKey なる関数を作っておくと便利です。

■内部コード

DataTable を渡して DataRow の中身を Dictionary にキャッシュしているだけです。某案件では、これに似たことをやりましたが、こっちのほうが洗練されている…と思う。

public abstract class SelectCacheTableRow
{
	protected Dictionary<string, DataRow> _dic;
	public DataRow Empty { get; set; }

	public SelectCacheTableRow()
	{
		_dic = new Dictionary<string, DataRow>();
		this.Empty = new DataTable().NewRow(); // 空を設定
	}
	public void Set(string key, DataRow value)
	{
		if (_dic.ContainsKey(key) == true)
		{
			_dic[key] = value;
		}
		else
		{
			_dic.Add(key, value);
		}
	}
	public DataRow Get(string key)
	{
		if (_dic.ContainsKey(key) == true)
		{
			return _dic[key];
		}
		else
		{
			return this.Empty;
		}
	}
	/// <summary>
	/// キー情報を作成
	/// </summary>
	/// <param name="t"></param>
	/// <returns></returns>
	public abstract string MakeKey(DataRow it);
	/// <summary>
	/// データを設定
	/// </summary>
	/// <param name="table"></param>
	public void SetData(DataTable table)
	{
		foreach (DataRow it in table.Rows)
		{
			this.Set(MakeKey(it), it);
		}
	}
}

public class AnkenCache : SelectCacheTableRow
{
	/// <summary>
	/// キー情報を作成
	/// </summary>
	/// <param name="t"></param>
	/// <returns></returns>
	public override string MakeKey(DataRow row)
	{
		return row["id"].ToString();
	}
}

■テンプレートを使う

上記の例だと、DataRow しか扱えないので、単純なデータクラスを使いたい場合(こっちのほうがお勧めなんだが)、テンプレート(正確にはジェネリック)版を使います。「Project」というプロパティだけのクラスを作っておいて、プロパティ名でアクセスできるようにします。DataRow からデータクラスへのコンバートは、リフレクションを使ったバージョンを用意しておくと(型なしDataTableから型付きDataTableにコピーする方法 | Moonmile Solutions Blog 参照)いいでしょう。

public abstract class SelectCache<T> where T: new()
{
	protected Dictionary<string, T> _dic;
	public T Empty { get; set; }

	public SelectCache()
	{
		_dic = new Dictionary<string, T>();
		this.Empty = new T(); // 空を設定
	}
	public void Set(string key, T value)
	{
		if (_dic.ContainsKey(key) == true)
		{
			_dic[key] = value;
		}
		else
		{
			_dic.Add(key, value);
		}
	}
	public T Get(string key)
	{
		if (_dic.ContainsKey(key) == true)
		{
			return _dic[key];
		}
		else
		{
			return this.Empty;
		}
	}
	/// <summary>
	/// キー情報を作成
	/// </summary>
	/// <param name="t"></param>
	/// <returns></returns>
	public abstract string MakeKey(T it);
	/// <summary>
	/// データを設定
	/// </summary>
	/// <param name="table"></param>
	public void SetData(List<T> items)
	{
		foreach (T it in items)
		{
			this.Set(MakeKey(it) , it);
		}
	}
}
public class Project
{
	public int id { get; set; }
	public int projnum { get; set; }
	public string name { get; set; }
	public int parent_id { get; set; }
	public string desc { get; set; }
	public DateTime updatedate { get; set; }
}
public class ProjectCache : SelectCache<Project>
{
	// キー情報を作成
	public override string MakeKey(Project it)
	{
		return it.id.ToString() + "_" + it.projnum.ToString();
	}
}

Empty プロパティを null にしないのは、以下のような書き方でもエラーにならないようにするためです。DataRow の場合は名前がマッチングしないと例外を発生しますが、こちらの場合は1行で書けるから便利かと。if文でいちいちチェックしなくてよいので。

string name = cache.Get( key ).name ;

敢えて空を調べたいときは、以下のように書きます。

string name = "";
if ( cache.Get(key) != cache.Empty ) {
  name = cache.Get( key ).name ;
}
カテゴリー: C# パーマリンク