carga antecipada e padrão de repositório
-
06-07-2019 - |
Pergunta
Eu estou querendo saber como lidar corretamente com problema ansioso-loading para gráficos de objetos complexos quando usando o padrão Repository. Isso não é problema específico ORM eu acho.
Primeira tentativa:
public interface IProductRepository : IRepository<Product>
{
Product GetById(int id);
IProductRepository WithCustomers();
}
Isso poderia funcionar bem, mas isso implicaria repetir-me o tempo todo (escrevendo personalizado 'com' métodos em implementações de repositórios em todos os lugares).
A seguir abordagem:
public interface IRepository<T> where T : IAggregateRoot
{
...
void With(Expression<Func<T, object>> propToExpand);
}
método With
irá adicionar um item à coleção particular que será usado mais tarde para descobrir o que adereços deve estar ansioso carregado ao recuperar necessárias entidade / s.
obras um deste tipo e é muito bem. Mas eu não gosto de uso:
productRepository.With(x=>x.Customer);
productRepository.With(x=>x.Price);
productRepository.With(x=>x.Manufacturer);
var product = productRepository.GetById(id);
Basicamente - o problema é que não há encadeamento. Eu gostaria que fosse assim:
var product = productRepository
.With(x=>x.Customer)
.With(x=>x.Price)
.With(x=>x.Manufacturer)
.GetById(id);
eu não poderia conseguir isso . Mesmo se eu pudesse -. Eu não tenho certeza se essa solução seria elegante
Isto leva a pensamentos que eu estou faltando algo fundamental (falta de exemplos em qualquer lugar). São maneiras há diferentes como lidar com isso? Quais são as melhores práticas?
Solução
problema interessante e eu tenho certeza que você não é o primeiro a ter problemas com isso (eu absolutelty ter).
Para mim, a verdadeira questão é: onde você quer colocar sua lógica de carregamento ansioso?
Do lado de fora do repositório no código do cliente
var product = productRepository
.With(x=>x.Customer)
.With(x=>x.Price)
.With(x=>x.Manufacturer)
.GetById(id);
Eu não acho que é bom design de software:. Parece que isso poderia causar "morte por mil cortes" se tais construções estão espalhadas através de seu aplicativo inteiro
Ou dentro do repositório . Exemplo:
interface IProductRepository {
Product GetById(int id);
Product GetByIdWithCustomers(int i);
}
Portanto, o seu código de cliente ficaria assim:
var product = productRepository.GetByIdWithCustomers(id);
Normalmente eu fazer um BaseRepository que tem apenas as operações básicas CRUD definido:
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
}
Em seguida, estendo esta base de Classe / interface, a fim de proporcionar métodos específicos para a recuperação de objectos de domínio. Sua abordagem parece ir em uma direção um pouco semelhante.
public class MediaRepository : BaseRepository<Media, int> {
public long CountMediaWithCategories() { ... }
public IList<Media> MediaInCategories(IList<Category> categories) { .... }
}
A coisa boa: todo o material ORM (ansioso carregamento de configuração, buscar profundidade etc) é encapsulado na classe Repository, o código do cliente só fica o conjunto de resultados.
Eu tentei trabalhar com repositórios muito genéricas como você está tentando fazer, mas principalmente eu acabei escrevendo consultas e repositórios específicos para os meus objetos de domínio.
Outras dicas
var product = productRepository
.With(x=>x.Customer)
.With(x=>x.Price)
.With(x=>x.Manufacturer)
.GetById(id);
Eu posso entender o seu desejo de determinar a profundidade de consulta do objeto gráfico, como acima, mas eu acho que pode haver uma maneira mais fácil de fazê-lo. Que tal em vez de escolher para devolver um produto (com o Cliente, preço e Manufacturer) por ID i simplesmente devolver o Produto? - e todas essas outras coisas são propriedades carregadas preguiçosos de produtos
eu conseguir isso 'acessibilidade grafo completo' por 'encadeamento' pelo modelo de objeto POCO na minha camada de acesso a dados. Desta forma eu não preciso saber a quantidade de dados carregados ansiosos para retirar a qualquer momento, eu só pedir o que eu preciso do gráfico do objeto, eo modelo sabe o que é carregado e que precisa recuperar adicionalmente a partir da DAL. Dê uma olhada em estes três respostas - Eu tento explicar a minha abordagem lá. Se precisar de mais esclarecimentos me avise e eu vou editar esta resposta.
É uma questão de idade, mas talvez ele pode ajudar alguém. Eu passei algum tempo para encontrar um bom aproach, aqui é o que eu encontrei em 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();
}
}
Como usar:
var repository = new Repository<User>();
var users = repository.GetAll(p => p.Id == 1, d => d.Address, d => d.Carts);
Ref: Fazer a ligação
Eu posso apreciar o que você está tentando fazer, mas você está um pouco além do repositório-padrão básico.
A interface de repositório mínima pode incluir métodos para:
- GetById
- Adicionar
- Remover
Se você adicionar métodos adicionais em cima disso, você começar a correr em situações onde a interface não necessariamente fazem sentido para todas as suas raízes agregadas.
Às vezes é apenas não é viável ter um completamente bela API. Se o que você tem obras "suficientemente bom" para você, eu iria com ele. Se você precisa ficar longe do padrão de repositório para proporcionar uma melhor API para o programa contra, fazê-lo!
O padrão de repositório não é um ser-tudo extremidade-toda solução /. Às vezes você precisa de uma solução diferente.
Se você quiser indicar toda a Inclui você precisa fora de seu repositório, você pode listar parâmetros opcionais (C #) para cada método genérico:
TEntity Find(Func<TEntity, bool> expression, params string[] eagerLoads);
Em seguida, em seu camada do cliente:
IProductRepository.Find(x => x.Id == id, "Customer", "Price")
Se você quer ser tipo seguro, enumerar suas entidades:
public enum BusinessEntities { Customer, Price, Manufacturer }
IProductRepository.Find(x => x.Id == id, BusinessEntities.Customer.ToString(), BusinessEntities.Price.ToString())
Eu acho que é responsabilidade do cliente para pedir especificamente para o que quer. Repositório genérico deve apenas lidar com CRUD básico.
No BaseRepository.cs
você pode criar esse método:
public async Task<IEnumerable<T>> GetWithChild(string child)
{
return await _entities.Include(child).ToListAsync();
}
Na minha API eu também implementou uma camada de serviço, mas a partir da API eu simplesmente chamar esse método e passá-lo o nome da variável a carga.
Obviamente, em sua situação, você precisa incluir mais algumas cordas.
Eu postei uma resposta mais cedo, mas eu ainda não estava satisfeito com a solução. Então aqui está uma solução melhor.
nos 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();
}
e você pode simplesmente usar o método da seguinte maneira
await _service.GetAll(x => x.Customer, x => x.Price, x => x.Manufacturer);