Comandos de consulta y / o especificaciones bien diseñados


Llevo bastante tiempo buscando una buena solución a los problemas que presenta el típico patrón de repositorio (lista creciente de métodos para consultas especializadas, etc.).. véase: http://ayende.com/blog/3955/repository-is-the-new-singleton).

Me gusta mucho la idea de usar consultas de comandos, particularmente a través del uso del patrón de Especificación. Sin embargo, mi problema con la especificación es que solo se relaciona con los criterios de selecciones simples (básicamente, el where clause), y no se ocupa de las otras cuestiones de las consultas, como la unión, agrupación, selección de subconjuntos o proyección, etc.. básicamente, todos los aros adicionales que muchas consultas deben pasar para obtener el conjunto correcto de datos.

(nota: Uso el término "comando" como en el patrón de comandos, también conocido como objetos de consulta. No estoy hablando de comando como en la separación comando / consulta donde hay una distinción hecha entre consultas y comandos (actualizar, eliminar, insertar))

Así que estoy buscando alternativas que encapsulen toda la consulta, pero lo suficientemente flexibles como para que no solo estés intercambiando repositorios spaghetti por una explosión de clases de comando.

He usado, por ejemplo, Linqspecs, y aunque encuentro algo de valor en poder asignar nombres significativos a los criterios de selección, simplemente no es suficiente. Tal vez estoy buscando una solución combinada que combina múltiples enfoques.

Estoy buscando soluciones que otros puedan haber desarrollado para abordar este problema, o abordar un problema diferente, pero todavía satisface estos requisitos. En el artículo vinculado, Ayende sugiere usar el contexto NHibernate directamente, pero creo que eso complica en gran medida su capa de negocio porque ahora también tiene que contener información de consulta.

Ofreceré una recompensa por esto, tan pronto como transcurra el período de espera. Así que, por favor, haga que sus soluciones sean dignas de recompensa, con buenas explicaciones y seleccionaré la mejor solución, y votaré a los corredores hasta.

NOTA: Estoy buscando algo que esté basado en OR. No tiene que ser EF o NHibernate explícitamente, pero esos son los más comunes y encajarían mejor. Si se puede adaptar fácilmente a otros OR, eso sería una ventaja. Linq compatible también estaría bien.

ACTUALIZACIÓN: Estoy realmente sorprendido de que no haya muchas buenas sugerencias aquí. Parece que las personas son totalmente CQR, o están completamente en el campamento de Repositorios. La mayoría de mis aplicaciones no son lo suficientemente complejas para warrant CQRS (algo con la mayoría de los defensores de CQRS dicen fácilmente que no debe usarlo).

ACTUALIZACIÓN: Parece que hay un poco de confusión aquí. No estoy buscando una nueva tecnología de acceso a datos, sino una interfaz razonablemente bien diseñada entre el negocio y los datos.

Idealmente, lo que estoy buscando es algún tipo de cruce entre objetos de consulta, patrón de especificación y repositorio. Como he dicho anteriormente, el patrón de especificación solo se ocupa del aspecto de la cláusula where, y no los otros aspectos de la consulta, tales como joins, sub-selects, etc.. Los repositorios se ocupan de toda la consulta, pero se salen de control después de un tiempo. Los objetos de consulta también se ocupan de toda la consulta, pero no quiero simplemente reemplazar los repositorios con explosiones de objetos de consulta.

Author: Erik Funkenbusch, 2013-01-20

4 answers

Descargo de responsabilidad: Dado que aún no hay grandes respuestas, decidí publicar una parte de una gran publicación de blog que leí hace un tiempo, copiada casi textualmente. Puedes encontrar la entrada completa del blog aquí. Así que aquí está:


Podemos definir las siguientes dos interfaces:

public interface IQuery<TResult>
{
}

public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
    TResult Handle(TQuery query);
}

El IQuery<TResult> especifica un mensaje que define una consulta específica con los datos que devuelve utilizando el tipo genérico TResult. Con la interfaz previamente definida podemos definir un mensaje de consulta así:

public class FindUsersBySearchTextQuery : IQuery<User[]>
{
    public string SearchText { get; set; }
    public bool IncludeInactiveUsers { get; set; }
}

Esta clase define una operación de consulta con dos parámetros, que dará como resultado una matriz de objetos User. La clase que maneja este mensaje se puede definir de la siguiente manera:

