¿Alternativas al patrón de repositorio para encapsular la lógica ORM?

24

Solo tuve que cambiar un ORM y fue una tarea relativamente desalentadora, porque la lógica de consulta se estaba filtrando por todas partes. Si alguna vez hubiera tenido que desarrollar una nueva aplicación, mi preferencia personal sería encapsular toda la lógica de consulta (usando un ORM) para prevenir el cambio en el futuro. El patrón de repositorio es bastante problemático para codificar y mantener, así que me preguntaba si hay algún otro patrón para resolver el problema.

Puedo prever publicaciones sobre no agregar complejidad adicional antes de que sea realmente necesario, ser ágil, etc., pero solo estoy interesado en los patrones existentes para resolver un problema similar de una manera más simple.

Mi primer pensamiento fue tener un repositorio de tipos genéricos, al que agrego métodos según sea necesario para clases específicas de repositorios de tipos a través de métodos de extensión, pero las pruebas unitarias de métodos estáticos son terriblemente dolorosas. ES DECIR:

public static class PersonExtensions
{
    public static IEnumerable<Person> GetRetiredPeople(this IRepository<Person> personRep)
    {
        // logic
    }
}
Dante
fuente
55
¿Qué es exactamente sobre el patrón de repositorio que le resulta difícil codificar y mantener?
Matthew Flynn el
1
Hay alternativas por ahí. Uno de ellos es usar el patrón de Objeto de consulta que, aunque no he usado, parece un buen enfoque. El repositorio en mi humilde opinión es un buen patrón si se usa correctamente, pero no el ser todo y terminar todo
dreza

Respuestas:

14

En primer lugar: un repositorio genérico debe considerarse una clase base y no implementaciones completas. Debería ayudarlo a implementar los métodos CRUD comunes. Pero aún debe implementar los 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:

No regrese IQueryable. Es una abstracción permeable. Intente implementar esa interfaz usted mismo o úselo sin conocer el proveedor de base de datos subyacente. Por ejemplo, cada proveedor de db tiene su propia API para cargar entidades con entusiasmo. Por eso es una abstracción permeable.

Alternativa

Como alternativa, puede usar consultas (por ejemplo, según lo define el patrón de separación Comando / Consulta). CQS se describe en wikipedia.

Básicamente crea clases de consulta que invoca:

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


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

Lo mejor de las consultas es que puede utilizar diferentes métodos de acceso a datos para diferentes consultas sin saturar el código. por ejemplo, puede usar un servicio web en uno, nhibernate en otro y ADO.NET en un tercero.

Si está interesado en una implementación de .NET, lea mi artículo: http://blog.gauffin.org/2012/10/griffin-decoupled-the-queries/

jgauffin
fuente
¿No sería el patrón alternativo que sugirió crear aún más desorden en proyectos más grandes, ya que lo que sería un método en el patrón de repositorio ahora es una clase completa?
Dante
3
Si su definición de tener clases pequeñas es desordenada, bueno, sí. En ese caso, tampoco debe intentar seguir los principios SÓLIDOS, ya que su aplicación siempre producirá más clases (más pequeñas).
jgauffin
2
Las clases más pequeñas son mejores, pero la navegación de las clases de consulta existentes no sería más problemática que decir, navegar por el intellisense de un repositorio (de dominio) para ver si la funcionalidad necesaria se encuentra allí y, de no ser así, implementarla.
Dante
44
La estructura del proyecto se vuelve más importante. Si coloca todas las consultas en un espacio de nombres como YourProject.YourDomainModelName.Querieseste, será fácil navegar.
jgauffin
Una cosa que me resulta difícil hacer en el buen sentido con CQS son las consultas comunes. Por ejemplo, dado un usuario, una consulta obtiene todos los estudiantes asociados (calificaciones diferentes, hijos propios, etc.). Ahora, otra consulta necesita obtener hijos para un usuario y luego hacer alguna operación en ellos, por lo que la consulta 2 puede aprovechar la lógica común en la consulta 1, pero no hay una manera simple de hacerlo. No he podido encontrar ninguna implementación de CQS que facilite esto fácilmente. Los repositorios ayudan mucho a este respecto. No soy fanático de ninguno de los patrones, sino solo compartir mi opinión
Mrchief
8

