昨日、『.NET 大牛之路』グループの仲間たちがEF Coreを使ったリポジトリパターンの実装について話し合っていました。以前、ある海外の著名なエンジニアが書いた記事を読んだことを思い出し、非常に参考になると感じたので、今日翻訳してみました。ご覧ください。
原文:https://www.thereformedprogrammer.net/is-the-repository-pattern-useful-with-entity-framework-core/
著者:Jon P Smith
翻訳:精致码农-王亮
注記:原文は2018年2月に初版公開、2020年7月に最終更新。
本文:
私は2014年にリポジトリパターンに関する最初の記事を書き、それは今でも人気のある記事です。この記事はその更新版であり、ここ数年におけるEF Coreの新しいリリースとEF Coreデータベースアクセスパターンに関するさらなる研究に基づいています。
原文: Analysing whether Repository pattern useful with Entity Framework (2014年5月).
https://www.thereformedprogrammer.net/is-the-repository-pattern-useful-with-entity-framework/
最初の解決策: Four months on – my solution to replacing the Repository pattern (2014年10月).
https://www.thereformedprogrammer.net/is-the-repository-pattern-useful-with-entity-framework-part-2/
本記事: Is the repository pattern useful with Entity Framework Core?
1 概要
答えは「いいえ」。リポジトリ/Unit of Workパターン(以降、Rep/UoW)はEF Coreには役に立ちません。EF CoreはすでにRep/UoWパターンを実装しているため、EF Coreの上にさらにRep/UoWパターンを追加することは無意味です。
より良い解決策は、EF Coreを直接使用することです。これにより、EF Coreの全機能を活用して高性能なデータベースアクセスを構築できます。
2 本記事の目的
この記事では以下に焦点を当てます。
- EFにおけるRep/UoWパターンに対する人々の見解
- EFでRep/UoWパターンを使用することの利点と欠点
- Rep/UoWパターンをEF Coreコードに置き換える3つの方法
- EF Coreデータベースアクセスコードを発見しやすく、リファクタリングしやすくする方法
- EF Coreの単体テストに関する議論
C#とEF 6.xもしくはEF Coreライブラリに精通していることを前提とします。この記事では特にEF Coreについて述べますが、ほとんどの内容はEF6.xにも関連します。
3 背景
2013年、私は医療モデリング専用の大規模なWebアプリケーションの開発に参加しました。ASP.NET MVC4とEF 5(当時登場したばかりで、地理データのSQL Spatial型をサポート)を使用しました。当時の一般的なデータベースアクセスパターンはRep/UoWパターンでした。Microsoftが2013年に書いた「Implementing the Repository and Unit of Work Patterns in an ASP.NET MVC Application」を参照してください。
- Implementing the Repository and Unit of Work Patterns in an ASP.NET MVC Application
Rep/UoWを使用してアプリケーションを構築しましたが、開発中にこれが大きな痛点であることに気づきました。小さな問題を修正するためにリポジトリのコードを「調整」し続けなければならず、そのたびに他の何かを壊してしまいました。この経験から、データベースアクセスコードをより良く実装する方法を研究するようになりました。
そうした中、2017年末に新しく設立された会社と契約し、EF6.xアプリケーションのパフォーマンス問題の解決を支援しました。パフォーマンス問題の大部分は遅延読み込みに起因していることが判明しました。これはアプリケーションがRep/UoWパターンを使用していたために必要になったものでした。
さらに、スタートアッププロジェクトに参加していたあるプログラマーがかつてRep/UoWパターンを使用していたことがわかり、会社の創設者との会話の中で、アプリケーションのRep/UoW部分はかなり不透明で操作が難しいと語っていました。
4 リポジトリパターンに対する人々の見解
Spatial Modeller™の設計を研究する中で、リポジトリパターンを放棄する強力な証拠を提供するブログ記事をいくつか見つけました。その中でも最も説得力があり、よく考え抜かれた記事は「Repositories On Top UnitOfWork Are Not a Good Idea」です。Rob Coneryの主な主張は、Rep/UoWはEntity Framework (EF) DbContextが提供するものを単に繰り返しているだけであり、なぜ完璧なフレームワークを価値のない外皮で覆うのか、というものです。Robはこれを「過度な抽象化の愚かさ」と呼んでいます。
もう一つのブログは「Why Entity Framework renders the Repository pattern obsolete」です。この記事でIsaac Abrahamは、リポジトリパターンはテストを容易にするわけではなく、本来そうあるべきだったと補足しています。この点はEF Coreではさらに現実的であり、後述します。
では、彼らは正しいのでしょうか?
5 Rep/UoWパターンに対する私の見解
可能な限り公平な方法でRep/UoWパターンの利点と欠点をレビューしてみましょう。以下が私の見解です。
5.1 Rep/UoWパターンの利点
データベースアクセスコードの隔離。リポジトリパターンの最大の利点は、すべてのデータベースアクセスコードがどこにあるかを把握できることです。また、通常はリポジトリをカタログリポジトリ、注文処理リポジトリなどのセクションに分割するため、エラーがあったりパフォーマンス調整が必要な特定のクエリのコードを簡単に見つけることができます。これは間違いなく大きな利点です。
集約。ドメイン駆動設計(DDD)はシステムを設計する方法であり、ルートエンティティがあり、他の関連エンティティがその下にまとめられることを推奨します。私の著書『Entity Framework Core in Action』で使用した例は、Reviewエンティティのコレクションを持つBookエンティティです。これらのReviewはBookと関連付けられている場合にのみ意味を持つため、DDDではBookエンティティを通じてのみReviewを変更すべきとしています。Rep/UoWパターンは、Bookリポジトリ内でReviewの追加/削除を行う方法を提供することでこれを実現します。
複雑なT-SQLコマンドの隠蔽。場合によってはEF CoreをバイパスしてT-SQLを使用する必要があります。この種のアクセスは上位層から隠蔽されるべきですが、メンテナンスやリファクタリングを容易にするために見つけやすいものであるべきです。指摘しておくと、Rob Coneryのポスト「Command/Query Objects」もこれを処理できます。
モック/テストの容易さ。個別のリポジトリを簡単にモックでき、データベースにアクセスするコードの単体テストが容易になります。これは数年前には真実でしたが、現在では他の方法でこの問題を解決できます。これについては後述します。
「EF Coreを別のデータベースアクセスライブラリに置き換える」という点は挙げませんでした。これはRep/UoWの背後にある考え方の一つですが、a) データベースアクセスライブラリの置き換えは困難であり、b) 実際にアプリケーションでそのような重要なライブラリを交換するでしょうか?という理由から、誤解だと考えます。
5.2 Rep/UoWパターンの欠点
最初の3つはパフォーマンスに関連しています。効率的なRep/UoWを書けないと言っているわけではありませんが、それは難しい作業であり、多くの実装に内在するパフォーマンス問題(Microsoftの古いRep/UoW実装も含む)を見てきました。以下がRep/UoWパターンで私が見つけた欠点のリストです。
パフォーマンス—エンティティ関係の処理。リポジトリは通常、単一の型の
IEnumerable/IQueryable結果を返します(例:Microsoftの例におけるStudentエンティティクラス)。例えば、Studentの関係から住所などの情報を表示したい場合、リポジトリ内の最も簡単な方法は遅延読み込みを使用してStudentのアドレスエンティティを読み取ることです。この方法はよく使われます。問題は、遅延読み込みがそれぞれの関係に対して個別のデータベースラウンドトリップを必要とし、すべてのデータベースアクセスを1回のラウンドトリップにまとめるよりも遅くなることです。(別の方法として、異なる戻り値型を持つ複数のクエリメソッドを持つことですが、これによりリポジトリが非常に大きくなり面倒になります—第4点を参照)。データが必要な形式に合わない。リポジトリコンポーネントは通常データベースに基づいて作成されるため、返されるデータがサービスやユーザーが必要とする正確な形式でない場合があります。リポジトリの出力を調整することもできますが、それは二段階の作業です。クエリをフロントエンドに近い場所で形成し、必要なデータの調整を含める方が良いと考えます。
パフォーマンス—更新:多くのRep/UoW実装はEF Coreを隠蔽しようとしますが、そのすべての機能を活用していません。例えば、Rep/UoWはEF Coreの
Updateメソッドを使用してエンティティを更新しますが、これはエンティティのすべてのプロパティを保存します。一方、EF Coreの組み込み変更追跡機能を使用すれば、変更されたプロパティのみが更新されます。汎用的すぎる。Rep/UoWの魅力は、汎用リポジトリ(
Repository<T>)を書き、それを使ってすべてのサブリポジトリ(カタログリポジトリ、注文処理リポジトリなど)を構築できることです。これにより記述するコードを最小限に抑えられるはずですが、私の経験では、汎用リポジトリは最初は機能しますが、事態が複雑になるにつれて、個々のリポジトリごとにコードを追加しなければならなくなります。「コードが再利用可能であればあるほど、使用可能ではなくなる。」—Neil Ford
欠点をまとめると、Rep/UoWはEF Coreを隠蔽するため、EF Coreの機能を使ってシンプルかつ効率的なデータベースアクセスコードを書くことができなくなります。
6 Rep/UoWの利点をEF Coreで維持する方法
前述の利点のセクションでは、隔離、集約、隠蔽、単体テストを挙げました。Rep/UoWはこれらをうまく実現しています。このセクションでは、EF Coreを直接使用した場合に、適切なアーキテクチャ設計と組み合わせることで同じ隔離や集約などの機能を提供する、さまざまなソフトウェアパターンとプラクティスについて説明します。
各利点の実現方法を説明した後、それらを階層化されたソフトウェアアーキテクチャに配置します。
- クエリオブジェクト:データベース読み取りを隔離・隠蔽する方法
データベースアクセスは、作成、読み取り、更新、削除(CRUD)の4種類に分類できます。私にとって、読み取り部分(EF Coreではクエリと呼ばれる)は、構築とパフォーマンス調整が最も難しいことが多いです。多くのアプリケーションは、良好で高速なクエリ(例:購入する製品リスト、タスクリストなど)に依存しています。そこで考案されたのがクエリオブジェクトです。
初めて知ったのは2013年のRob Coneryの記事(前述)で、彼はコマンド/クエリオブジェクトに言及しています。また、Jimmy Bogardは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 メソッドを適用したときにそれらのコマンドが実行される仕組みです。この単純な例ではデータベースを使用していませんが、numsQ 変数をアプリケーションの DbContext の DbSet<T> プロパティに置き換えれば、IQueryable<T> 型内のコマンドはデータベースコマンドに変換されます。
IQueryable<T> 型は最後まで実行されないため、複数のクエリオブジェクトを連鎖させることができます。私の著書『Entity Framework Core in Action』から、より複雑なデータベースクエリの例を挙げます。以下のコードでは、4つのクエリオブジェクトが連鎖して、書籍データの選択、並べ替え、フィルタリング、ページネーションを行っています。実際のサイト 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操作を実行する2つの方法を紹介します。EF Coreコマンドを直接使用する方法と、エンティティクラス内のDDDアプローチです。非常にシンプルな更新例として、私の書籍アプリにレビューを追加するケースを見てみましょう(efcoreinaction.com を参照)。
注:レビューを追加してみたい場合は、私の書籍に付属するGitHubリポジトリ(
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メソッドは、ServiceLayerにあるAddReviewServiceというクラスにあります。このクラスはサービスとして登録されており、アプリケーションのDbContextを受け取るコンストラクタを持ちます(DIで注入)。注入された値はプライベートフィールド_contextに格納され、AddReviewToBookメソッドはそれを使用してデータベースにアクセスします。
これにより新しいレビューがデータベースに追加されます。効果的ですが、別の方法として、よりDDDのアプローチを用いて構築することもできます。
方法2:DDDスタイルのエンティティクラス
EF Coreは、エンティティクラス内部に更新コードを記述する新しい場所を提供します。EF Coreには「バッキングフィールド」機能があり、DDDエンティティの構築を可能にします。バッキングフィールドを使用すると、任意の関係構造へのアクセスを制御できます。これはEF6.xでは実際には不可能でした。
DDDは集約(前述)について述べており、すべての集約はルートエンティティ内のメソッド(私は「アクセスメソッド」と呼びます)を通じてのみ変更されるべきです。DDD用語では、レビューはBookエンティティの集約であり、AddReview というアクセスメソッドをBookエンティティクラス内に置くことでレビューを追加するべきです。これにより、上記のコードは 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();
//...他のプロパティは省略
//...コンストラクタは省略
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 がすでにロードされている場合と、まだの場合の2つの異なるケースを処理できるからです。しかし、レビューがまだロードされていない場合、元のケースよりも高速です。「外部キーによる関係作成」のアプローチを使用しているからです。
アクセスメソッドのコードがエンティティクラス内にあるため、必要に応じてより複雑にすることができ、記述する必要がある唯一のバージョンのコードになります。方法1では、Book の Review コレクションを更新する必要があるたびに、同じコードを異なる場所で繰り返す可能性があります。
注:私は「Creating Domain-Driven Design entity classes with Entity Framework Core」という記事を書いており、DDDスタイルのエンティティクラスについてすべて説明しています。この記事ではこのトピックについてより詳しく紹介しています。また、EF Coreでビジネスロジックを記述する方法についての記事も更新し、同じDDDスタイルのエンティティクラスを使用するようにしました。
なぜエンティティクラス内のメソッドは SaveChanges を呼ばないのでしょうか? 方法1では、メソッドは a) エンティティのロード、b) エンティティの更新、c) SaveChanges の呼び出しのすべての部分を含んでいます。これはWebリクエストによって呼び出され、それがやりたいことのすべてだと分かっているからです。DDDエンティティメソッドの場合、操作が完了したかどうか確信できないため、エンティティメソッド内で SaveChanges を呼び出すことはできません。例えば、バックアップから書籍をロードする場合、書籍を作成し、著者を追加し、レビューを追加してから SaveChanges を呼び出し、すべてを一度にデータベースにコミットしたいかもしれません。
方法3:GenericServicesライブラリ
3つ目の方法もあります。私が構築したASP.NETアプリケーションでCRUDコマンドを使用する際に標準的なパターンに気づき、2014年にはEF6.xと連携する GenericServices というライブラリを構築しました。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スタイルのエンティティクラスの両方で動作します。
- どの方法が良いか
方法1(EF Coreコードを直接使用)は記述するコードが最小ですが、アプリケーションの異なる部分が同じエンティティに対してCUDコマンドを適用する可能性があるため、重複が発生する可能性があります。例えば、ユーザーが何かを変更する際には ServiceLayer を通じて更新を行うかもしれませんが、外部APIは ServiceLayer を通らないため、CUDコードを繰り返す必要があります。
方法2(DDDスタイルのエンティティクラス)は重要な更新部分をエンティティクラス内に配置するため、エンティティインスタンスを取得できる人なら誰でもコードが利用可能です。実際、DDDスタイルのエンティティクラスはプロパティやコレクションへのアクセスを「ロック」するため、Review コレクションを更新したい場合は誰もが Book エンティティの AddReview アクセスメソッドを使用しなければなりません。多くの理由から、これは将来のアプリケーションで使用したい方法です(メリット・デメリットの議論については私の記事を参照)。欠点(軽微)は、別途ロード/保存の部分が必要となり、コードが増えることです。
方法3(GenericServicesライブラリ)は私の好みの方法であり、特にDDDスタイルのエンティティクラスを扱える EfCore.GenericServices バージョンを構築した今はなおさらです。EfCore.GenericServices に関する記事で見られるように、このライブラリはWeb/モバイル/デスクトップアプリケーションで記述する必要のあるコードを大幅に削減します。もちろん、ビジネスロジック内でデータベースにアクセスする必要は依然としてありますが、それは別の話です。
7 CRUDコードの整理
Rep/UoWパターンの利点の一つは、すべてのデータアクセスコードを1か所にまとめることです。EF Coreを直接使用する場合、データアクセスコードはどこにでも配置できますが、これにより自分自身や他のチームメンバーがそれを見つけるのが難しくなります。したがって、コードを配置する場所について明確な計画を持ち、それを守ることをお勧めします。
以下の図は、階層化またはヘキサゴナルアーキテクチャを示しており、3つのアセンブリのみを表示しています(ビジネスロジックは省略しています。ヘキサゴナルアーキテクチャではさらに多くのアセンブリがあります)。表示されている3つのアセンブリは次のとおりです。
ASP.NET Core:プレゼンテーション層で、HTMLページまたはWeb APIを提供します。データベースアクセスコードは含まれず、ServiceLayerおよびBusinessLayerのさまざまなメソッドに依存します。
サービス層:データベースアクセスコード(クエリオブジェクトや作成・更新・削除メソッド)を含みます。サービス層はアダプターパターンとコマンドパターンを使用して、データ層とASP.NET Core(プレゼンテーション)層を接続します。
データ層:アプリケーションのDbContextとエンティティクラスを含みます。DDDスタイルのエンティティクラスは、ルートエンティティとその集約が変更されることを許可するアクセスメソッドを含みます。

