web-dev-qa-db-ja.com

Entity Framework 6 Code First-リポジトリ実装は良いものですか?

リポジトリと作業単位でEntity Framework 6の設計を実装しようとしています。

周りに非常に多くの記事があり、最良のアドバイスが何であるかわかりません:たとえば、ここで実装されているパターンが本当に好きです:記事で提案されている理由のため here

ただし、Tom Dykstra (Senior Programming Writer on Microsoft's Web Platform & Tools Content Team)は、別の記事で行う必要があることを示唆しています。 here

私はPluralsightをサブスクライブしますが、コースで使用されるたびにわずかに異なる方法で実装されるため、デザインの選択は困難です。

一部の人々は、この post のように、作業単位がDbContextによってすでに実装されていることを示唆しているように見えるため、実装する必要はまったくありません。

このタイプの質問は以前に尋ねられたものであり、これは主観的かもしれないことを理解していますが、私の質問は直接的です:

私は最初の(Code Fizzle)記事のアプローチが好きで、他のアプローチと同じくらい保守性があり、テストが容易で、安全であるかどうかを知りたいですか?

他のビューは歓迎です。

49
davy

@Chris Hardieは正しいです。EFはすぐにUoWを実装します。しかし、多くの人々は、EFがすぐに使える汎用リポジトリパターンも実装しているという事実を見落としています。

var repos1 = _dbContext.Set<Widget1>();
var repos2 = _dbContext.Set<Widget2>();
var reposN = _dbContext.Set<WidgetN>();

...そしてこれは、ツール自体に組み込まれている非常に優れた汎用リポジトリ実装です。

DbContextが必要なものすべてを提供してくれるのに、なぜ他のインターフェースやプロパティを大量に作成するのに苦労するのでしょうか?アプリケーションレベルのインターフェイスの背後でDbContextを抽象化し、コマンドクエリの分離を適用する場合は、次のような簡単な操作を実行できます。

public interface IReadEntities
{
    IQueryable<TEntity> Query<TEntity>();
}

public interface IWriteEntities : IReadEntities, IUnitOfWork
{
    IQueryable<TEntity> Load<TEntity>();
    void Create<TEntity>(TEntity entity);
    void Update<TEntity>(TEntity entity);
    void Delete<TEntity>(TEntity entity);
}

public interface IUnitOfWork
{
    int SaveChanges();
}

これら3つのインターフェイスをすべてのエンティティアクセスに使用でき、3つ以上のエンティティセットで機能するビジネスコードに3つ以上の異なるリポジトリを挿入することを心配する必要はありません。もちろん、IoCを使用してWebリクエストごとにDbContextインスタンスが1つだけであることを保証しますが、3つのインターフェースはすべて同じクラスによって実装されるため、簡単になります。

public class MyDbContext : DbContext, IWriteEntities
{
    public IQueryable<TEntity> Query<TEntity>()
    {
        return Set<TEntity>().AsNoTracking(); // detach results from context
    }

    public IQueryable<TEntity> Load<TEntity>()
    {
        return Set<TEntity>();
    }

    public void Create<TEntity>(TEntity entity)
    {
        if (Entry(entity).State == EntityState.Detached)
            Set<TEntity>().Add(entity);
    }

    ...etc
}

これで、必要なエンティティの数に関係なく、単一のインターフェイスを依存関係に挿入するだけで済みます。

// NOTE: In reality I would never inject IWriteEntities into an MVC Controller.
// Instead I would inject my CQRS business layer, which consumes IWriteEntities.
// See @MikeSW's answer for more info as to why you shouldn't consume a
// generic repository like this directly by your web application layer.
// See http://www.cuttingedge.it/blogs/steven/pivot/entry.php?id=91 and
// http://www.cuttingedge.it/blogs/steven/pivot/entry.php?id=92 for more info
// on what a CQRS business layer that consumes IWriteEntities / IReadEntities
// (and is consumed by an MVC Controller) might look like.
public class RecipeController : Controller
{
    private readonly IWriteEntities _entities;

    //Using Dependency Injection 
    public RecipeController(IWriteEntities entities)
    {
        _entities = entities;
    }

    [HttpPost]
    public ActionResult Create(CreateEditRecipeViewModel model)
    {
        Mapper.CreateMap<CreateEditRecipeViewModel, Recipe>()
            .ForMember(r => r.IngredientAmounts, opt => opt.Ignore());

        Recipe recipe = Mapper.Map<CreateEditRecipeViewModel, Recipe>(model);
        _entities.Create(recipe);
        foreach(Tag t in model.Tags) {
            _entities.Create(tag);
        }
        _entities.SaveChanges();
        return RedirectToAction("CreateRecipeSuccess");
    }
}

この設計に関する私のお気に入りの1つは、consumerのエンティティストレージ依存性を最小限に抑えることです。この例では、RecipeControllerはコンシューマーですが、実際のアプリケーションでは、コンシューマーはコマンドハンドラーになります。 (クエリハンドラの場合、通常はIReadEntitiesを消費します。これは、状態を変更せずにデータを返すだけであるためです。)しかし、この例では、RecipeControllerを調べるコンシューマとして使用します。依存関係の意味:

上記のアクションのために書かれた一連の単体テストがあるとします。これらの各ユニットテストでは、コントローラーを更新し、モックをコンストラクターに渡します。次に、新しいレシピを作成するときに、ユーザーが新しいクックブックを作成したり、既存のクックブックに追加したりできるようにしたいと顧客が決めたと言います。

エンティティごとのリポジトリまたは集約ごとのリポジトリインターフェイスパターンでは、新しいリポジトリインスタンスを挿入する必要がありますIRepository<Cookbook>をコントローラーコンストラクターに追加します(または@Chris Hardieの回答を使用して、UoWインスタンスにさらに別のリポジトリをアタッチするコードを記述します)。これにより、他のすべての単体テストがすぐに壊れるので、それらすべての構成コードを変更し、さらに別のモックインスタンスを渡し、依存関係配列を広げる必要があります。ただし、上記を使用すると、他のすべての単体テストは少なくともコンパイルされます。あなたがしなければならないのは、新しいクックブック機能をカバーする追加のテストを書くことです。

46
danludwig

Codefizzle、Dykstaの記事、および以前の回答がwrongであると言って残念ではありません。ドメイン(ビジネス)オブジェクトとしてEFエンティティを使用するという単純な事実は、大きなWTFです。

更新:あまり技術的でない説明(平易な言葉で)を読む ダミーのリポジトリパターン

簡単に言うと、リポジトリインターフェイスを永続性(ORM)の詳細に結合しないでください。リポジトリインターフェースは、アプリの残りの部分(ドメイン、おそらくプレゼンテーションのようなUI)にとって意味のあるオブジェクトのみを扱います。多くの人々(MSがパックを率いており、意図は疑っています)は、自分のEFエンティティを再利用できる、またはビジネス上のオブジェクトになる可能性があると誤解しています。

canが発生する可能性はありますが、非常にまれです。実際には、データベースルール、つまり不適切なモデリングの後に「設計」された多くのドメインオブジェクトがあります。リポジトリの目的は、アプリの残りの部分(主にビジネスレイヤー)を永続化フォームから分離することです。

リポジトリがEFエンティティ(永続性の詳細)を処理する場合、またはそのメソッドがこの目的のために間違ったセマンティクスを持つリークしている抽象化であるIQueryableを返すときにどのように分離しますかリポジトリの目的と機能を無効にしますか?)