public class FindUsersBySearchTextQueryHandler
    : IQueryHandler<FindUsersBySearchTextQuery, User[]>
{
    private readonly NorthwindUnitOfWork db;

    public FindUsersBySearchTextQueryHandler(NorthwindUnitOfWork db)
    {
        this.db = db;
    }

    public User[] Handle(FindUsersBySearchTextQuery query)
    {
        return db.Users.Where(x => x.Name.Contains(query.SearchText)).ToArray();
    }
}

Ahora podemos dejar que los consumidores dependan de la interfaz genérica IQueryHandler:

public class UserController : Controller
{
    IQueryHandler<FindUsersBySearchTextQuery, User[]> findUsersBySearchTextHandler;

    public UserController(
        IQueryHandler<FindUsersBySearchTextQuery, User[]> findUsersBySearchTextHandler)
    {
        this.findUsersBySearchTextHandler = findUsersBySearchTextHandler;
    }

    public View SearchUsers(string searchString)
    {
        var query = new FindUsersBySearchTextQuery
        {
            SearchText = searchString,
            IncludeInactiveUsers = false
        };

        User[] users = this.findUsersBySearchTextHandler.Handle(query);    
        return View(users);
    }
}

Inmediatamente este modelo nos da mucha flexibilidad, porque ahora podemos decidir qué inyectar en el UserController. Podemos inyectar una implementación completamente diferente, o una que envuelva lo real implementación, sin tener que realizar cambios en UserController (y todos los demás consumidores de esa interfaz).

La interfaz IQuery<TResult> nos da soporte en tiempo de compilación al especificar o inyectar IQueryHandlers en nuestro código. Cuando cambiamos el FindUsersBySearchTextQuery para devolver UserInfo[] en su lugar (implementando IQuery<UserInfo[]>), el UserController fallará al compilar, ya que la restricción de tipo genérico en IQueryHandler<TQuery, TResult> no será capaz de mapear FindUsersBySearchTextQuery a User[].

Inyectar la interfaz IQueryHandler en un consumidor sin embargo, tiene algo menos problemas obvios que aún deben abordarse. El número de dependencias de nuestros consumidores puede llegar a ser demasiado grande y puede conducir a la sobreinyección del constructor-cuando un constructor toma demasiados argumentos. El número de consultas que ejecuta una clase puede cambiar con frecuencia, lo que requeriría cambios constantes en el número de argumentos del constructor.

Podemos solucionar el problema de tener que inyectar demasiados IQueryHandlers con una capa extra de abstracción. Creamos un mediador que se sienta entre el los consumidores y los manejadores de consultas:

public interface IQueryProcessor
{
    TResult Process<TResult>(IQuery<TResult> query);
}

La IQueryProcessor es una interfaz no genérica con un método genérico. Como puede ver en la definición de la interfaz, IQueryProcessor depende de la interfaz IQuery<TResult>. Esto nos permite tener soporte de tiempo de compilación en nuestros consumidores que dependen del IQueryProcessor. Reescribamos el UserController para usar el nuevo IQueryProcessor:

public class UserController : Controller
{
    private IQueryProcessor queryProcessor;

    public UserController(IQueryProcessor queryProcessor)
    {
        this.queryProcessor = queryProcessor;
    }

    public View SearchUsers(string searchString)
    {
        var query = new FindUsersBySearchTextQuery
        {
            SearchText = searchString,
            IncludeInactiveUsers = false
        };

        // Note how we omit the generic type argument,
        // but still have type safety.
        User[] users = this.queryProcessor.Process(query);

        return this.View(users);
    }
}

El UserController ahora depende de un IQueryProcessor que puede manejar todas nuestras consultas. El método UserController ' s SearchUsers llama al método IQueryProcessor.Process pasando un objeto de consulta inicializado. Dado que FindUsersBySearchTextQuery implementa la interfaz IQuery<User[]>, podemos pasarla al método genérico Execute<TResult>(IQuery<TResult> query). Gracias a la inferencia de tipo C#, el compilador es capaz de determinar el tipo genérico y esto nos ahorra tener que declarar explícitamente el tipo. También se conoce el tipo de retorno del método Process.

Ahora es responsabilidad de la implementación del IQueryProcessor encontrar el derecho IQueryHandler. Esto requiere algo de escritura dinámica, y opcionalmente el uso de una inyección de dependencias framework, y todo se puede hacer con solo unas pocas líneas de código:

sealed class QueryProcessor : IQueryProcessor
{
    private readonly Container container;

    public QueryProcessor(Container container)
    {
        this.container = container;
    }

    [DebuggerStepThrough]
    public TResult Process<TResult>(IQuery<TResult> query)
    {
        var handlerType = typeof(IQueryHandler<,>)
            .MakeGenericType(query.GetType(), typeof(TResult));

        dynamic handler = container.GetInstance(handlerType);

        return handler.Handle((dynamic)query);
    }
}

