Нежелательная загрузка и шаблон хранилища
Мне интересно, как правильно обрабатывать проблему с нетерпением загрузки для сложных графов объектов при использовании шаблона репозитория. Я не думаю, что это не проблема с ORM.
Первая попытка:
public interface IProductRepository : IRepository<Product>
{
Product GetById(int id);
IProductRepository WithCustomers();
}
Это будет работать нормально, но это потребует повторения себя все время (написание пользовательских методов "С" во всех реализациях репозитория).
Следующий подход:
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);
Я не мог этого достичь. Даже если бы я мог - я не уверен, будет ли это решение элегантным.
Это приводит к мысли, что я пропускаю что-то фундаментальное (отсутствие примеров где-либо). Существуют ли разные способы решения этой проблемы? Каковы наилучшие методы?
Ответы
Ответ 1
Интересная проблема, и я уверен, что вы не первый, у кого проблемы с этим (у меня есть абсолют).
Для меня реальный вопрос: где вы хотите поставить свою нетерпеливую логику загрузки?
Вне репозитория в клиентском коде
var product = productRepository
.With(x=>x.Customer)
.With(x=>x.Price)
.With(x=>x.Manufacturer)
.GetById(id);
Я не думаю, что это хороший дизайн программного обеспечения: похоже, что это может привести к "смерти тысячами разрезов", если такие конструкции разбросаны по всему вашему приложению.
Или в репозитории. Пример:
interface IProductRepository {
Product GetById(int id);
Product GetByIdWithCustomers(int i);
}
Таким образом, ваш код клиента будет выглядеть следующим образом:
var product = productRepository.GetByIdWithCustomers(id);
Обычно я делаю один BaseRepository, который имеет только основные операции CRUD:
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 (загружаемая конфигурация, глубина выборки и т.д.) инкапсулируются в класс репозитория, клиентский код просто получает набор результатов.
Я пробовал работать с очень универсальными репозиториями, как вы пытаетесь сделать, но в основном я писал конкретные запросы и репозитории для своих объектов домена.
Ответ 2
var product = productRepository
.With(x=>x.Customer)
.With(x=>x.Price)
.With(x=>x.Manufacturer)
.GetById(id);
Я могу понять ваше желание определить глубину запроса графического объекта, как показано выше, но я думаю, что может быть более простой способ сделать это. Как насчет того, чтобы вместо того, чтобы возвращать продукт (с клиентом, ценой и изготовителем) по идентификатору, я просто возвращаю продукт - и все эти вещи являются ленивыми загруженными свойствами продукта?
Я достигаю этой "полной доступности графа" путем "цепочки" по объектной модели POCO на моем уровне доступа к данным. Таким образом, мне не нужно знать, сколько загруженных загружаемых данных можно вытащить в любой момент времени, я просто спрошу, что мне нужно от графа объектов, и модель знает, что загружается и что нужно восстановить из DAL. Посмотрите эти три ответы - Я пытаюсь объяснить свой подход там. Если вам нужно больше разъяснений, дайте мне знать, и я отредактирую этот ответ.
Ответ 3
Это старый вопрос, но, возможно, он может помочь кому-то. Я провел некоторое время, чтобы найти хорошее aproach, вот что я нашел в С#:
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);
Ссылка: Ссылка
Ответ 4
Я могу оценить то, что вы пытаетесь сделать, но вы находитесь за пределами основного шаблона репозитория.
Минимальный интерфейс репозитория может включать в себя методы для:
Если вы добавите дополнительные методы поверх этого, вы начнете работать в ситуациях, когда интерфейс не обязательно имеет смысл для всех ваших совокупных корней.
Иногда просто невозможно реализовать полностью красивый API. Если то, что у вас есть, работает "достаточно хорошо" для вас, я бы пошел с ним. Если вам нужно уйти от шаблона репозитория, чтобы предоставить лучший API для программирования, сделайте это!
Шаблон репозитория не является решением "все-все-все-все-все". Иногда вам нужно другое решение.
Ответ 5
Если вы хотите указать все необходимые для вас компоненты вне своего репозитория, вы можете указать необязательные параметры (С#) для каждого общего метода:
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.
Ответ 6
В BaseRepository.cs
вы можете создать этот метод:
public async Task<IEnumerable<T>> GetWithChild(string child)
{
return await _entities.Include(child).ToListAsync();
}
В моем API я также реализовал сервисный уровень, но из API я просто вызываю этот метод и передаю ему имя загружаемой переменной.
Очевидно, что в вашей ситуации вам нужно будет добавить еще несколько строк.
Ответ 7
Я отправил ответ раньше, но я все еще не был доволен решением. Итак, вот лучшее решение.
в 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);