Dominオブジェクトは、永続性、EF、結合などについて決して知るべきではありません。どのdbエンジンを使用しているか、または使用しているのかどうかを知りません。永続性の詳細からdecoupledにする場合は、アプリの他の部分と同じです。

リポジトリインターフェースは、上位層が知っていることのみを知っています。つまり、汎用ドメインリポジトリインターフェースは次のようになります。

public interface IStore<TDomainObject> //where TDomainObject != Ef (ORM) entity
{
   void Save(TDomainObject entity);
   TDomainObject Get(Guid id);
   void Delete(Guid id);
 }

implementationはDALに常駐し、EFを使用してdbを操作します。ただし、実装は次のようになります

public class UsersRepository:IStore<User>
 {
   public UsersRepository(DbContext db) {}


    public void Save(User entity)
    {
       //map entity to one or more ORM entities
       //use EF to save it
    }
           //.. other methods implementation ...

 }

concrete汎用リポジトリーは実際にはありません。具体的な汎用リポジトリの唯一の使用法は、任意のドメインオブジェクトがテーブルのようなキー値にシリアル化された形式で格納されている場合です。 ORMには当てはまりません。

クエリはどうですか?

 public interface IQueryUsers
 {
       PagedResult<UserData> GetAll(int skip, int take);
       //or
       PagedResult<UserData> Get(CriteriaObject criteria,int skip, int take); 
 }