La clase QueryProcessor construye un tipo IQueryHandler<TQuery, TResult> específico basado en el tipo de la instancia de consulta suministrada. Este tipo se usa para pedir a la clase contenedor suministrada que obtenga una instancia de ese tipo. Desafortunadamente necesitamos llamar al método Handle usando reflexión (usando la palabra clave dymamic de C # 4.0 en este caso), porque en este punto es imposible convertir la instancia del controlador, ya que el argumento genérico TQuery no es disponible en tiempo de compilación. Sin embargo, a menos que el método Handle sea renombrado u obtenga otros argumentos, esta llamada nunca fallará y si lo desea, es muy fácil escribir una prueba unitaria para esta clase. El uso de la reflexión dará una ligera caída, pero no es nada de lo que realmente preocuparse.


Para responder a una de sus preocupaciones:

Así que estoy buscando alternativas que encapsulen toda la consulta, pero todavía lo suficientemente flexible como para que no solo intercambies espaguetis Repositorios para una explosión de clases de mando.

Una consecuencia de usar este diseño es que habrá muchas clases pequeñas en el sistema, pero tener muchas clases pequeñas/enfocadas (con nombres claros) es algo bueno. Este enfoque es claramente mucho mejor que tener muchas sobrecargas con diferentes parámetros para el mismo método en un repositorio, ya que puede agruparlos en una clase de consulta. Así que todavía obtienes muchas menos clases de consulta que métodos en un repositorio.

 80
Author: david.s,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-08-11 10:32:20

Mi manera de lidiar con eso es en realidad simplista y agnóstica. Mi punto de vista para un repositorio es este: El trabajo del repositorio es proporcionar a la aplicación el modelo requerido para el contexto, por lo que la aplicación solo le pregunta al repositorio qué quiere, pero no le dice cómo obtenerlo.

Proporciono al método repository un Criterio (sí, estilo DDD), que será utilizado por el repositorio para crear la consulta (o lo que sea necesario, puede ser una solicitud de servicio web). Se une y los grupos en mi humilde opinión son detalles de cómo, no el qué y un criterio debe ser solo la base para construir una cláusula where.

Modelo = el objeto final o la estructura de datos neede por la aplicación.

public class MyCriteria
{
   public Guid Id {get;set;}
   public string Name {get;set;}
    //etc
 }

 public interface Repository
  {
       MyModel GetModel(Expression<Func<MyCriteria,bool>> criteria);
   }

Probablemente pueda usar el criterio OR (Nhibernate) directamente si lo desea. La implementación del repositorio debe saber cómo usar los Criterios con el almacenamiento subyacente o DAO.

No conozco su dominio y los requisitos del modelo, pero sería extraño si la mejor manera es que la aplicación para construir la consulta en sí. El modelo cambia tanto que no se puede definir algo estable?

Esta solución claramente requiere algún código adicional, pero no acopla el resto a un OR o lo que sea que esté utilizando para acceder al almacenamiento. El repositorio hace su trabajo para actuar como una fachada y IMO está limpio y el código 'criteria translation' es reutilizable

 4
Author: MikeSW,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-01-20 08:40:42

He hecho esto, soportado esto y deshecho esto.

El mayor problema es este: no importa cómo lo hagas, la abstracción añadida no te da independencia. Se filtrará por definición. En esencia, estás inventando una capa completa solo para hacer que tu código se vea lindo... pero no reduce el mantenimiento, mejora la legibilidad ni te gana ningún tipo de modelo de agnosticismo.

La parte divertida es que respondiste a tu propia pregunta en respuesta a la respuesta de Olivier: "esto es esencialmente duplicar la funcionalidad de Linq sin todos los beneficios que obtienes de Linq".

Pregúntate: ¿cómo no podría ser?

 2
Author: Stu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-01-28 19:43:19

Puede utilizar una interfaz fluida. La idea básica es que los métodos de una clase devuelven la instancia actual esta misma clase después de haber realizado alguna acción. Esto le permite encadenar llamadas a métodos.

Al crear una jerarquía de clases apropiada, puede crear un flujo lógico de métodos accesibles.

public class FinalQuery
{
    protected string _table;
    protected string[] _selectFields;
    protected string _where;
    protected string[] _groupBy;
    protected string _having;
    protected string[] _orderByDescending;
    protected string[] _orderBy;

    protected FinalQuery()
    {
    }

    public override string ToString()
    {
        var sb = new StringBuilder("SELECT ");
        AppendFields(sb, _selectFields);
        sb.AppendLine();

        sb.Append("FROM ");
        sb.Append("[").Append(_table).AppendLine("]");

        if (_where != null) {
            sb.Append("WHERE").AppendLine(_where);
        }

        if (_groupBy != null) {
            sb.Append("GROUP BY ");
            AppendFields(sb, _groupBy);
            sb.AppendLine();
        }

        if (_having != null) {
            sb.Append("HAVING").AppendLine(_having);
        }

        if (_orderBy != null) {
            sb.Append("ORDER BY ");
            AppendFields(sb, _orderBy);
            sb.AppendLine();
        } else if (_orderByDescending != null) {
            sb.Append("ORDER BY ");
            AppendFields(sb, _orderByDescending);
            sb.Append(" DESC").AppendLine();
        }

        return sb.ToString();
    }

    private static void AppendFields(StringBuilder sb, string[] fields)
    {
        foreach (string field in fields) {
            sb.Append(field).Append(", ");
        }
        sb.Length -= 2;
    }
}

public class GroupedQuery : FinalQuery
{
    protected GroupedQuery()
    {
    }

    public GroupedQuery Having(string condition)
    {
        if (_groupBy == null) {
            throw new InvalidOperationException("HAVING clause without GROUP BY clause");
        }
        if (_having == null) {
            _having = " (" + condition + ")";
        } else {
            _having += " AND (" + condition + ")";
        }
        return this;
    }

    public FinalQuery OrderBy(params string[] fields)
    {
        _orderBy = fields;
        return this;
    }

    public FinalQuery OrderByDescending(params string[] fields)
    {
        _orderByDescending = fields;
        return this;
    }
}

public class Query : GroupedQuery
{
    public Query(string table, params string[] selectFields)
    {
        _table = table;
        _selectFields = selectFields;
    }

    public Query Where(string condition)
    {
        if (_where == null) {
            _where = " (" + condition + ")";
        } else {
            _where += " AND (" + condition + ")";
        }
        return this;
    }

    public GroupedQuery GroupBy(params string[] fields)
    {
        _groupBy = fields;
        return this;
    }
}

Lo llamarías así{[20]]}

