Вопрос

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

Я не думаю, что это хороший дизайн программного обеспечения: похоже, это может привести к "смерти от тысячи порезов" если такие конструкции разбросаны по всему вашему приложению.

Или в хранилище . Пример:

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, клиентский код просто получает набор результатов.

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

Другие советы

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

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

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

Это старый вопрос, но, возможно, он кому-то поможет.Я потратил некоторое время, чтобы найти хороший подход, вот что я нашел в C#:

IRepository.cs:

public interface IRepository<TEntity> where TEntity : class
{
    IEnumerable<TEntity> GetAll(Expression<Func<TEntity, bool>> where
                              , params Expression<Func<TEntity, object>>[] properties);
}

Репозиторий.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())

Я думаю, что клиент должен спросить конкретно, чего он хочет. Универсальный репозиторий должен просто обрабатывать базовый 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); 
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top