UserDataは、クエリコンテキストの使用に適した読み取り/表示モデルです。

DALがビューモデルについて知っていることを気にしない場合は、 クエリハンドラ でクエリに直接EFを使用できます。その場合、クエリリポジトリは必要ありません。

結論

  • あなたのビジネスオブジェクトはEFエンティティについて知るべきではありません。
  • リポジトリはORMを使用しますが、ORMを決して公開しませんアプリの残りの部分。したがって、レポインターフェースはドメインオブジェクトまたはビューモデル(または永続性の詳細ではない他のアプリコンテキストオブジェクト)のみを使用します。
  • リポジトリhowにその作業を行うように指示しません。つまり、IQueryableをリポジトリインターフェースで使用しないでください。
  • より簡単/クールな方法でdbを使用したいだけで、懸念の分離を維持する必要がない(それについて)シンプルなCRUDアプリを扱っている場合は、skipリポジトリをすべて一緒に、すべてのデータに直接EFを使用します。このアプリはEFと緊密に結合されますが、少なくともあなたは中間者を削減し、それは間違いではなく意図的に行われます。

誤った方法でリポジトリを使用すると、その使用が無効になり、アプリは永続性(ORM)と密接に結合されることに注意してください。

ドメインオブジェクトを魔法のように保存するためにORMが存在すると信じる場合は、そうではありません。 ORMの目的は、リレーショナルテーブルの上のOOPストレージをシミュレートすることです。永続性とドメインとは関係がないため、永続性の外部でORMを使用しないでください。

42
MikeSW

DbContextは実際に作業単位パターンで構築されています。これにより、すべてのエンティティが作業するときに同じコンテキストを共有できます。この実装は、DbContextに対して内部です。

ただし、2つのDbContextオブジェクトをインスタンス化した場合、どちらもそれぞれが追跡している相手のエンティティを認識しないことに注意してください。それらは互いに絶縁されているため、問題が生じる可能性があります。

MVCアプリケーションを構築するとき、リクエストの過程で、すべてのデータアクセスコードが単一のDbContextで機能することを確認したいと思います。それを実現するために、作業ユニットをDbContextの外部パターンとして適用します。

これが、私が作成しているバーベキューレシピアプリの作業単位オブジェクトです。

public class UnitOfWork : IUnitOfWork
{
    private BarbecurianContext _context = new BarbecurianContext();
    private IRepository<Recipe> _recipeRepository;
    private IRepository<Category> _categoryRepository;
    private IRepository<Tag> _tagRepository;

    public IRepository<Recipe> RecipeRepository
    {
        get
        {
            if (_recipeRepository == null)
            {
                _recipeRepository = new RecipeRepository(_context);
            }
            return _recipeRepository;
        }
    }

    public void Save()
    {
        _context.SaveChanges();
    }
    **SNIP**

同じDbContextでインジェクトされるすべてのリポジトリを作業ユニットオブジェクトにアタッチします。作業ユニットオブジェクトからリポジトリが要求される限り、すべてのデータアクセスコードが同じDbContext-素晴らしいソースで管理されることを保証できます!

これをMVCアプリで使用する場合、コントローラーでインスタンス化し、アクション全体で使用することで、リクエスト全体で作業ユニットが使用されるようにします。

public class RecipeController : Controller
{
    private IUnitOfWork _unitOfWork;
    private IRepository<Recipe> _recipeService;
    private IRepository<Category> _categoryService;
    private IRepository<Tag> _tagService;