Lo primero que creo que digo es que ORM ya es una abstracción lo suficientemente grande sobre su base de datos. Algunos ORM proporcionan enlaces a todas las bases de datos relacionales comunes (NHibernate tiene enlaces a MSSQL, Oracle, MySQL, Postgress, etc.) Por lo tanto, construir nuevas abstracciones no me parece rentable. Además, argumentando que necesita "abstraer" este ORM no tiene sentido.

Si todavía desea construir esta abstracción, iría en contra del patrón de repositorio. Fue genial en la era del SQL puro, pero es bastante problemático frente al ORM moderno. Principalmente porque terminas re-implementando la mayoría de las características de ORM, como las operaciones CRUD y las consultas.

Si tuviera que construir tales abstracciones, usaría estas reglas / patrones

  • CQRS
  • Utilice la mayor cantidad posible de funciones ORM existentes
  • Encapsula solo lógica compleja y consultas
  • Trate de tener "plomería" del ORM oculto dentro de una arquitectura

En la implementación concreta, usaría las operaciones CRUD de ORM directamente, sin ningún ajuste. También haría consultas simples directamente en el código. Pero las consultas complejas se encapsularían en sus propios objetos. Para "ocultar" el ORM, intentaría inyectar el contexto de datos de forma transparente en un servicio / objetos de interfaz de usuario y hacer lo mismo para consultar objetos.

Lo último que me gustaría decir es que muchas personas usan ORM sin saber cómo usarlo y cómo obtener el mejor "beneficio". Las personas que recomiendan repositorios suelen ser de este tipo.

Como lectura recomendada, diría el blog de Ayende , especialmente este artículo .

Eufórico
fuente
+1 "Lo último que me gustaría decir es que muchas personas usan ORM sin saber cómo usarlo y cómo obtener el mejor" beneficio ". Las personas que recomiendan repositorios suelen ser de este tipo"
Misters
1

El problema con la implementación con fugas es que necesita muchas condiciones de filtro diferentes.

Puede reducir esta API si implementa un método de repositorio FindByExample y lo usa así

// 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
fuente
0

Considere hacer que sus extensiones operen en IQueryable en lugar de IRepository.

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

A prueba unitaria:

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

No se requiere burlarse de fancing para las pruebas. También puede combinar estos métodos de extensión para crear consultas más complejas según sea necesario.

Jared S
fuente
55
-1 a) IQueryablees una mala madre, o más bien una interfaz de DIOS. La implementación es muy necesaria y hay muy pocos (si los hay) proveedores completos de LINQ to Sql. b) Los métodos de extensión hacen imposible la extensión (uno de los principios fundamentales de OOP).
jgauffin
0

Escribí un patrón de objeto de consulta bastante bueno para NHibernate aquí: https://github.com/shaynevanasperen/NHibernate.Sessions.Operations

Funciona usando una interfaz 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 una clase de entidad POCO como esta:

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

Puede construir 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; }
}

Y luego úsalos así:

var database1Poco = _databases.Query(new Database1PocoByProperty1 { Property1 = 1 });
Shayne
fuente
1
Si bien este enlace puede responder la pregunta, es mejor incluir las partes esenciales de la respuesta aquí y proporcionar el enlace como referencia. Las respuestas de solo enlace pueden volverse inválidas si la página vinculada cambia.
Dan Pichelman
Mis disculpas, tenía prisa. Respuesta actualizada para mostrar un código de ejemplo.
Shayne
1
+1 para mejorar la publicación con más contenido.
Songo