IQueryableをアプリケーションの残りの部分に公開せずに、クリーンなリポジトリを作成するにはどうすればよいですか?
-
06-07-2019 - |
質問
だから、プロジェクトの残りにIQueryableを公開するかどうかの件名に関して、SOに関するすべてのQ& Aを読みました(こちら、およびこちら)、そして最終的にはIQueryableを自分のモデル以外には公開したくないと判断しました。 IQueryableは特定の永続化実装に結び付けられているため、これに縛られるという考えは好きではありません。同様に、レポジトリにない実際のクエリを変更する呼び出しチェーンの下位のクラスについて、どの程度気分が良いのかわかりません。
では、これを行わずにクリーンで簡潔なリポジトリを作成する方法について提案はありますか?私が見る1つの問題は、私のリポジトリがクエリをフィルタリングするために必要なさまざまなもののためにたくさんのメソッドから爆発することです。
次のものを持っています:
IEnumerable GetProductsSinceDate(DateTime date);
IEnumberable GetProductsByName(string name);
IEnumberable GetProductsByID(int ID);
IQueryableを渡すことを許可していた場合、次のような一般的なリポジトリを簡単に作成できました。
public interface IRepository<T> where T : class
{
T GetById(int id);
IQueryable<T> GetAll();
void InsertOnSubmit(T entity);
void DeleteOnSubmit(T entity);
void SubmitChanges();
}
ただし、IQueryableを使用していない場合、GetAll()などのメソッドは実際には実用的ではありません。これは、遅延評価が今後行われないためです。そのうちの10個を使用するためだけに10,000個のレコードを返したくありません。
ここでの答えは何ですか? ConeryのMVCストアフロントで、彼は&quot; Service&quot;という別のレイヤーを作成しました。リポジトリからIQueryableの結果を受け取り、さまざまなフィルターの適用を担当したレイヤー。
これは私がすべきことなのか、それとも似たようなものですか?リポジトリにIQueryableを返させますが、IListやIEnumerableなどの具象型を返すGetProductByNameなどのフィルタークラスの後ろに隠して、アクセスを制限しますか?
解決
IQueryable
を公開することは非常に実行可能なソリューションであり、これが現在行われているほとんどのリポジトリ実装です。 (SharpArchitectureとFubuMVC contribを含む。)
これはあなたが間違っている場所です:
ただし、使用していない場合 GetAll()などのIQueryable thenメソッド 怠sinceなので本当に実用的ではありません 評価は行われません この線。戻りたくない そのうちの10個を使用するためだけに10,000レコード 後で。
これは本当ではありません。あなたの例は正しいので、GetAll()の名前をよりわかりやすい名前に変更してください。
それを呼び出しても、すべてのアイテムが返されるわけではありません。それがIQueryableの目的です。この概念は「遅延読み込み」と呼ばれます。これは、 IQueryable
を列挙するときにのみデータを読み込む(およびデータベース要求を行う)ためです。
つまり、次のようなメソッドがあるとします:
IQueryable<T> Retrieve() { ... }
その後、次のように呼び出すことができます:
Repository.Retrieve<Customer>().Single(c => c.ID == myID);
これは、データベースから1行のみを取得します。
そしてこれ:
Repository.Retrieve<Customer>().Where(c => c.FirstName == "Joe").OrderBy(c => c.LastName);
これにより、対応するクエリも生成され、列挙した場合にのみ実行されます。 (クエリから式ツリーを生成し、クエリプロバイダーはそれをデータソースに対する適切なクエリに変換する必要があります。)
詳細については、このMSDN記事。
他のヒント
Robのメソッドは実際にはコアの問題を解決しません。それは、実行したいクエリの種類ごとに個別のメソッドを記述したくないためです。残念ながら、IQueryableを使用していない場合は残しました。
メソッドが&quot;サービス&quot;にあることを確認してください層ですが、それでも&quot; GetProductsByName、GetProductsByDate&quot; ...
と記述する必要があります。他の方法は次のようなものです:
GetProducts(QueryObject);
これにより、返される内容を制限できるという点で、IQueryableを使用するよりも利点が得られる場合があります。
うーん。使用しているORMの種類に応じて、さまざまな方法でこれを解決しました。
主なアイデアは、1つのリポジトリベースクラスと、すべての可能なwhere / orderby / expand | include / paging / etcオプションを示す非常に多くのパラメーターを受け取る1つのクエリメソッドを持つことです。
これは、LINQ to NHibernateを使用した簡単で汚いサンプルです(もちろん、リポジトリ全体が実装の詳細である必要があります):
public class RepositoryBase
{
private ISession Session;
public RepositoryBase()
{
Session = SessionPlaceHolder.Session;
}
public TEntity[] GetPaged<TEntity>(IEnumerable<Expression<Func<TEntity, bool>>> filters,
IEnumerable<Expression<Func<TEntity, object>>> relatedObjects,
IEnumerable<Expression<Func<TEntity, object>>> orderCriterias,
IEnumerable<Expression<Func<TEntity, object>>> descOrderCriterias,
int pageNumber, int pageSize, out int totalPages)
{
INHibernateQueryable<TEntity> nhQuery = Session.Linq<TEntity>();
if (relatedObjects != null)
foreach (var relatedObject in relatedObjects)
{
if (relatedObject == null) continue;
nhQuery = nhQuery.Expand(relatedObject);
}
IQueryable<TEntity> query = nhQuery;
if (filters != null)
foreach (var filter in filters)
{
if (filter == null) continue;
query = query.Where(filter);
}
bool pagingEnabled = pageSize > 0;
if (pagingEnabled)
totalPages = (int) Math.Ceiling((decimal) query.Count()/(decimal) pageSize);
else
totalPages = 1;
if (orderCriterias != null)
foreach (var orderCriteria in orderCriterias)
{
if (orderCriteria == null) continue;
query = query.OrderBy(orderCriteria);
}
if (descOrderCriterias != null)
foreach (var descOrderCriteria in descOrderCriterias)
{
if (descOrderCriteria == null) continue;
query = query.OrderByDescending(descOrderCriteria);
}
if (pagingEnabled)
query = query.Skip(pageSize*(pageNumber - 1)).Take(pageSize);
return query.ToArray();
}
}
通常、たとえばページングなどを必要としない場合、多くのチェーンオーバーロードをショートカットとして追加します。
別の汚いものがあります。申し訳ありませんが、最終版を公開できるかどうかわかりません。これらは下書きであり、表示しても問題ありません:
using Context = Project.Services.Repositories.EntityFrameworkContext;
using EntitiesContext = Project.Domain.DomainSpecificEntitiesContext;
namespace Project.Services.Repositories
{
public class EntityFrameworkRepository : IRepository
{
#region IRepository Members
public bool TryFindOne<T>(Expression<Func<T, bool>> filter, out T result)
{
result = Find(filter, null).FirstOrDefault();
return !Equals(result, default(T));
}
public T FindOne<T>(Expression<Func<T, bool>> filter)
{
T result;
if (TryFindOne(filter, out result))
return result;
return default(T);
}
public IList<T> Find<T>() where T : class, IEntityWithKey
{
int count;
return new List<T>(Find<T>(null, null, 0, 0, out count));
}
public IList<T> Find<T>(Expression<Func<T, bool>> filter, Expression<Func<T, object>> sort)
{
int count;
return new List<T>(Find(filter, sort, 0, 0, out count));
}
public IEnumerable<T> Find<T>(Expression<Func<T, bool>> filter, Expression<Func<T, object>> sort, int pageSize,
int pageNumber, out int count)
{
return ExecuteQuery(filter, sort, pageSize, pageNumber, out count) ?? new T[] {};
}
public bool Save<T>(T entity)
{
var contextSource = new EntityFrameworkContext();
EntitiesContext context = contextSource.Context;
EntityKey key = context.CreateEntityKey(GetEntitySetName(entity.GetType()), entity);
object originalItem;
if (context.TryGetObjectByKey(key, out originalItem))
{
context.ApplyPropertyChanges(key.EntitySetName, entity);
}
else
{
context.AddObject(GetEntitySetName(entity.GetType()), entity);
//Attach(context, entity);
}
return context.SaveChanges() > 0;
}
public bool Delete<T>(Expression<Func<T, bool>> filter)
{
var contextSource = new EntityFrameworkContext();
EntitiesContext context = contextSource.Context;
int numberOfObjectsFound = 0;
foreach (T entity in context.CreateQuery<T>(GetEntitySetName(typeof (T))).Where(filter))
{
context.DeleteObject(entity);
++numberOfObjectsFound;
}
return context.SaveChanges() >= numberOfObjectsFound;
}
#endregion
protected IEnumerable<T> ExecuteQuery<T>(Expression<Func<T, bool>> filter, Expression<Func<T, object>> sort,
int pageSize, int pageNumber,
out int count)
{
IEnumerable<T> result;
var contextSource = new EntityFrameworkContext();
EntitiesContext context = contextSource.Context;
ObjectQuery<T> originalQuery = CreateQuery<T>(context);
IQueryable<T> query = originalQuery;
if (filter != null)
query = query.Where(filter);
if (sort != null)
query = query.OrderBy(sort);
if (pageSize > 0)
{
int pageIndex = pageNumber > 0 ? pageNumber - 1 : 0;
query = query.Skip(pageIndex).Take(pageSize);
count = query.Count();
}
else
count = -1;
result = ExecuteQuery(context, query);
//if no paging total count is count of the entire result set
if (count == -1) count = result.Count();
return result;
}
protected internal event Action<ObjectContext, IEnumerable> EntitiesFound;
protected void OnEntitiesFound<T>(ObjectContext context, params T[] entities)
{
if (EntitiesFound != null && entities != null && entities.Length > 0)
{
EntitiesFound(context, entities);
}
}
//Allowing room for system-specific-requirement extensibility
protected Action<IEnumerable> ItemsFound;
protected IEnumerable<T> ExecuteQuery<T>(ObjectContext context, IQueryable<T> query)
{
IEnumerable<T> result = null;
if (query is ObjectQuery)
{
var objectQuery = (ObjectQuery<T>) query;
objectQuery.EnablePlanCaching = false;
objectQuery.MergeOption = MergeOption.PreserveChanges;
result = new List<T>(objectQuery);
if (ItemsFound != null)
ItemsFound(result);
return result;
}
return result;
}
internal static RelationshipManager GetRelationshipManager(object entity)
{
var entityWithRelationships = entity as IEntityWithRelationships;
if (entityWithRelationships != null)
{
return entityWithRelationships.RelationshipManager;
}
return null;
}
protected ObjectQuery<T> CreateQuery<T>(ObjectContext context)
{
ObjectQuery<T> query = context.CreateQuery<T>(GetEntitySetName(typeof (T)));
query = this.AggregateEntities(query);
return query;
}
protected virtual ObjectQuery<T> AggregateEntities<T>(ObjectQuery<T> query)
{
return query;
}
private static string GetEntitySetName(Type entityType)
{
return string.Format("{0}Set", entityType.Name);
}
}
public class EntityFrameworkContext
{
private const string CtxKey = "ctx";
private bool contextInitialized
{
get { return HttpContext.Current.Items[CtxKey] != null; }
}
public EntitiesContext Context
{
get
{
if (contextInitialized == false)
{
HttpContext.Current.Items[CtxKey] = new EntitiesContext(ConfigurationManager.ConnectionStrings["CoonectionStringName"].ToString());
}
return (EntitiesContext)HttpContext.Current.Items[CtxKey];
}
}
public void TrulyDispose()
{
if (contextInitialized)
{
Context.Dispose();
HttpContext.Current.Items[CtxKey] = null;
}
}
}
internal static class EntityFrameworkExtensions
{
internal static ObjectQuery<T> Include<T>(this ObjectQuery<T> query,
Expression<Func<T, object>> propertyToInclude)
{
string include = string.Join(".", propertyToInclude.Body.ToString().Split('.').Skip(1).ToArray());
const string collectionsLinqProxy = ".First()";
include = include.Replace(collectionsLinqProxy, "");
return query.Include(include);
}
internal static string After(this string original, string search)
{
if (string.IsNullOrEmpty(original))
return string.Empty;
int index = original.IndexOf(search);
return original.Substring(index + search.Length);
}
}
}
彼が作成したConeryのMVCストアフロント 「サービス」と呼ばれる別の層; IQueryableを受け取ったレイヤー リポジトリからの結果でした さまざまな適用を担当 フィルター。
すべての場合において、サービス層を除き、誰もリポジトリと直接対話するべきではありません。
最も柔軟なのは、上記のコードと同じように、サービスが必要な方法でリポジトリとやり取りできるようにすることです(ただし、例のように1つのポイントでDRYコードを記述し、最適化の場所を見つけます)。
ただし、一般的なDDDパターンに関しては、「仕様」を使用するのがより適切です。パターン、すべてのフィルターなどを変数(通常はデリゲート型のLINQのクラスメンバー)でカプセル化します。 LINQを「コンパイル済みクエリ」と組み合わせると、これにより大きな最適化のメリットが得られます。 {Specification Pattern}と{LINQ Compiled Queries}をグーグルで検索すると、ここで私が言っていることに近づきます。
IEnumerableを返すメソッド(この場合はIQueryable)とCollectionを返すメソッド(コンテンツをリポジトリから送信する前にプルする)の2つのメソッドセットを作成しました。
これにより、リポジトリの外部のサービスでアドホッククエリを作成したり、副作用に強いコレクションを直接返すリポジトリメソッドを使用したりできます。つまり、2つのリポジトリエンティティを結合すると、見つかったエンティティごとに1つの選択クエリではなく、1つの選択クエリが作成されます。
本当に悪いことが起こらないように保護レベルを設定できると思います。
この問題の実行可能な解決策を自分で見つけるのに苦労していたので、リポジトリ内のリポジトリおよび作業単位パターンの実装ASP.NET MVCアプリケーション(9/10)の記事。
public virtual IEnumerable<TEntity> Get(
Expression<Func<TEntity, bool>> filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
string includeProperties = "")
{
IQueryable<TEntity> query = dbSet;
if (filter != null)
{
query = query.Where(filter);
}
foreach (var includeProperty in includeProperties.Split
(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
}
if (orderBy != null)
{
return orderBy(query).ToList();
}
else
{
return query.ToList();
}
}
この記事ではこの正確な問題については言及していませんが、一般的で再利用可能なリポジトリメソッドについては述べています。
これまでのところ、これが解決策として思いついたすべてです。