    //Using Dependency Injection 
    public RecipeController(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
        _categoryRepository = _unitOfWork.CategoryRepository;
        _recipeRepository = _unitOfWork.RecipeRepository;
        _tagRepository = _unitOfWork.TagRepository;
    }

これで、アクションで、すべてのデータアクセスコードが同じDbContextを使用することが保証されます。

    [HttpPost]
    public ActionResult Create(CreateEditRecipeViewModel model)
    {
        Mapper.CreateMap<CreateEditRecipeViewModel, Recipe>().ForMember(r => r.IngredientAmounts, opt => opt.Ignore());

        Recipe recipe = Mapper.Map<CreateEditRecipeViewModel, Recipe>(model);
        _recipeRepository.Create(recipe);
        foreach(Tag t in model.Tags){
             _tagRepository.Create(tag); //I'm using the same DbContext as the recipe repo!
        }
        _unitOfWork.Save();
4
Mister Epic

インターネットで検索すると、これが見つかりました http://www.thereformedprogrammer.net/is-the-repository-pattern-useful-with-entity-framework/ これは、 Jon Smithによるリポジトリパターン。 2番目の部分では、ソリューションに焦点を当てます。それが役に立てば幸い!

3

作業単位パターンを実装したリポジトリは、質問に答えるのに悪いものです。

エンティティフレームワークのDbContextは、作業単位パターンに従ってMicrosoftによって実装されます。つまり、context.SaveChangesは変更をトランザクションで一度に保存します。

DbSetは、Repositoryパターンの実装でもあります。ただできるリポジトリを構築しないでください:

void Add(Customer c)
{
   _context.Customers.Add(c);
}

とにかくサービス内でできることのためのワンライナーメソッドを作成します???

利益はなく、誰もEF ORMを最近別のORMに変更していません...

あなたはその自由を必要としません...

Chris Hardieは、複数のコンテキストオブジェクトをインスタンス化できる可能性があると主張していますが、すでにこれを行っているので、間違っています...

必要なIOCツールを使用し、HttpリクエストごとにMyContextを設定するだけで問題ありません。

Ninjectを例に取ります:

kernel.Bind<ITeststepService>().To<TeststepService>().InRequestScope().WithConstructorArgument("context", c => new ITMSContext());

ビジネスロジックを実行するサービスは、コンテキストを注入します。

単純に愚かにしてください:-)

2
Pascal

代替手段として「コマンド/クエリオブジェクト」を検討する必要があります。この分野に関する興味深い記事がたくさんありますが、ここでは良い記事を紹介します。

https://rob.conery.io/2014/03/03/repositories-and-unitofwork-are-not-a-good-idea/

複数のDBオブジェクトにわたるトランザクションが必要な場合は、コマンドごとに1つのコマンドオブジェクトを使用して、UOWパターンの複雑さを回避してください。

ほとんどのプロジェクトでは、クエリごとのクエリオブジェクトはおそらく不要です。代わりに、「FooQueries」オブジェクトで開始することを選択できます。つまり、READSのリポジトリパターンから開始できますが、それを明示するために「Queries」という名前を付けます。挿入/更新は行わないでください。

後で、認可やロギングなどを追加したい場合、個々のクエリオブジェクトを分割する価値があるとmight見つけます。クエリオブジェクトをパイプラインにフィードできます。

1
Darren

私は常にEFコードでUoWを最初に使用します。メモリリークなどを防ぐために、コンテキストをより効率的かつ簡単に管理できると思います。私のgithubで回避策の例を見つけることができます: http://www.github.com/stefchri RADARプロジェクトで。

質問がある場合は、お気軽にお問い合わせください。

0
Gecko IT