Alternativas ao padrão do repositório para encapsular a lógica ORM?

24

Eu apenas tive que mudar um ORM e foi uma tarefa relativamente assustadora, porque a lógica da consulta estava vazando por toda parte. Se eu tivesse que desenvolver um novo aplicativo, minha preferência pessoal seria encapsular toda a lógica de consulta (usando um ORM) para protegê-lo de alterações no futuro. Padrão de repositório é bastante problemático para codificar e manter, então eu queria saber se existem outros padrões para resolver o problema?

Posso prever postagens sobre não adicionar complexidade extra antes que seja realmente necessário, ser ágil etc., mas estou interessado apenas nos padrões existentes que resolvem um problema semelhante de uma maneira mais simples.

Meu primeiro pensamento foi ter um repositório de tipos genéricos, ao qual adiciono métodos conforme necessário a classes de repositórios de tipos específicos por meio de métodos de extensão, mas os métodos estáticos de teste de unidade são terrivelmente dolorosos. IE:

public static class PersonExtensions
{
    public static IEnumerable<Person> GetRetiredPeople(this IRepository<Person> personRep)
    {
        // logic
    }
}
Dante
fonte
5
O que exatamente é sobre o padrão de repositório que você acha problemático para codificar e manter?
Matthew Flynn
1
Existem alternativas por aí. Uma delas é usar o padrão Query Object que, embora eu não tenha usado, parece ser uma boa abordagem. Repositório IMHO é um bom padrão, se usado corretamente, mas não o be all e end all
#

Respostas:

14

Primeiro de tudo: um repositório genérico deve ser considerado uma classe base e não implementações completas. Isso deve ajudá-lo a implementar os métodos CRUD comuns. Mas você ainda precisa implementar os métodos de consulta:

public class UserRepository : EntityFrameworkRepository<User>, IUserRepository
{
    public UserRepository(DbContext context){}

    public ICollection<Person> FindRetired()
    {
        return context.Persons.Where(p => p.Status == "Retired").ToList();
    }
}

Segundo:

Não volte IQueryable. É uma abstração com vazamento. Tente implementar você mesmo essa interface ou use-a sem conhecimento sobre o provedor de banco de dados subjacente. Por exemplo, todo provedor de banco de dados tem sua própria API para carregar entidades com entusiasmo. É por isso que é uma abstração com vazamento.

Alternativo

Como alternativa, você pode usar consultas (por exemplo, conforme definido pelo padrão de separação Comando / Consulta). O CQS é descrito na wikipedia.

Você basicamente cria classes de consulta que você chama:

public class GetMyMessages
{
    public GetMyMessages(IDbConnection connection)
    {
    }


    public Message[] Execute(DateTime minDate)
    {
        //query
    }
}

o melhor das consultas é que você pode usar métodos diferentes de acesso a dados para consultas diferentes sem sobrecarregar o código. você pode, por exemplo, usar um serviço da web em um, inibir em outro e ADO.NET em um terceiro.

Se você estiver interessado em uma implementação .NET, leia meu artigo: http://blog.gauffin.org/2012/10/griffin-decoupled-the-queries/

jgauffin
fonte
O padrão alternativo não seria sugerido? Você sugeriu criar ainda mais confusão em projetos maiores, visto que o que seria um método no Padrão de Repositório agora é uma classe inteira?
Dante
3
Se sua definição de ter turmas pequenas é desordenada, bem, sim. Nesse caso, você não deve tentar seguir os princípios do SOLID, pois sua aplicação sempre produzirá mais classes (menores).
jgauffin
2
Classes menores são melhores, mas a navegação pelas classes de consulta existentes não seria mais problemática do que dizer, navegando no intellisense de um repositório (domínio) para ver se a funcionalidade necessária está lá e, se não, implementá-la.
Dante
4
A estrutura do projeto se torna mais importante. Se você colocar todas as consultas em um espaço para nome YourProject.YourDomainModelName.Queries, será fácil navegar.
jgauffin
Uma coisa que estou achando difícil fazer de uma maneira boa com o CQS são as consultas comuns. Por exemplo, dado um usuário, uma consulta obtém todos os alunos associados (notas de diferença, filhos próprios etc.). Agora, outra consulta precisa obter filhos para um usuário e, em seguida, executar alguma operação neles - portanto, a consulta 2 pode aproveitar a lógica comum na consulta 1, mas não há uma maneira simples de fazer isso. Não consegui encontrar nenhuma implementação do CQS que facilite isso facilmente. Os repositórios ajudam bastante nesse aspecto. Não é fã de nenhum dos dois padrões, mas apenas compartilhando minha opinião.
Mrchief
8

Primeiro, acho que digo que o ORM já é suficientemente grande para abstração no seu banco de dados. Alguns ORMs fornecem ligação a todos os bancos de dados relacionais comuns (o NHibernate possui ligações ao MSSQL, Oracle, MySQL, Postgress, etc.). Portanto, criar uma nova abstração em cima dela não me parece rentável. Além disso, argumentar que você precisa "abstrair" esse ORM não faz sentido.

