適切に設計されたクエリコマンドや仕様


90

私はかなりの時間をかけて、典型的なリポジトリパターン(特殊なクエリのメソッドのリストの増加など)によって提示される問題の適切な解決策を探していました。http//ayende.com/blog/3955/repository-を参照してください。 is-the-new-singleton)。

特に仕様パターンを使用することで、コマンドクエリを使用するアイデアが本当に気に入っています。ただし、仕様に関する私の問題は、単純な選択(基本的にはwhere句)の基準にのみ関連し、結合、グループ化、サブセットの選択または射影などのクエリの他の問題を処理しないことです。基本的に、多くのクエリが正しいデータセットを取得するために通過しなければならないすべての追加のフープ。

(注:クエリオブジェクトとも呼ばれるコマンドパターンのように、「コマンド」という用語を使用します。クエリとコマンド(更新、削除、インサート))

したがって、クエリ全体をカプセル化する代替手段を探していますが、それでも、スパゲッティリポジトリをコマンドクラスの急増に交換するだけではないほど十分に柔軟性があります。

私は、たとえばLinqspecsを使用しましたが、選択基準に意味のある名前を割り当てることができることに価値があると感じましたが、それだけでは不十分です。おそらく、私は複数のアプローチを組み合わせた混合ソリューションを探しています。

この問題に対処するため、または別の問題に対処するために他の人が開発した可能性のある解決策を探していますが、これらの要件は満たしています。リンクされた記事では、AyendeがnHibernateコンテキストを直接使用することを提案していますが、クエリ情報も含める必要があるため、ビジネスレイヤーが大幅に複雑になっているように感じます。

待機期間が過ぎ次第、私はこれについて報奨金を提供します。だから、良い説明を付けてあなたの解決策を報奨に値するものにしてください、そして私は最良の解決策を選び、そしてランナーを賛成します。

注:私はORMベースの何かを探しています。EFやnHibernateを明示的に指定する必要はありませんが、これらは最も一般的であり、最も適しています。それが他のORMに簡単に適用できる場合は、ボーナスになります。Linq互換性もいいです。

更新:ここに良い提案があまりないことに本当に驚いています。人々は完全にCQRSであるか、完全にリポジトリキャンプにいるようです。私のアプリのほとんどは、CQRSを保証するほど複雑ではありません(ほとんどのCQRS擁護者が使用するべきではないとすぐに言っています)。

更新:ここで少し混乱があるようです。私は新しいデータアクセステクノロジーを探しているのではなく、ビジネスとデータ間の適切に設計されたインターフェイスを探しています。

理想的には、私が探しているのは、クエリオブジェクト、仕様パターン、およびリポジトリの間のある種のクロスです。上で述べたように、仕様パターンはwhere句の側面のみを扱い、結合や副選択などのクエリの他の側面は扱いません。リポジトリはクエリ全体を扱いますが、しばらくすると手に負えなくなります。クエリオブジェクトもクエリ全体を処理しますが、単純にリポジトリをクエリオブジェクトの爆発に置き換えたくありません。


5
素晴らしい質問です。私も私が提案している以上の経験を持つ人々を見たいと思っています。現在、汎用リポジトリにCommandオブジェクトまたはQueryオブジェクトのオーバーロードが含まれているコードベースに取り組んでいます。その構造は、Ayendeが彼のブログで説明しているものと似ています。PS:これは、programmers.SEにも注意を引くかもしれません。
Simon Whitehead

LINQへの依存を気にしない場合は、なぜIQueryableを公開するリポジトリを使用しないのですか?一般的なアプローチは、一般的なリポジトリであり、上記の再利用可能なロジックが必要な場合は、追加のメソッドを使用して派生リポジトリタイプを作成します。
devdigital 2013年