注:前述のライブラリ
GenericServices(EF6.x)およびEfCore.GenericServices(EF Core)は、実際にはServiceLayer機能を提供するライブラリであり、DataLayerとWeb/モバイル/デスクトップアプリケーションの間でアダプターパターンおよびコマンドパターンとして機能します。
この図から伝えたいのは、異なるアセンブリ、シンプルな命名規則(図中の太字 Book を参照)、およびフォルダを使用することで、データベースコードが独立して見つけやすくなるアプリケーションを構築できるということです。アプリケーションが成長するにつれて、これは極めて重要になる可能性があります。
8 EF Coreメソッドの単体テスト
最後に検討する部分は、EF Coreを使用するアプリケーションの単体テストです。リポジトリパターンの利点の一つは、テスト時にモックで置き換えられることです。したがって、EF Coreを直接使用するとモックの選択肢が失われます(技術的にはEF Coreをモックすることは可能ですが、うまく行うのは困難です)。
幸いなことに、現在のEF Coreは進歩しており、インメモリデータベースを使用してデータベースをモックできます。インメモリデータベースは作成が高速で、デフォルトの開始点(空)があるため、それに対するテストの記述がはるかに容易です。詳細については、私の記事「Using in-memory databases for unit testing EF Core applications」を参照してください。また、EfCore.TestSupport というNuGetパッケージがあり、EF Coreの単体テストの記述をより迅速にするメソッドを提供しています。
9 結論
Rep/UoWパターンを最後に使用したプロジェクトは2013年に遡り、それ以降は一度も使用していません。いくつかの方法を試しました。EF6.xベースのカスタムライブラリ GenericServices、そして現在はより標準的なEF CoreベースのクエリオブジェクトとDDDスタイルのエンティティクラスメソッドを実装したカスタムライブラリ EfCore.GenericServices です。これらはコードの記述を容易にし、通常は良好に動作します。しかし、遅い場合は簡単に特定し、個々のデータベースアクセスをパフォーマンス調整できます。
Manning出版社向けに書いた書籍の中では、ASP.NET Coreアプリケーション(書籍を「販売」する)のパフォーマンス調整についての章があります。このプロセスではクエリオブジェクトとDDDエンティティメソッドを使用しており、高性能なデータベースアクセスを生成できることを示しています(私の記事「Entity Framework Core performance tuning – a worked example」を参照)。
私自身の仕事では、読み取りにクエリオブジェクトを使用し、CUDおよびビジネスロジックにはDDDスタイルのエンティティクラスとそのアクセスメソッドを使用しています。これらを適切なアプリケーションで使用して、本当に効果的かどうかを確認する必要があります。DDDスタイルのエンティティクラスやその恩恵を受けるアーキテクチャ、そしておそらく新しいライブラリについての詳細は、私のブログをご期待ください:)。
コーディングを楽しんでください!