Подтвердить что ты не робот

Нежелательная загрузка и шаблон хранилища

Мне интересно, как правильно обрабатывать проблему с нетерпением загрузки для сложных графов объектов при использовании шаблона репозитория. Я не думаю, что это не проблема с 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);

Я не мог этого достичь. Даже если бы я мог - я не уверен, будет ли это решение элегантным.

Это приводит к мысли, что я пропускаю что-то фундаментальное (отсутствие примеров где-либо). Существуют ли разные способы решения этой проблемы? Каковы наилучшие методы?

4b9b3361

Ответ 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

Я могу оценить то, что вы пытаетесь сделать, но вы находитесь за пределами основного шаблона репозитория.

Минимальный интерфейс репозитория может включать в себя методы для:

  • GetByID
  • Добавить
  • Удалить

Если вы добавите дополнительные методы поверх этого, вы начнете работать в ситуациях, когда интерфейс не обязательно имеет смысл для всех ваших совокупных корней.

Иногда просто невозможно реализовать полностью красивый 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);