92

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

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

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

だから私はクエリ全体をカプセル化する代替案を探していますが、それでも十分な柔軟性があるので、コマンドクラスの爆発的な増加のためにスパゲッティリポジトリを交換するだけではありません。

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

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

待機期間が経過するとすぐに、これに報奨金を提供します。ですから、あなたの解決策を価値のあるものにして、良い説明をしてください。私は最良の解決策を選び、次点者に賛成します。

注:ORMベースのものを探しています。明示的にEFまたはnHibernateである必要はありませんが、これらが最も一般的であり、最適です。他のORMに簡単に適応できるのであれば、それはボーナスになります。Linq互換もいいでしょう。

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

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

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

4

4 に答える 4

100

免責事項:まだ素晴らしい答えがないので、私はしばらく前に読んだ素晴らしいブログ投稿の一部をほぼ逐語的にコピーして投稿することにしました。あなたはここで完全なブログ投稿を見つけることができます。だからここにあります:


次の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; }
}

このクラスは、2つのパラメーターを使用してクエリ操作を定義します。これにより、Userオブジェクトの配列が生成されます。このメッセージを処理するクラスは、次のように定義できます。

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);
}

これIQueryProcessorは、1つのジェネリックメソッドを持つ非ジェネリックインターフェイスです。インターフェイス定義でわかるように、はインターフェイスIQueryProcessorによって異なりIQuery<TResult>ます。これにより、に依存するコンシューマーでコンパイル時のサポートを利用できますIQueryProcessorUserController新しいを使用するように書き直してみましょう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のメソッドは、初期化されたクエリオブジェクトを渡すメソッドを呼び出しUserControllerます。はインターフェースを実装しているので、それを汎用メソッドに渡すことができます。C#型推論のおかげで、コンパイラーはジェネリック型を判別できるため、型を明示的に指定する必要がなくなります。メソッドの戻りタイプも知られています。SearchUsersIQueryProcessor.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 dymamicキーワードを使用して)メソッドを呼び出す必要があります。これTQueryは、コンパイル時に汎用引数が使用できないため、現時点ではハンドラーインスタンスをキャストできないためです。ただし、Handleメソッドの名前が変更されるか、他の引数を取得しない限り、この呼び出しは失敗することはなく、必要に応じて、このクラスの単体テストを作成するのは非常に簡単です。リフレクションを使用するとわずかに低下しますが、実際に心配する必要はありません。


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

だから私はクエリ全体をカプセル化する代替案を探していますが、それでも十分な柔軟性があるので、コマンドクラスの爆発的な増加のためにスパゲッティリポジトリを交換するだけではありません。

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

于 2013-01-25T09:07:27.227 に答える
4

これに対処する私の方法は、実際には単純でORMに依存しません。リポジトリに対する私の見解は次のとおりです。リポジトリの仕事は、コンテキストに必要なモデルをアプリに提供することです。そのため、アプリはリポジトリに必要なものを要求するだけで、取得方法は指示しません。

リポジトリメソッドにCriteria(はい、DDDスタイル)を提供します。これは、リポジトリがクエリ(または必要なもの-Webサービスリクエストの場合もあります)を作成するために使用されます。結合とグループimhoは方法の詳細であり、whatではなく、基準は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でCriteriaを使用する方法を知っている必要があります。

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

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

于 2013-01-20T08:40:42.990 に答える
2

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

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

楽しい部分は、オリヴィエの応答に応じて自分の質問に答えたことです。「これは、Linqから得られるすべての利点なしに、本質的にLinqの機能を複製することです」。

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

于 2013-01-28T19:43:19.807 に答える
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。他のクラスには保護されたコンストラクターがあります。階層のポイントは、メソッドを「無効にする」ことです。たとえば、メソッドはの基本クラスであり、メソッドを持たないGroupByaを返します(whereメソッドはで宣言されています)。したがって、の後に呼び出すことはできません。GroupedQueryQueryWhereQueryWhereGroupBy

しかし、それは完璧ではありません。このクラス階層を使用すると、メンバーを連続して非表示にすることはできますが、新しいメンバーを表示することはできません。したがって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マッパーの代わりではありませんが、単純な文字列連結を使用してコマンドを作成する場合に役立ちます。

于 2013-01-20T02:12:52.313 に答える