積極的な読み込みとリポジトリパターン
-
06-07-2019 - |
質問
Repositoryパターンを使用する際に、複雑なオブジェクトグラフのeager-loading問題を適切に処理する方法について疑問に思っています。これは私が推測するORM特有の問題ではありません。
最初の試行:
public interface IProductRepository : IRepository<Product>
{
Product GetById(int id);
IProductRepository WithCustomers();
}
これは問題なく動作しますが、それは常に自分自身を繰り返すことを必要とします(どこでもリポジトリ実装でカスタムの 'With'メソッドを書く)。
次のアプローチ:
public interface IRepository<T> where T : IAggregateRoot
{
...
void With(Expression<Func<T, object>> propToExpand);
}
With
メソッドは、プライベートコレクションにアイテムを追加します。これは、後で必要なエンティティを取得するときに、どの小道具を積極的に読み込む必要があるかを見つけるために使用されます。
この種は機能し、問題ありません。しかし、私は使用法が嫌いです:
productRepository.With(x=>x.Customer);
productRepository.With(x=>x.Price);
productRepository.With(x=>x.Manufacturer);
var product = productRepository.GetById(id);
基本的に問題は、連鎖がないことです。次のようになりたい:
var product = productRepository
.With(x=>x.Customer)
.With(x=>x.Price)
.With(x=>x.Manufacturer)
.GetById(id);
これを達成できませんでした。できたとしても-その解決策がエレガントかどうかはわかりません。
これにより、基本的なものが不足しているという考えが生じます(どこにも例がありません)。これを処理する方法はいくつかありますか?ベストプラクティスとは?
解決
おもしろい問題で、これで問題を抱えているのはあなたが初めてではないはずです(私は絶対に持っています)。
私にとっての本当の質問は、熱心なロードロジックをどこに置きたいですか?
クライアントコードのリポジトリの外側
var product = productRepository
.With(x=>x.Customer)
.With(x=>x.Price)
.With(x=>x.Manufacturer)
.GetById(id);
これは優れたソフトウェア設計とは思いません。これは、「1000カットの死」を引き起こす可能性があるようです。そのような構成要素がアプリ全体に散在している場合。
またはリポジトリ内。例:
interface IProductRepository {
Product GetById(int id);
Product GetByIdWithCustomers(int i);
}
クライアントコードは次のようになります。
var product = productRepository.GetByIdWithCustomers(id);
通常、基本的なCRUD操作のみが定義されたBaseRepositoryを1つ作成します。
public class BaseRepository<TEntity, TPrimaryKey> {
public void Save(TEntity entity) { ... }
public void Delete(TEntity entity) { ... }
public TEntity Load(TPrimaryKey id) { ... } // just gets the entity by primary key
}
その後、ドメインオブジェクトを取得するための特定のメソッドを提供するために、この基本クラス/インターフェイスを拡張します。あなたのアプローチはやや似た方向に進むようです。
public class MediaRepository : BaseRepository<Media, int> {
public long CountMediaWithCategories() { ... }
public IList<Media> MediaInCategories(IList<Category> categories) { .... }
}
良いこと:すべてのORMの内容(設定の読み込み、取得の深さなど)はRepositoryクラスにカプセル化され、クライアントコードは結果セットを取得するだけです。
あなたがやろうとしているように非常に一般的なリポジトリで作業しようとしましたが、ほとんどの場合、ドメインオブジェクトの特定のクエリとリポジトリを作成することになりました。
他のヒント
var product = productRepository
.With(x=>x.Customer)
.With(x=>x.Price)
.With(x=>x.Manufacturer)
.GetById(id);
上記のようにオブジェクトグラフのクエリの深さを決定したいという希望は理解できますが、もっと簡単な方法があると思います。 IDで製品(顧客、価格、製造元)を返すことを選択する代わりに、単に製品を返します。他のすべてのものは、製品の遅延読み込みプロパティです。
データアクセスレイヤーのPOCOオブジェクトモデルによる「連鎖」によって、この「完全なグラフのアクセシビリティ」を実現します。このように、一度に引き出すためにどれだけの熱心にロードされたデータを知る必要はありません。オブジェクトグラフから必要なものを尋ねるだけで、モデルは何がロードされ、DALからさらに回復する必要があるかを知っています。 これら 3つの 回答-私のアプローチを説明しようとしています。さらに説明が必要な場合はお知らせください。この回答を編集します。
これは古い質問ですが、おそらく誰かを助けることができます。良いアプローチを見つけるために時間を費やしました。C#で見つけたものは次のとおりです。
IRepository.cs:
public interface IRepository<TEntity> where TEntity : class
{
IEnumerable<TEntity> GetAll(Expression<Func<TEntity, bool>> where
, params Expression<Func<TEntity, object>>[] properties);
}
Repository.cs
public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
{
private readonly DbSet<TEntity> _dbset;
public Repository(DbSet<TEntity> dbset)
{
_dbset = dbset;
}
public virtual IEnumerable<TEntity> GetAll(Expression<Func<TEntity, bool>> where
, Expression<Func<TEntity, object>>[] properties)
{
if (where == null)
throw new ArgumentNullException(nameof(where));
if (properties == null)
throw new ArgumentNullException(nameof(properties));
var query = _dbset as IQueryable<TEntity>; // _dbSet = dbContext.Set<TEntity>()
query = properties
.Aggregate(query, (current, property) => current.Include(property));
return query.AsNoTracking().Where(where).ToList();
}
}
使用方法:
var repository = new Repository<User>();
var users = repository.GetAll(p => p.Id == 1, d => d.Address, d => d.Carts);
参照:リンク
あなたがやろうとしていることは理解できますが、基本的なリポジトリパターンを多少超えています。
最小限のリポジトリインターフェースには、次のメソッドが含まれます。
- GetById
- 追加
- 削除
その上に追加のメソッドを追加すると、すべての集約ルートに対して必ずしもインターフェイスが意味をなさない状況に陥り始めます。
完全に美しいAPIを持つことは不可能な場合があります。あなたが持っているものが「十分に」うまくいくならあなたのために、私はそれで行きます。プログラミング対象のより良いAPIを提供するためにリポジトリパターンから離れる必要がある場合は、それを実行してください!
リポジトリパターンは、すべてを網羅したソリューションではありません。別のソリューションが必要な場合があります。
リポジトリの外部で必要なすべてのインクルードを指定する場合は、各汎用メソッドのオプションのパラメーター(C#)をリストできます。
TEntity Find(Func<TEntity, bool> expression, params string[] eagerLoads);
その後、クライアント層で:
IProductRepository.Find(x => x.Id == id, "Customer", "Price")
タイプセーフにする場合は、エンティティを列挙します。
public enum BusinessEntities { Customer, Price, Manufacturer }
IProductRepository.Find(x => x.Id == id, BusinessEntities.Customer.ToString(), BusinessEntities.Price.ToString())
具体的に何が欲しいのかを尋ねるのはクライアントの責任だと思います。 Generic Repositoryは基本的なCRUDを処理するだけです。
BaseRepository.cs
で、このメソッドを作成できます:
public async Task<IEnumerable<T>> GetWithChild(string child)
{
return await _entities.Include(child).ToListAsync();
}
APIにはサービスレイヤーも実装していますが、APIからこのメソッドを呼び出して、ロードする変数の名前を渡すだけです。
明らかに、あなたの状況では、さらにいくつかの文字列を含める必要があります。
以前に回答を投稿しましたが、解決策にまだ満足していませんでした。そこで、より良い解決策があります。
BaseRepository.cs内
public async Task<IEnumerable<T>> GetAll(params Expression<Func<T, object>>[] properties)
{
IQueryable<T> query = _entities;
query = properties.Aggregate(query, (current, property) => current.Include(property));
return await query.AsNoTracking().ToListAsync();
}
そして次のように単純にメソッドを使用できます
await _service.GetAll(x => x.Customer, x => x.Price, x => x.Manufacturer);