簡易ORMフレームワークを作成してみる(2)
検索条件構築クラス(QueryObject)
まずは、検索条件構築クラスを作成していきたいと思います。
この検索条件クラスを導入するメリットは、データアクセスクラスの汎用化を進めてくれます。
データアクセスの条件をデータアクセスを行うメソッドの引数で表現することが良くあるのですが、この場合条件が異なるとメソッドを分けるようなことになり、個別のコードが多くなり共通化が促進されません。検索条件クラスを導入すると汎用的に検索条件を表現できるため、ビジネス層またはUI層で様々な検索条件を作成することが可能になります。データアクセスクラスに検索条件を受け入れる検索メソッドを1つ作成すれば多くの検索パターンに対応することができます。なお、(抽象化された)検索条件の組み合わせは本来ビジネスロジックの責務と考えているので、検索条件をビジネス層で利用することはすごく自然なことだと思っています。
ということで、検索条件構築クラスの最初に目標として以下のようなシナリオを考えます
[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); }
まず、今回の主役である検索条件を表すクラスですがこれは、QueryObjectとします。PoEAAで利用されているパターン名を拝借しました。このQueryObjectは検索条件のための複合的な情報を管理するのが責務になります。たとえば、フィルタ条件、ソート条件、検索件数などを管理します。
次に、個々のフィルタなどの検索オペレータについては、いろいろバリエーションが考えれるため抽象クラスのCriteriaクラスを導入します。このクラスは個々の検索オペレータに対する情報を管理します。バリエーションとしては、最初の目標である等号などのフィルタを表現するFilterCriteriaのほか、AND、OR、NOTの検索オペレータ、さらにサブクエリなども含めて追加していくことになります。
最後は、実際にSQL文に利用できる検索条件文を構築するクラスが必要になります。単純なサンプルではQueryObjectに構築の責務を負わせることもあるようですが、実際には対象とするRDBMSで生成する検索条件文を微妙に変えることが必要になります。たとえば、パラメータのプレフィックスがSQL Serverは@でOracleは:であるとか、エスケープ用の文字列が異なるなどがあります。このため、検索条件文を構築するクラスをQueryObjectBuilderクラスとして分離して、RDBMSのバリエーションの1つとしてSQL Server用のSqlQueryObjectBuilderを導入します。これで、QueryObjectはRDBMSに依存しない汎用的なクラスとして利用することができます。
もう少し具体化してみます。
まずは、個別のバリエーションを除いたクラスの関連が分かるレベルのコードを見てみたいと思います。QueryObjectは、とりあえず最初の目標では、検索オペレータのみ指定できるだけ良いのでCriteriaを保持するだけで十分です。
[Serializable] public class QueryObject { private Criteria _rootFilter; public Criteria Filter { get { return _rootFilter; } set { _rootFilter = value; } } }
Criteriaクラスは、まずは抽象クラスとして考えた場合、具象クラスのさまざまな情報から検索条件文を生成できれば良いのでGenerateQueryStatementをメソッドとして定義します。ただし、実際の生成処理のRDBMSに依存する部分の処理はQueryObjectBuilderに任せることになるので、生成用の構築クラスQueryObjectBuilderを外部からもらうようにします。
public abstract class Criteria { public abstract string GenerateQueryStatement(QueryObjectBuilder builder); }
QueryObjectBuilderクラスは、QueryObjectからSQL文の検索条件文を構築するクラスです。コンストラクタの引数で指定されたQueryObjectの情報を元に生成し、構築された検索文やパラメータは参照にだけ利用されることになります。*1
実際の検索文を生成する処理(コンストラクタ)を行うと、QueryObjectBuilderの派生クラスはCriteriaの派生クラスと行ったり来たりするような動作をしながら最終的な検索条件を作成することになります。(VISTORパターン)
public abstract class QueryObjectBuilder { private QueryObject _item; private string _queryText; private string _filterText; private List_parameters = new List (); public QueryObjectBuilder(QueryObject item) { _item = item; GenerateQueryStatement(); } public QueryObject Item { get { return _item; } } public string QueryText { get { return _queryText; } } public List Parameters { get { return _parameters; } } //特定のRDBMSで利用可能な検索文全体を生成する protected void GenerateQueryStatement() { } }
*1:できればイミュータブルにしたほうが良いかな