簡易ORMフレームワークを作成してみる(4)

SQL文の生成

QueryObjectから実際のSQL文を生成する基本的には責務はQueryObjectBuilderがもちます。
ただ、今回はCriteriaもRDBMSに依存しない範囲でフィルタ条件などの生成責務をもつようにすることでCriteriaの一般化と単純化を狙うようにします。なお、外部からは、この仕組み自体が隠蔽されているので、いつでもCriteriaの処理をQueryObjectBuilderに移動することができます。


以下は、QueryObjectBuilderのコンストラクタで指定されたQueryObjectから検索文が生成される処理部ですが、実際のSQL文の生成はQueryObjectで指定されているCriteriaに委任するようになっています。

public abstract class QueryObjectBuilder
{
	public QueryObjectBuilder(QueryObject item)
	{
		_item = item;
		GenerateQueryStatement();
	}
	
	...
	
          //特定のRDBMSで利用可能な検索文全体を生成する
	protected void GenerateQueryStatement()
	{
		_parameters.Clear();
		System.Text.StringBuilder strBuilder = new System.Text.StringBuilder();
		_filterText = GenerateFilterStatement();
		strBuilder.Append(_filterText);
		_queryText = strBuilder.ToString();
	}
	
	protected virtual string GenerateFilterStatement()
	{
		if (Item.Filter == null) return string.Empty;
		string filterString = Item.Filter.GenerateQueryStatement(this);
		if (filterString == string.Empty) return string.Empty;
		return string.Format(" Where {0}", filterString);
	}
}

委任された個々のCriteriaはさまざまな処理を行います。等号条件を表現するFilterCriteriaの場合、再度QueryObjectBuilderのFilterCriteriaからSQL文を生成処理を呼び出します。

public abstract class QueryObjectBuilder
{
	public abstract DbProviderAdapter Adapter
	{
		get;
	}
	...
	
	//個別の検索フィルタから特定のRDBMSで利用可能な検索文を生成する
	public virtual string GenerateFilterCriteria(FilterCriteria item)
	{
		if (item.Parameters == null)
		{
			return string.Format(item.Expression, 
				Adapter.GetColumnExpression(item.ColumnName));
		}
		else
		{
			List paraName = new List();
			paraName.Add(Adapter.GetColumnExpression(item.ColumnName));
			foreach (object para in item.Parameters)
			{
				DbParameter sqlPara = Adapter.CreateParameter();
				string paraString = string.Format("p{0}", Parameters.Count + 1);
				sqlPara.ParameterName = Adapter.GetParameterName(paraString);
				sqlPara.Value = para;
				sqlPara.Direction = ParameterDirection.Input;
				paraName.Add(sqlPara.ParameterName);
				Parameters.Add(sqlPara);
			}
			return string.Format(item.Expression, paraName.ToArray());
		}
	}
}

実際のSQL文を生成するためには、ターゲットなるRDBMSを意識する処理する必要があります。ただ、.NET BCLには複数のRDBMSを一般化して処理仕組みを持っているいます。その1つのDbProviderFactoryは、RDBMSに依存した各種インスタンスを生成するための一般化したファクトリを持っており、このクラスを利用すると、特定のRDBMSのライブラリに依存しないでADO.NETの処理ロジックを記述することができます。ただ、今回はこれ以外にもパラメータのプレフィックスなどいくつか一般化する必要があることが分かっており、DbProviderAdapterクラスにラップして利用するようにします。

public abstract class DbProviderAdapter
{
	protected abstract DbProviderFactory Factory
	{ 
		get;
	}
	public abstract string GetParameterName(string parameterName);
	
	public DbParameter CreateParameter()
	{
		return Factory.CreateParameter();
	}

	public virtual string GetColumnExpression(string columnName)
	{
		return columnName;
	}
}

最初のシナリオはSQL Serverをターゲットにしているので、SQL Server用をこの時点で作成しておきます。パラメータ名のプレフィックスとして@を付与し、カラム名の[]で括るように指定しました。

class SqlDbProviderAdapter : DbProviderAdapter
{
	protected override DbProviderFactory Factory
	{
		get { return SqlClientFactory.Instance; }
	}

	public override string GetParameterName(string parameterName)
	{
		return ("@" + parameterName);
	}

	public override string GetColumnExpression(string columnName)
	{
		return "[" + base.GetColumnExpression(columnName) + "]";
	}
}

合わせて、QueryObjectBuilderのSQL Server版も作成します。

public class SqlQueryObjectBuilder : QueryObjectBuilder
{
	public SqlQueryObjectBuilder(QueryObject item)
		: base(item)
	{
	}

	public override DbProviderAdapter Adapter
	{
		get { return new SqlDbProviderAdapter(); }
	}
} 

これで取りあえず、最初の目標であった。以下のテストが通るようになります。

[Test]
public void 等号()
{
	QueryObject qo = new QueryObject();
	qo.Filter = qo.Eq("COLUMN",10);
	QueryObjectBuilder b = new SqlQueryObjectBuilder(qo);
	Assert.AreEqual(" Where [COLUMN] = @p1", b.QueryText);
	Assert.AreEqual(10, b.Parameters[0].Value);
}

つづく