@devdigital-Linqへの依存性は、データ実装への依存性と同じではありません。オブジェクトにLinqを使用したいので、他のビジネスレイヤー機能を並べ替えたり実行したりできます。しかし、それは、データモデルの実装に依存したいという意味ではありません。ここで私が本当に話しているのは、レイヤー/層インターフェースです。例として、クエリを変更できるようにしたいのですが、200か所で変更する必要はありません。これは、IQueryableをビジネスモデルに直接プッシュした場合に起こります。
エリックファンケンブッシュ2013年

1
@devdigital-基本的に、リポジトリの問題をビジネスレイヤーに移動します。あなたはただ問題を回避しているだけです。
Erik Funkenbusch 2013年

回答:


94

免責事項:すばらしい回答はまだないため、しばらく前に読んだすばらしいブログの投稿の一部を投稿することにしました。完全なブログ投稿はここにあります。だからここにあります:


次の2つのインターフェースを定義できます。

public interface IQuery<TResult>
{
}

public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
    TResult Handle(TQuery query);
}

IQuery<TResult>指定し、それが使用して返すデータを特定のクエリを定義するメッセージTResultジェネリック型を。以前に定義したインターフェースを使用して、次のようなクエリメッセージを定義できます。

public class FindUsersBySearchTextQuery : IQuery<User[]>
{
    public string SearchText { get; set; }
    public bool IncludeInactiveUsers { get; set; }
}

このクラスは、Userオブジェクトの配列になる2つのパラメーターを持つクエリ操作を定義します。このメッセージを処理するクラスは、次のように定義できます。

public class FindUsersBySearchTextQueryHandler
    : IQueryHandler<FindUsersBySearchTextQuery, User[]>
{
    private readonly NorthwindUnitOfWork db;

    public FindUsersBySearchTextQueryHandler(NorthwindUnitOfWork db)
    {
        this.db = db;
    }

    public User[] Handle(FindUsersBySearchTextQuery query)
    {
        return db.Users.Where(x => x.Name.Contains(query.SearchText)).ToArray();
    }
}

これで、消費者に汎用IQueryHandlerインターフェースに依存させることができます。

public class UserController : Controller
{
    IQueryHandler<FindUsersBySearchTextQuery, User[]> findUsersBySearchTextHandler;

    public UserController(
        IQueryHandler<FindUsersBySearchTextQuery, User[]> findUsersBySearchTextHandler)
    {
        this.findUsersBySearchTextHandler = findUsersBySearchTextHandler;
    }

    public View SearchUsers(string searchString)
    {
        var query = new FindUsersBySearchTextQuery
        {
            SearchText = searchString,
            IncludeInactiveUsers = false
        };

        User[] users = this.findUsersBySearchTextHandler.Handle(query);    
        return View(users);
    }
}

すぐに何を注入するかを決定できるので、このモデルはすぐに多くの柔軟性をもたらしますUserController。完全に異なる実装、または実際の実装をラップする実装を注入できUserControllerます。そのインターフェース(およびそのインターフェースの他のすべてのコンシューマ)を変更する必要はありません。

IQuery<TResult>インターフェイスは、私たちに指定するか、または注入コンパイル時にサポートできますIQueryHandlers我々のコードでは。我々は変更するとFindUsersBySearchTextQuery返すためにUserInfo[](実装することで、代わりにIQuery<UserInfo[]>)、UserController上のジェネリック型制約があるため、コンパイルに失敗しますIQueryHandler<TQuery, TResult>マッピングすることはできませんFindUsersBySearchTextQueryUser[]

注入IQueryHandlerしかし、消費者へのインタフェースを、まだ対処する必要があるいくつかのあまり目立たない問題があります。コンシューマーの依存関係の数は非常に大きくなり、コンストラクターの過剰注入につながる可能性があります-コンストラクターが引数を取りすぎる場合。クラスが実行するクエリの数は頻繁に変化する可能性があり、コンストラクター引数の数を常に変更する必要があります。

IQueryHandlers抽象化のレイヤーを追加することで、注入しすぎる問題を解決できます。コンシューマーとクエリハンドラーの間に位置するメディエーターを作成します。

public interface IQueryProcessor
{
    TResult Process<TResult>(IQuery<TResult> query);
}