Se você ainda deseja construir essa abstração, eu iria contra o padrão do Repositório. Foi ótimo na era do SQL puro, mas é bastante problemático diante do ORM moderno. Principalmente porque você acaba reimplementando a maioria dos recursos do ORM, como operações e consultas CRUD.

Se eu fosse construir tais abstrações, usaria essas regras / padrões

  • CQRS
  • Use o máximo possível dos recursos ORM existentes
  • Encapsula apenas lógica e consultas complexas
  • Tente ocultar o "encanamento" do ORM dentro de uma arquitetura

Na implementação concreta, eu usaria operações CRUD do ORM diretamente, sem nenhuma quebra. Eu também faria consultas simples diretamente no código. Mas consultas complexas seriam encapsuladas em seus próprios objetos. Para "ocultar" o ORM, tentaria injetar o contexto de dados de forma transparente em objetos de serviço / interface do usuário e faria o mesmo para consultar objetos.

A última coisa que gostaria de dizer é que muitas pessoas usam o ORM sem saber como usá-lo e como obter o melhor "lucro" dele. As pessoas que recomendam repositórios geralmente são desse tipo.

Como leitura recomendada, diria o blog de Ayende , especialmente este artigo .

Eufórico
fonte
+1 "A última coisa que gostaria de dizer é que muitas pessoas usam o ORM sem saber como usá-lo e como obter o melhor" lucro "dele. As pessoas que recomendam repositórios geralmente são desse tipo"
Misters,
1

O problema com a implementação com vazamento é que você precisa de muitas condições de filtro diferentes.

Você pode restringir essa API se implementar um método de repositório FindByExample e usá-lo desta maneira

// find all retired persons
Person filter = new Person {Status=PersonStatus.Retired};
IEnumerable<Person> found = personRepository.FindByExample(filter);


// find all persons who have a dog named "sam"
Person filter = new Person();
filter.AddPet(new Dog{Name="sam"});
IEnumerable<Person> found = personRepository.FindByExample(filter);
k3b
fonte
0

Considere fazer suas extensões operar no IQueryable em vez do IRepository.

public static class PersonExtensions
{
    public static IQueryable<Person> AreRetired(this IQueryable<Person> people)
    {
        return people.Where(p => p.Status == "Retired");
    }
}

Para teste de unidade:

List<Person> people = new List<Person>();
people.Add(new Person() { Name = "Bob", Status = "Retired" });
people.Add(new Person() { Name = "Sam", Status = "Working" });

var retiredPeople = people.AsQueryable().AreRetired();
// assert that Bob is in the list
// assert that Sam is not in the list

Nenhuma zombaria de financiamento é necessária para o teste. Você também pode combinar esses métodos de extensão para criar consultas mais complexas, conforme necessário.

Jared S
fonte
5
-1 a) IQueryableé uma mãe ruim, ou melhor, uma interface GOD. Implementá-lo é muito necessário e existem muito poucos (se houver) fornecedores completos de LINQ to Sql. b) Os métodos de extensão tornam impossível a extensão (um dos princípios fundamentais de POO).
Javauffin
0

Eu escrevi um padrão de objeto de consulta bastante elegante para o NHibernate aqui: https://github.com/shaynevanasperen/NHibernate.Sessions.Operations

Funciona usando uma interface como esta:

public interface IDatabases
{
    ISessionManager SessionManager { get; }

    T Query<T>(IDatabaseQuery<T> query);
    T Query<T>(ICachedDatabaseQuery<T> query);

    void Command(IDatabaseCommand command);
    T Command<T>(IDatabaseCommand<T> command);
}

Dada uma classe de entidade POCO como esta:

class Database1Poco
{
    public int Property1 { get; set; }
    public string Property2 { get; set; }
}

Você pode criar objetos de consulta como este:

class Database1PocoByProperty1 : DatabaseQuery<Database1Poco>
{
    public override Database1Poco Execute(ISessionManager sessionManager)
    {
        return sessionManager.Session.Query<Database1Poco>().SingleOrDefault(x => x.Property1 == Property1);
    }

    public int Property1 { get; set; }
}

E depois use-os assim:

var database1Poco = _databases.Query(new Database1PocoByProperty1 { Property1 = 1 });
Shayne
fonte
1
Embora esse link possa responder à pergunta, é melhor incluir aqui as partes essenciais da resposta e fornecer o link para referência. As respostas somente para links podem se tornar inválidas se a página vinculada for alterada.
Dan Pichelman
Minhas desculpas, eu estava com pressa. Resposta atualizada para mostrar algum código de exemplo.
21315 Shayne
1
+1 para melhorar a postagem com mais conteúdo.
Songo