string query = new Query("myTable", "name", "SUM(amount) AS total")
    .Where("name LIKE 'A%'")
    .GroupBy("name")
    .Having("COUNT(*) > 2")
    .OrderBy("name")
    .ToString();

Solo puede crear una nueva instancia de Query. Las otras clases tienen un constructor protegido. El punto de la jerarquía es "desactivar" los métodos. Por ejemplo, el método GroupBy devuelve un GroupedQuery que es la clase base de Query y no tiene un método Where (el método where se declara en Query). Por lo tanto, no es posible llamar Where después de GroupBy.

Sin embargo, no es perfecto. Con esta jerarquía de clases puede ocultar sucesivamente miembros, pero no mostrar otros nuevos. Por lo tanto Having lanza una excepción cuando se llama antes de GroupBy.

Tenga en cuenta que es posible llamar a Where varias veces. Esto añade nuevas condiciones con un AND a las condiciones existentes. Esto hace que sea más fácil construir filtros programáticamente a partir de condiciones individuales. Lo mismo es posible con Having.

Los métodos que aceptan listas de campos tienen un parámetro params string[] fields. Le permite pasar nombres de campo individuales o una matriz de cadenas.


Las interfaces Fluent son muy flexibles y no requieren que cree una gran cantidad de sobrecargas de métodos con diferentes combinaciones de parámetros. Mi ejemplo funciona con cadenas, sin embargo, el enfoque se puede extender a otros tipos. También puede declarar métodos predefinidos para casos especiales o métodos que aceptan tipos personalizados. También puede agregar métodos como ExecuteReader o ExceuteScalar<T>. Esto le permitiría definir consultas como esta

var reader = new Query<Employee>(new MonthlyReportFields{ IncludeSalary = true })
    .Where(new CurrentMonthCondition())
    .Where(new DivisionCondition{ DivisionType = DivisionType.Production})
    .OrderBy(new StandardMonthlyReportSorting())
    .ExecuteReader();

Incluso los comandos SQL construidos de esta manera pueden tener parámetros de comando y así evitar problemas de inyección SQL y al mismo tiempo permitir que los comandos sean almacenados en caché por el servidor de base de datos. Esto no es un reemplazo para un O / R-mapper, pero puede ayudar en situaciones en las que crearía los comandos usando concatenación de cadena simple de lo contrario.

 1
Author: Olivier Jacot-Descombes,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-03-04 00:26:06