IQueryProcessor1つの汎用の方法で非汎用インタフェースです。インターフェース定義でわかるように、はインターフェースにIQueryProcessor依存していIQuery<TResult>ます。これにより、に依存するコンシューマでのコンパイル時サポートが可能になりIQueryProcessorます。UserControllernewを使用するようにを書き換えましょうIQueryProcessor

public class UserController : Controller
{
    private IQueryProcessor queryProcessor;

    public UserController(IQueryProcessor queryProcessor)
    {
        this.queryProcessor = queryProcessor;
    }

    public View SearchUsers(string searchString)
    {
        var query = new FindUsersBySearchTextQuery
        {
            SearchText = searchString,
            IncludeInactiveUsers = false
        };

        // Note how we omit the generic type argument,
        // but still have type safety.
        User[] users = this.queryProcessor.Process(query);

        return this.View(users);
    }
}

これUserControllerは、IQueryProcessorすべてのクエリを処理できるに依存しています。メソッドを呼び出し、初期化クエリオブジェクトを渡す方法を。以来実装インタフェースを、私たちはジェネリックにそれを渡すことができます方法。C#の型推論により、コンパイラーはジェネリック型を判別できるため、型を明示的に指定する必要がなくなります。メソッドの戻り値の型もわかっています。UserControllerSearchUsersIQueryProcessor.ProcessFindUsersBySearchTextQueryIQuery<User[]>Execute<TResult>(IQuery<TResult> query)Process

今ではIQueryProcessor、適切なものを見つけるのは実装の責任ですIQueryHandler。これには、動的なタイピングと、必要に応じて依存性注入フレームワークを使用する必要があり、すべて数行のコードで実行できます。

sealed class QueryProcessor : IQueryProcessor
{
    private readonly Container container;

    public QueryProcessor(Container container)
    {
        this.container = container;
    }

    [DebuggerStepThrough]
    public TResult Process<TResult>(IQuery<TResult> query)
    {
        var handlerType = typeof(IQueryHandler<,>)
            .MakeGenericType(query.GetType(), typeof(TResult));

        dynamic handler = container.GetInstance(handlerType);

        return handler.Handle((dynamic)query);
    }
}

