昨日、グループの小さなパートナーはEFコアストレージモードの使用について話しました。私は前に外国人の大物が書いた記事を読んだことを思い出し、非常に参考価値があると感じ、今日翻訳されました。
原文:httwww.example.com
著者:ジョン·P·スミス
翻訳:精緻なコード農家-王亮
注:オリジナルは2018年2月に最初に公開され、2020年7月に最終更新されました。
本文:より:
私は2014年に倉庫モデルに関する最初の記事を書きましたが、今でも非常に人気のある記事です。この記事は、EF Coreの最近のリリースとEF Coreデータベースアクセスパターンに関するさらなる研究に基づいて、その記事の更新版です。
Original Analysing whReposuseful with ity Framework 2014年5月.
https://www.thereformedprogrammer.net/is-the-repository-pattern-useful-with-entity-framework/
First Four months on my to reing the Repos2014年10月.
https://www.thereformedprogrammer.net/is-the-repository-pattern-useful-with-entity-framework-part-2/
THIS ARTICLE: Is the repository pattern useful with Entity Framework Core?
1概要1概要
答えは“いいえ”です。EF Coreでは、倉庫/作業単位モード(略してRep/UoW)は動作しません。EF CoreはすでにRep/UoWモードを実装しているので、EF CoreにRep/UoWモードを追加しても無駄です。
より良いソリューションは、EF Coreを直接使用することです。EF Coreのすべての機能を使用して、高性能なデータベースアクセスを構築できます。
2本稿の目的
この記事の焦点は:
EFのRep/UoWモデルをどう思いますか?
EFでRep/UoWモードを使用する利点と欠点。
Rep/UoWモードをEFコアコードに置き換える3つの方法。
EF Coreデータベースアクセスコードを簡単に発見してリファクタリングする方法
EF Coreの単体テストについて
C#とEF 6.xまたはEF Coreライブラリに精通していると仮定します。EF Coreについては特に触れましたが、EF 6.xについても触れました。
3背景について
2013年には、ヘルスケアモデリングに特化した大規模なWebアプリケーションの開発に携わっていましたASP.NET MVC 4とEF 5を使用しました。後者は当時リリースされたばかりで、地理データを扱うSQL Spatial型をサポートしました。当時一般的だったデータベースアクセスモードはRep/UoWモードで、EF CoreとRep/UoWモードを使ったデータベースアクセスについてMicrosoftが2013年に書いた記事を参照してください。
- Implementing the Repository and Unit of Work Patterns in an ASP.NET MVC Application
Rep/UoWを使ってアプリを構築しましたが、開発中にこれが本当に痛いことがわかりました。私はいくつかの小さな問題を修正するためにリポジトリのコードを絶えず“微調整”しなければならず、そのたびに“微調整”すると他の何かが台無しになります。そこで、データベースアクセスコードをより良く実装する方法を研究しました。
とはいえ、私は2017年後半に新しい会社と契約し、EF 6.xアプリケーションのパフォーマンス問題を解決する手助けをしました。パフォーマンスの問題の大部分は、アプリケーションがRep/UoWモードを使用するために必要な遅延ロードによるものであることが判明しました。
スタートアッププロジェクトに関わったあるプログラマーがRep/UoWモデルを使っていて、同社の創業者と話していると、アプリのRep/UoW部分がかなり不透明で操作が難しいと感じたという。
4ストレージモデルの見方
Spatial Modeller®の設計を調査しているうちに、倉庫モデルを放棄する強い証拠を提供するブログ記事を見つけました。この種の記事の中で最も説得力があり、よく考えられたものは、“Top UnitOfWork Repositories Are Not a Good Idea”です。Rob Conery氏の主なポイントは、Rep/UoWはEntity Framework(EF)Db Contextが提供するものを繰り返しているだけであり、なぜ完璧なフレームワークを価値のない表面の下に隠すのかということです。ロブはそれを“過度に抽象的な愚かさ”と呼んだ。
“なぜEntity Frameworkはリポジトリパターンを無視するのか”というブログ記事を掲載しましたこの記事で、Isaac Abraham氏は、倉庫モデルはテストを容易にしておらず、それがすべきことの1つであると付け加えました。これはEF Coreではより実用的です。後で見ることができます。
彼らは正しいのか?
Rep/UoWモデルについての私の意見
Rep/UoWモデルの長所と短所を、できるだけ公平な方法で振り返ってみましょう。以下は私の見解です。
5.1 Rep/UoWモデルの利点
データベースアクセスコードを分離します。Warehouseスキーマの最大の利点は、すべてのデータベースアクセスコードがどこにあるかを知っていることです。さらに、リポジトリをカタログや注文処理ライブラリなどのいくつかのセクションに分割することが多いため、バグやパフォーマンス調整が必要な特定のクエリのコードを簡単に見つけることができます。これは間違いなく大きな利点です。
集約Aggregation。ドメイン駆動設計(DDD)は、他の関連するエンティティが含まれるルートエンティティを持つことを提案するシステム設計のアプローチです。私がEntity Framework Core in Actionで使った例は、Review Entity Collectionを持つBookエンティティです。これらのレビューは本に関連している場合にのみ意味があるので、DDDは本エンティティを通じてのみレビューを変更すべきだと述べています。Rep/UoWモードは、ブックリポジトリにコメントを追加/削除する方法を提供することでこれを行います。
複雑なT-SQLコマンドを非表示にする。EF CoreをバイパスしてT-SQLを使用する必要がある場合もあります。このタイプのアクセスは、高レベルから隠されていますが、メンテナンスやリファクタリングに役立つように簡単に見つける必要があります。Rob Coneryの投稿Command/Query Objectsもこの問題を処理できることに注意してください。
シミュレーション/テストが簡単。単一のリポジトリをシミュレートするのは簡単で、データベースの単体テストコードに簡単にアクセスできます。これは数年前には真実でしたが、今では他の方法があります。
“EF Coreを別のデータベースアクセスライブラリに置き換える”という提案はしていないことに気付くでしょう。これはRep/UoWの背後にあるアイデアの1つですが、a)データベースアクセスライブラリを置き換えるのは難しい、b)アプリケーションでそのような重要なライブラリを実際に交換するのでしょうか?
5.2 Rep/UoWモードの欠点
最初の3つのプロジェクトはすべてパフォーマンスを中心に展開します。効率的なRep/UoWを書けないと言っているわけではありませんが、大変な作業であり、多くの実装には固有のパフォーマンス問題がある(Microsoftの古いRep/UoW実装を含む)。以下は、私がRep/UoWモードで見つけた欠点のリストです。
性能--处理实体关系。一个资源库通常会返回一种类型的
IEnumerable/IQueryable结果,例如在微软的例子中的一个 Student 实体类。假设你想从 Student 的关系中显示信息,比如他们的地址?在这种情况下,仓储库中最简单的方法是使用懒加载来读取学生的地址实体,我看到人们经常这样做。问题是懒加载会导致每一个关系都要单独往返于数据库,这比把所有的数据库访问合并成一次数据库往返要慢。(另一种方法是有多个不同返回类型的查询方法,但这将使你的资源库变得非常大和麻烦--见第 4 点)。データが必要なフォーマットに適合していません。倉庫コンポーネントは通常データベースから作成されるため、返されるデータはサービスやユーザーが必要とする正確な形式ではない場合があります。リポジトリの出力を調整できるかもしれませんが、これは書かなければならない2番目のフェーズです。フロントエンドの近くでクエリを形成し、必要なデータの調整を含める方が良いと思います。
パフォーマンス-アップデート:Rep/UoWの多くの実装はEF Coreを隠そうとしていますが、すべての機能を利用していません。たとえば、Rep/UoWはEF CoreのUpdateメソッドを使用してエンティティを更新し、エンティティ内のすべてのプロパティを保存します。EF Coreに組み込まれている変更追跡機能を使用すると、変更されたプロパティのみが更新されます。
あまりにも一般的。Rep/UoWの魅力は、一般的なリポジトリを書いて、それを使ってディレクトリリポジトリ、注文処理リポジトリなどのすべてのサブリポジトリを構築できるということです。これにより、書く必要があるコードを最小限に抑えることができますが、私の経験では、汎用リポジトリは最初は機能しますが、物事が複雑になるにつれて、個々のリポジトリごとにより多くのコードを追加する必要があります。“The more reusable the code is the less usable it is”。- ニール·フォード
-
- 悪い点をまとめると、Rep/UoWはEF Coreを隠しているため、EF Coreの機能を使ってシンプルで効率的なデータベースアクセスコードを書くことはできません。
EF CoreでRep/UoWの利点を維持する方法
前の利点セクションでは、分離、集計、隠蔽、単体テストを挙げましたが、Rep/UoWは非常にうまくいきました。このセクションでは、EF Coreを直接使用するときに、分離、集約などの同じ機能を提供する優れたアーキテクチャ設計と組み合わせて、いくつかの異なるソフトウェアパターンとプラクティスについて説明します。
それぞれの利点の実装を説明し、それらを階層的なソフトウェアアーキテクチャに配置します。
- クエリー·オブジェクトデータベースの読み取りを分離して隠す方法
データベース·アクセスには4つのタイプがあります。追加、読み取り、更新、削除はCRUDと呼ばれます。私にとって、EF Coreでクエリと呼ばれている読み取りの部分は、構築とパフォーマンスの調整が最も難しいことが多いです。多くのアプリケーションは、購入する製品のリスト、やるべきことのリストなど、優れた迅速なクエリに依存しています。人々が思いついた解決策は、オブジェクトを検索することです。
私が最初にそれらに触れたのは、2013年のRob Coneryの記事(前述)で、Command/Query Objectsについて言及しています。ジミー·ボガードは2012年に“Favor Query Objects over repositories”という記事を発表した。. NETのIQueryable型と拡張メソッドを使用すると、RobとJimmyの例のようにクエリオブジェクトパターンを改善できます。
次のリストは、整数リストのソート方法を選択できるクエリオブジェクトの簡単な例を示しています。
public static class MyLinqExtension
{
public static IQueryable<int> MyOrder
(this IQueryable<int> queryable, bool ascending)
{
return ascending
? queryable.OrderBy(num => num)
: queryable.OrderByDescending(num => num);
}
}
下面这是这个 MyOrder 查询对象使用示例:
var numsQ = new[] { 1, 5, 4, 2, 3 }.AsQueryable();
var result = numsQ
.MyOrder(true)
.Where(x => x > 3)
.ToArray();
MyOrder 查询对象的工作原理是,IQueryable 类型持有一个命令列表,当我应用 ToArray 方法时,这些命令会被执行。在我的简单例子中,我没有使用数据库,但如果我们用应用程序的 DbContext 的 DbSet<T> 属性替换 numsQ 变量,那么 IQueryable<T> 类型中的命令将被转换为数据库命令。
因为 IQueryable<T> 类型直到最后才被执行,所以你可以将多个查询对象连锁起来。让我从我的书《Entity Framework Core in Action》中给你一个更复杂的数据库查询的例子。在下面的代码中,使用了四个查询对象链在一起,对一些图书的数据进行选择、排序、过滤和分页。你可以在实时网站 efcoreinaction.com 看到这些。
public IQueryable<BookListDto> SortFilterPage
(SortFilterPageOptions options)
{
var booksQuery = _context.Books
.AsNoTracking()
.MapBookToDto()
.OrderBooksBy(options.OrderByOptions)
.FilterBooksBy(options.FilterBy,
options.FilterValue);
options.SetupRestOfDto(booksQuery);
return booksQuery.Page(options.PageNum-1,
options.PageSize);
}
查询对象提供了比 Rep/UoW 模式更好的隔离性,因为你可以把复杂的查询分割成一系列的查询对象,并把它们连在一起。这使得它更容易编写和理解、重构和测试。另外,如果你有一个需要原始 SQL 的查询,你可以使用 EF Core 的 FromSql 方法,它也可以返回 IQueryable<T>。
- データベース·アクセス·メソッドの、更新、削除
查询对象处理了 CRUD 的读取部分,但是新增、更新和删除部分呢,也就是你向数据库写入的部分?我将向你展示运行 CUD 操作的两种方法:直接使用 EF Core 命令,以及使用实体类中的 DDD 方法。让我们来看看非常简单的更新例子:在我的图书应用中添加一个评论(见efcoreinaction.com)。
注:如果你想尝试添加评论,有一个与我的书配套的 GitHub repo(
github.com/JonPSmith/EfCoreInAction),选择分支 Chapter05(每章都有一个分支)并在本地运行该应用程序。你会看到每本书旁边都有一个管理按钮,有几个 CUD 命令。
-
- オプション1:EF Coreコマンドを直接使用する **
最明显的方法是使用 EF Core 方法来完成数据库的更新。下面是一个方法,它将为一本书添加一个新的评论,其中包括用户提供的评论信息。注意:ReviewDto 是一个持有用户填写完评论信息后返回的信息类。
public Book AddReviewToBook(ReviewDto dto)
{
var book = _context.Books
.Include(r => r.Reviews)
.Single(k => k.BookId == dto.BookId);
var newReview = new Review(dto.numStars, dto.comment, dto.voterName);
book.Reviews.Add(newReview);
_context.SaveChanges();
return book;
}
注:AddReviewToBook方法是在一个叫做AddReviewService的类中,这个类在我的ServiceLayer中。这个类被注册为一个服务,并且有一个构造函数,它接收应用程序的DbContext,这个DbContext是通过 DI 注入的。注入的值被存储在私有字段_context中,AddReviewToBook方法可以使用它来访问数据库。
これによりデータベースに新しいコメントが追加されます。これは効果的ですが、DDDのアプローチを使って構築する別の方法があります。
-
- モード2:DDDスタイルのエンティティクラス **
EF Coreは、更新されたコードを書く新しい場所、すなわちエンティティークラス内を提供します。EF Coreには、DDDエンティティを構築することを可能にするバックアップフィールドと呼ばれる機能があります。バックアップフィールドを使用すると、任意の関係構造へのアクセスを制御できます。これはEF 6.xでは不可能です。
DDD 谈到了聚合(前面提到过),所有的聚合只能通过根实体中的方法来改变,我把它称为访问方法。在 DDD 术语中,评论是图书实体的聚合,所以我们应该通过图书实体类中的一个名为 AddReview 的访问方法来添加一个评论。这样一来,上面的代码就变成了 Book 实体中的一个方法,在这里:
public Book AddReviewToBook(ReviewDto dto)
{
var book = _context.Find<Book>(dto.BookId);
book.AddReview(dto.numStars, dto.comment,
dto.voterName, _context);
_context.SaveChanges();
return book;
}
Book 实体类中的 AddReview 访问方法看起来像这样:
public class Book
{
private HashSet<Review> _reviews;
public IEnumerable<Review> Reviews => _reviews?.ToList();
//...other properties left out
//...constructors left out
public void AddReview(int numStars, string comment,
string voterName, DbContext context = null)
{
if (_reviews != null)
{
_reviews.Add(new Review(numStars, comment, voterName));
}
else if (context == null)
{
throw new ArgumentNullException(nameof(context),
"You must provide a context if the Reviews collection isn't valid.");
}
else if (context.Entry(this).IsKeySet)
{
context.Add(new Review(numStars, comment, voterName, BookId));
}
else
{
throw new InvalidOperationException("Could not add a new review.");
}
}
//...
}
这个方法更复杂,因为它可以处理两种不同的情况:一种是已经加载了 Review,另一种是还没有。但如果评论还没有被加载,它比原来的情况要快,因为它使用了“通过外键创建关系”的方法。
因为访问方法的代码在实体类里面,如果需要的话可以更复杂,因为它将是你需要写的唯一版本的代码。在方式一中,你可以在不同的地方重复相同的代码,只要你需要更新 Book 的 Review 集合。
注:“Creating Domain-Driven Design Entity classes with Entity Framework Core”という記事を書きましたが、DDDスタイルのエンティティークラスについてです。この記事では、このトピックの詳細を紹介します。また、EF Coreでビジネスロジックを書く方法についても、同じDDDスタイルのエンティティークラスを使用する方法についても更新しました。
为什么实体类中的方法不调用 SaveChanges?在方式一中,一个方法包含了所有的部分:a)加载实体,b)更新实体,c)调用 SaveChanges 来更新数据库。我可以这样做,因为我知道它是由一个网络请求调用的,而这就是我想做的全部。对于 DDD 实体方法,你不能在实体方法中调用 SaveChanges,因为你不能确定操作已经完成。例如,如果你从备份中加载一本书,你可能想创建这本书,添加作者,添加任何评论,然后调用 SaveChanges,这样所有的东西都在一起被提交到数据库。
-
- モード3:Generic Servicesライブラリ **
还有第三种方式。我注意到在我构建的 ASP.NET 应用程序中使用 CRUD 命令时有一个标准模式,早在 2014 年我就建立了一个名为 GenericServices 的库,它可以与 EF6.x 一起使用。2018 年我为 EF Core 建立了一个更全面的版本,名为 EfCore.GenericServices,见这篇关于 EfCore.GenericServices 的文章:
- GenericServices: A library to provide CRUD front-end services from a EF Core database
这些库并没有真正实现仓储模式,而是在实体类和前端需要实际数据之间充当适配器模式。我曾使用过原始的 EF6.x,GenericServices 为我节省了几个月的枯燥的前端代码编写。新的 EfCore.GenericServices 甚至更好,因为它可以与标准风格的实体类和 DDD 风格的实体类一起工作。
- どちらの方法が良いか?
方式一(直接使用 EF Core 代码)要写的代码最少,但有可能出现重复,因为应用程序的不同部分可能要对一个实体应用 CUD 命令。例如,当用户通过改变事物时,你可能会通过 ServiceLayer 进行更新,但外部 API 可能不会通过 ServiceLayer,所以你必须重复 CUD 代码。
方式二(DDD 风格的实体类)将关键的更新部分放在实体类内,所以代码对任何能得到实体实例的人都是可用的。事实上,由于 DDD 风格的实体类“锁定”了对属性和集合的访问,每个人都必须使用 Book 实体的 AddReview 访问方法,如果他们想更新 Review 集合的话。出于许多原因,这是我想在未来的应用中使用的方法(见我的文章中关于利弊的讨论)。其(轻微的)缺点是它需要一个单独的加载/保存部分,这意味着更多的代码。
方式三(GenericServices 库)是我的首选方法,尤其是现在我已经建立了 EfCore.GenericServices 版本,可以处理 DDD 风格的实体类。正如你在关于 EfCore.GenericServices 的文章中所看到的,这个库极大地减少了你在 Web/移动/桌面应用程序中需要编写的代码。当然,你仍然需要在你的业务逻辑中访问数据库,但这是另一回事。
CRUDコードの整理
Rep/UoWモードの利点の1つは、すべてのデータアクセスコードを1か所に保管することです。EF Coreを直接使用すると、データアクセスコードをどこにでも置くことができますが、自分や他のチームメンバーが見つけるのが難しくなります。そのため、コードをどこに置くかについて明確な計画を立て、それに固執することをお勧めします。
下の図は、3つのアセンブリのみを示す階層的または六角形のアーキテクチャを示しています(ビジネスロジックは省略しています。六角形のアーキテクチャでは、より多くのアセンブリがあります)。表示されている3つのアセンブリは:
ASP.NET Core。これは、HTMLページまたはWeb APIを提供するプレゼンテーション層です。データベースアクセスコードはありませんが、Service LayerとBusiness Layerのさまざまなメソッドに依存します。
サービスのレベル。クエリオブジェクトや追加、更新、削除メソッドを含むデータベースアクセスコードが含まれています。サービス層は、アダプタモードとコマンドモードを使用して、データ層とASP.NET Core(プレゼンテーション)層を接続します。
データ層ですアプリケーションのDb Contextとエンティティークラスを含みます。DDDスタイルのエンティティークラスは、ルートエンティティとその集合体を変更できるようにするアクセスメソッドを含んでいる。

