repository pattern generic Стремительная загрузка и шаблон хранилища



repository pattern java (6)

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

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

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


Answer #1
var product = productRepository
 .With(x=>x.Customer)
 .With(x=>x.Price)
 .With(x=>x.Manufacturer)
 .GetById(id);

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

Я достигаю этой «полной доступности графа» путем «сцепления» объектной модели POCO в моем слое доступа к данным. Таким образом, мне не нужно знать, сколько загруженных данных вытащить за один раз, я просто спрашиваю, что мне нужно из графа объектов, и модель знает, что загружено и что нужно восстанавливать дополнительно из DAL. Взгляните на these three answers - я пытаюсь объяснить свой подход там. Если вам нужно больше разъяснений, дайте мне знать, и я отредактирую этот ответ.


Answer #2

Если вы хотите указать все Включения, которые вам нужны за пределами вашего репозитория, вы можете перечислить необязательные параметры (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())

Я думаю, что клиент должен спросить конкретно, чего он хочет. Универсальный репозиторий должен просто обрабатывать базовый CRUD.


Answer #3

Я отправил ответ ранее, но я все еще не был доволен решением. Так что вот лучшее решение.

в 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); 

Answer #4

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

Ссылка: Link


Answer #5

Интересная проблема, и я уверен, что вы не первый, у кого проблемы с этим (у меня есть абсолютность).

Для меня реальный вопрос: куда вы хотите поместить свою нетерпеливую логику загрузки?

Вне репозитория в коде клиента

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 (энергичная загрузка конфигурации, глубина выборки и т. Д.) Инкапсулированы в классе Repository, клиентский код просто получает набор результатов.

Я пытался работать с очень общими репозиториями, как вы пытаетесь, но в основном я написал конкретные запросы и репозитории для своих доменных объектов.


Answer #6

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

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

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

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

Иногда просто невозможно иметь совершенно красивый API. Если то, что у вас есть, работает «достаточно хорошо» для вас, я бы пошел с этим. Если вам нужно уйти от шаблона репозитория, чтобы обеспечить лучший API для программирования, сделайте это!

Шаблон репозитория не является окончательным решением. Иногда вам нужно другое решение.