QueryProcessorクラスは、特定の構築IQueryHandler<TQuery, TResult>された問合せインスタンスのタイプに基づいてタイプを。このタイプは、提供されたコンテナークラスにそのタイプのインスタンスを取得するように要求するために使用されます。残念ながら、Handleリフレクションを使用して(この場合はC#4.0動的キーワードを使用して)メソッドを呼び出す必要TQueryがあります。コンパイル時にジェネリック引数を使用できないため、この時点ではハンドラーインスタンスをキャストすることができないためです。ただし、Handleメソッドの名前を変更したり、他の引数を取得したりしない限り、この呼び出しは失敗しません。必要に応じて、このクラスの単体テストを作成するのは非常に簡単です。リフレクションを使用するとわずかに低下しますが、心配する必要はありません。


あなたの懸念の1つに答えるには:

したがって、クエリ全体をカプセル化する代替手段を探していますが、それでも、スパゲッティリポジトリをコマンドクラスの急増に交換するだけではないほど十分に柔軟性があります。

この設計を使用すると、システムに多数の小さなクラスが存在することになりますが、(明確な名前を持つ)多数の小さなクラスに焦点を当てることは良いことです。このアプローチは、1つのクエリクラスにグループ化できるので、リポジトリ内の同じメソッドに異なるパラメーターを持つ多くのオーバーロードがある場合よりも明らかに優れています。したがって、リポジトリ内のメソッドよりもはるかに少ないクエリクラスを取得できます。


2
賞を獲得したようです。私はコンセプトが好きです。誰かが本当に違うものを提示することを望んでいました。おめでとうございます。
エリックファンケンブッシュ2013年

1
@FuriCuri、単一のクラスは本当に5つのクエリを必要としますか?おそらく、それは責任が多すぎるクラスであると考えることができます。または、クエリが集計されている場合、実際には単一のクエリである必要があります。もちろん、これらは単なる提案です。
2013年

1
@stakx私の最初の例TResultでは、IQueryインターフェイスのジェネリックパラメーターが役に立たないことは間違いありません。しかし、私の更新された応答では、実行時にを解決するためにのメソッドでTResultパラメーターが使用されています。ProcessIQueryProcessorIQueryHandler
david.s 2013年

1
私はまた、非常によく似た実装のブログを持っています。これにより、私は正しい道を進んでいます。これはリンクjupaol.blogspot.mx/2012/11/…であり、PRODアプリケーションでしばらく使用しています。しかし、私はこのアプローチに問題がありました。クエリの連鎖と再利用より複雑なクエリを作成するために組み合わせる必要があるいくつかの小さなクエリがあるとしましょう。結局、コードを複製するだけになりましたが、より適切でクリーンな方法を探しています。何か案は?
Jupaol 2013

4
@Cemre私はクエリを拡張メソッドでカプセル化してIQueryable、コレクションを列挙しないようにしてQueryHandler、クエリを呼び出し/チェーンしました。これにより、クエリを単体テストしてチェーン化する柔軟性が得られました。私の上にQueryHandler
アプリケーション

4

私のそれに対処する方法は、実際には単純化されており、ORMに依存していません。リポジトリの私の見解はこれです:アプリはちょうどのためのレポを要求するように、リポジトリの仕事は、コンテキストに必要なモデルとアプリを提供することでどのようなことは望んでいるが、それを教えてくれない、それを取得します。

リポジトリメソッドに基準(はい、DDDスタイル)を指定します。これは、リポジトリ(または必要なものは何でも、Webサービスリクエストの場合もあります)を作成するためにリポジトリで使用されます。結合とグループimhoは、whatの詳細ではなく、whatの詳細であり、criteriaはwhere句を作成するための基本にすぎません。

モデル=アプリが必要とする最終的なオブジェクトまたはデータ構造。

public class MyCriteria
{
   public Guid Id {get;set;}
   public string Name {get;set;}
    //etc
 }

 public interface Repository
  {
       MyModel GetModel(Expression<Func<MyCriteria,bool>> criteria);
   }

おそらく、必要に応じてORM基準(Nhibernate)を直接使用できます。リポジトリの実装では、基になるストレージまたはDAOで基準を使用する方法を知っている必要があります。

ドメインとモデルの要件はわかりませんが、アプリ自体がクエリを作成するのが最善の方法であるとしたら奇妙です。モデルが大きく変化しているため、安定したものを定義できませんか?

このソリューションは明らかにいくつかの追加コードを必要としますが、残りの部分をORMまたはストレージへのアクセスに使用しているものに結合しません。リポジトリはファサードとして機能し、IMOはクリーンで「基準変換」コードは再利用可能です。


これは、リポジトリの増加の問題に対処せず、さまざまな種類のデータを返すためのメソッドのリストを常に拡大しています。これで問題は見られないかもしれませんが(多くの人はそうではありません)、他の人は異なる見方をしています(私がリンクした記事を読むことをお勧めします。同様の意見を持つ他の人がたくさんいます)。
Erik Funkenbusch 2013年

1
基準によって多くのメソッドが不要になるため、私はそれに対処します。もちろん、それらすべてについてではなく、あなたが必要とするものについて何も知らなければ、私は多くを言うことができません。私はあなたがデータベースを直接クエリしたいのですが、おそらくリポジトリが邪魔になっているという印象を受けています。Relational sotrageで直接作業する必要がある場合は、リポジトリを必要とせず、直接実行してください。そして、メモとして、何人の人がその投稿でアイエンデを引用しているのか迷惑です。私はそれに同意しません、そして多くの開発者がパターンを間違った方法で使用していると思います。
MikeSW 2013年

1
問題は多少軽減されるかもしれませんが、十分に大きなアプリケーションが与えられた場合でも、モンスターリポジトリが作成されます。メインロジックで直接nHibernateを使用するというAyendeの解決策には同意しませんが、制御不能なリポジトリの増加の不条理については同意します。データベースに直接クエリを実行する必要はありませんが、問題をリポジトリからクエリオブジェクトの急増に移動するだけではありません。
Erik Funkenbusch 2013年

2

私はこれを実行し、これをサポートし、これを元に戻しました。

主な問題はこれです。どのようにしても、追加された抽象化は独立性を獲得しません。当然のことながらリークします。本質的には、コードをかわいく見えるようにするためにレイヤー全体を発明していますが、メンテナンスを減らしたり、読みやすさを向上させたり、モデルにとらわれないタイプの知識を得たりはしません。

おもしろいのは、オリビエの回答に対して自分の質問に答えたことです。「これは、本質的には、Linqから得られるすべての利点なしに、Linqの機能を複製しています」。

自問してみてください:それはどうしてできないのでしょうか?


まあ、私は間違いなくLinqをビジネスレイヤーに統合する問題を経験しました。これは非常に強力ですが、データモデルを変更するときは悪夢です。ビジネスレイヤーに大きな影響を与えずにローカライズされた場所で変更を加えることができるので(変更をサポートするためにビジネスレイヤーも変更する必要がある場合を除いて)、物事はリポジトリで改善されます。ただし、リポジトリはこれらの肥大化したレイヤーになり、SRPに大幅に違反します。おっしゃるとおりですが、問題は解決しません。
エリックファンケンブッシュ2013年

データレイヤーがLINQを使用していて、データモデルの変更でビジネスレイヤーの変更が必要な場合...適切にレイヤー化されていません。
Stu

そのレイヤーはもう追加しないと言っていたと思いました。追加した抽象化によって何も得られないと言うときは、nHibernateセッション(またはEFコンテキスト)をビジネスレイヤーに直接渡すことについてAyendeと合意していることを意味します。
Erik Funkenbusch 2013年

1

流暢なインターフェースを使用できます。基本的な考え方は、クラスのメソッドは、なんらかのアクションを実行した後、現在のインスタンスをこのまさにこのクラスで返すということです。これにより、メソッド呼び出しを連鎖させることができます。

適切なクラス階層を作成することにより、アクセス可能なメソッドの論理フローを作成できます。

public class FinalQuery
{
    protected string _table;
    protected string[] _selectFields;
    protected string _where;
    protected string[] _groupBy;
    protected string _having;
    protected string[] _orderByDescending;
    protected string[] _orderBy;

    protected FinalQuery()
    {
    }

    public override string ToString()
    {
        var sb = new StringBuilder("SELECT ");
        AppendFields(sb, _selectFields);
        sb.AppendLine();

        sb.Append("FROM ");
        sb.Append("[").Append(_table).AppendLine("]");

        if (_where != null) {
            sb.Append("WHERE").AppendLine(_where);
        }

        if (_groupBy != null) {
            sb.Append("GROUP BY ");
            AppendFields(sb, _groupBy);
            sb.AppendLine();
        }

        if (_having != null) {
            sb.Append("HAVING").AppendLine(_having);
        }

        if (_orderBy != null) {
            sb.Append("ORDER BY ");
            AppendFields(sb, _orderBy);
            sb.AppendLine();
        } else if (_orderByDescending != null) {
            sb.Append("ORDER BY ");
            AppendFields(sb, _orderByDescending);
            sb.Append(" DESC").AppendLine();
        }

        return sb.ToString();
    }

    private static void AppendFields(StringBuilder sb, string[] fields)
    {
        foreach (string field in fields) {
            sb.Append(field).Append(", ");
        }
        sb.Length -= 2;
    }
}

public class GroupedQuery : FinalQuery
{
    protected GroupedQuery()
    {
    }

    public GroupedQuery Having(string condition)
    {
        if (_groupBy == null) {
            throw new InvalidOperationException("HAVING clause without GROUP BY clause");
        }
        if (_having == null) {
            _having = " (" + condition + ")";
        } else {
            _having += " AND (" + condition + ")";
        }
        return this;
    }

    public FinalQuery OrderBy(params string[] fields)
    {
        _orderBy = fields;
        return this;
    }

    public FinalQuery OrderByDescending(params string[] fields)
    {
        _orderByDescending = fields;
        return this;
    }
}

public class Query : GroupedQuery
{
    public Query(string table, params string[] selectFields)
    {
        _table = table;
        _selectFields = selectFields;
    }

    public Query Where(string condition)
    {
        if (_where == null) {
            _where = " (" + condition + ")";
        } else {
            _where += " AND (" + condition + ")";
        }
        return this;
    }

    public GroupedQuery GroupBy(params string[] fields)
    {
        _groupBy = fields;
        return this;
    }
}

あなたはそれをこのように呼ぶでしょう

string query = new Query("myTable", "name", "SUM(amount) AS total")
    .Where("name LIKE 'A%'")
    .GroupBy("name")
    .Having("COUNT(*) > 2")
    .OrderBy("name")
    .ToString();

の新しいインスタンスのみを作成できますQuery。他のクラスには、保護されたコンストラクタがあります。階層のポイントは、メソッドを「無効にする」ことです。たとえば、GroupByメソッドGroupedQueryはの基本クラスでQueryあり、Whereメソッドを持たないa を返します(whereメソッドはで宣言されていますQuery)。したがって、のWhere後に呼び出すことはできませんGroupBy

しかし、それは完璧ではありません。このクラス階層を使用すると、メンバーを連続的に非表示にできますが、新しいメンバーは表示できません。したがってHaving、前に呼び出されたときに例外をスローしますGroupBy

Where複数回の呼び出しが可能であることに注意してください。これによりAND、既存の条件に新しい条件が追加されます。これにより、単一の条件からプログラムでフィルターを簡単に作成できます。同じことがで可能Havingです。

フィールドリストを受け入れるメソッドにはパラメータがありますparams string[] fields。単一のフィールド名または文字列配列を渡すことができます。


Fluentインターフェイスは非常に柔軟性があり、さまざまなパラメーターの組み合わせでメソッドのオーバーロードを多数作成する必要がありません。私の例は文字列で機能しますが、このアプローチは他のタイプにも拡張できます。特別な場合のために事前定義されたメソッドまたはカスタムタイプを受け入れるメソッドを宣言することもできます。ExecuteReaderまたはのようなメソッドを追加することもできますExceuteScalar<T>。これにより、次のようなクエリを定義できます

var reader = new Query<Employee>(new MonthlyReportFields{ IncludeSalary = true })
    .Where(new CurrentMonthCondition())
    .Where(new DivisionCondition{ DivisionType = DivisionType.Production})
    .OrderBy(new StandardMonthlyReportSorting())
    .ExecuteReader();

この方法で構築されたSQLコマンドでさえ、コマンドパラメータを持つことができ、SQLインジェクションの問題を回避すると同時に、データベースサーバーによってコマンドをキャッシュすることができます。これはO / R-mapperの代替ではありませんが、それ以外の場合は単純な文字列連結を使用してコマンドを作成する状況で役立ちます。


3
おもしろいですが、あなたの解決策はSQLインジェクションの可能性に問題があるように見え、プリコンパイルされた実行のために準備されたステートメントを実際には作成しません(したがって、実行速度が遅くなります)。おそらくそれらの問題を修正するためにそれを適応させることができますが、それからタイプセーフでないデータセットの結果とそうでないことに行き詰まっています。私はORMベースのソリューションを好み、おそらくそれを明示的に指定する必要があります。これは基本的にLinqの機能を複製するものであり、Linqから得られるすべての利点はありません。
エリックファンケンブッシュ2013年

私はこれらの問題を認識しています。これは、すばやく簡単なソリューションであり、流暢なインターフェイスを構築する方法を示しています。実際のソリューションでは、既存のアプローチをニーズに合わせた流暢なインターフェイスに「焼き付ける」ことでしょう。
Olivier Jacot-Descombes
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.