注:前面提到的库
GenericServices(EF6.x)和EfCore.GenericServices(EF Core)实际上是一个提供ServiceLayer功能的库,即在DataLayer和你的 Web/移动/桌面应用程序之间充当适配器模式和命令模式。
从这个图中我想说的是,通过使用不同的程序集,一个简单的命名标准(见图中黑体字 Book)和文件夹,你可以建立一个应用程序,其中你的数据库代码是独立的,很容易找到。随着你的应用程序的增长,这可能是至关重要的。
EFコアメソッドの単体テスト
最後の部分は、EF Coreを使用したアプリケーションの単体テストです。ストレージモデルの利点の1つは、テスト中にシミュレーションに置き換えることができることです。したがって、EF Coreを直接使用すると、シミュレーションの選択肢がなくなります(技術的にはEF Coreをシミュレートできますが、うまく行うのは難しいです)。
ありがたいことに、EF Coreは現在進歩しており、インメモリデータベースでデータベースをシミュレートできるようになりました。インメモリデータベースは作成が速く、デフォルトの開始点(すなわち空)があるため、それに対するテストを書くのがはるかに簡単です。詳細については、私の記事“Using in-memory databases for unit testing EF Core applications”を参照してください。また、EF Core単体テストをより迅速に記述する方法を提供するEfCore.Test SupportというNuGetパッケージもあります。
9つの結論
我上一个使用 Rep/UoW 模式的项目要追溯到 2013 年,从那以后我再也没有使用过 Rep/UoW 模式。我尝试过一些方法,一个基于 EF6.x 名为 GenericServices 的自定义库,以及现在一个更标准的基于 EF Core 实现查询对象和 DDD 风格的实体类方法的 EfCore.GenericServices 自定义库。它们使得编写代码更容易,而且通常表现良好。但如果它们很慢,就很容易定位并对单个数据库访问进行性能调整。
Manning Publishingのために書いた私の本の中に、本を“販売”するASP.NET Coreアプリケーションのパフォーマンスチューニングに関する章があります。この手順では、クエリオブジェクトとDDDエンティティメソッドを使用して、パフォーマンスの高いデータベースアクセスが得られることを示しています(私の記事“Entity Framework Core Performance tuning:a worked example”を参照)。
私自身の仕事は、クエリオブジェクトを読み取りに使用し、CUDやビジネスロジックのアクセスメソッドを持つDDDスタイルのエンティティークラスを使用することです。効果があるかどうかを知るには、適切なアプリケーションでこれらを使用する必要があります。DDDスタイルのエンティティークラス、その恩恵を受けるアーキテクチャ、そしておそらく新しいライブラリについては、私のブログをフォローしてください。
楽しいコード!