QueryOver Specification: reusando critérios

É muito comum termos que duplicar partes dos critérios feitos em consultas a banco de dados para atender regras de negócio. O Specification pattern tenta resolver esse problema agrupando esses critérios em classes, e usa uma composição de specifications para unir todos os critérios. QueryOverSpecification é uma pequena biblioteca implementando uma variação do padrão para uso com NHibernate QueryOver.




Specification Pattern

Basicamente o padrão é uma interface com um método que diz se a entidade satisfaz ou não um critério, ou seja, um predicado sobre um objeto.

public interface ISpecification
{
    bool IsSatisfiedBy(object candidate);
}
 
public class CourseWithVotes : ISpecification
{
    public bool IsSatisfiedBy(Course course)
    {
        return course.Votes.Count > 0;
    }
}


O problema com essa abordagem é que a implementação pode causar problemas de performance se usado em um base de dados com uma grande quantidade de registros, já que para cada registro, deverá ser feita a chamada do método para verificação se o objeto satisfaz ou não a Specification.

Uma alternativa para .NET é o uso do LINQ, mas para projetos que preferivelmente seja melhor o uso do QueryOver, a biblioteca QueryOverSpecification dá o suporte necessário para o reuso das consultas.

Domain

Fazendo esse mesmo exemplo usando QueryOverSpecification, teríamos:

    public interface ICourseWithVotes
    {
        ISpecification<CourseBy();
    }
 
    public class CourseWithVotes : ICourseWithVotes
    {
        public ISpecification<CourseBy()
        {
           Action<IQueryOver<CourseCourse>> action = x => x.JoinQueryOver(y => y.Votes);
           return new QueryOverSpecification<Course>(action);
        }
    }

Veja que mudamos a forma como a Specification é usada. Criamos um delegate action sobre uma instância de QueryOver de Course, ou seja, o que eu devo adicionar ao QueryOver para efetuar o critério sobre o Course. Passamos essa action para a classe QueryOverSpecification, que cria uma specification usando um QueryOver.

Repository

Abaixo segue um exemplo de um método Find() genérico da classe Repository, que recebe uma ISpecification e retorna a lista de objetos que satisfazem ao critério:

        public IList<TEntityFind<TEntity>(ISpecification<TEntity> spec) 
            where TEntity : classIIdAccessor
        {
            var session = Factory.OpenSession();
            var query = session.QueryOver<TEntity>();
 
            var visitor = new QueryOverSpecificationVisitor<TEntity>(query);
            spec.Accept(visitor);
 
            var list = query.List();
 
            session.Close();
            return list;
        }

*Esse método é somente um exemplo com nenhum tratamento de exception ou transaction.

QueryOverSpecificationVisitor é uma classe que recebe um QueryOver e adiciona todos os critérios necessários para efetuar a consulta, depois é só executar o método List() do QueryOver para ter a lista de objetos.

Service

Dentro de uma classe Service, teríamos a chamada do Repository:

var list = _repository.Find(courseWithVotes.By());

Operadores

Criando outras Specifications, podemos uni-lás usando os operadores And e Or, e até passar parametros para o método By():

var list = _repository.Find(
                    courseWithVotes.By()
                    .And(courseByExample.By(new Course() { Name = "C#" })));

IIdAccessor

Uma restrição da biblioteca que é todas as suas entidades devem implementar a interface IIdAccessor. Ela descreve um Id único para aquela entidade.

namespace Naskar.QueryOverSpec
{
    public interface IIdAccessor
    {
        longId { getset; }
    }
}

Para maioria dos projetos, uma simples classe base abstrata para suas entidades já resolve essa restrição:

    public abstract class Entity : IIdAccessor
    {
        public virtual longId { getset; }
    }

    public class Course : Entity
    {
        public Course()
        {
            this.Votes = new List<Vote>();
        }
 
        public virtual string Name { getset; }
 
        public virtual Instructor Instructor { getset; }
 
        public virtual IList<VoteVotes { getset; }
    }

Atualmente, temos quatro tipos de formas de criar Specifications e três operadores para uní-las:


- QueryOverSpecification: usa um QueryOver para fazer os critérios e é preferível por padrão, como mostrado acima.

- LambdaSpecification: usa uma expressão para adicionar o critério.

new LambdaSpecification<Course>(x => x.Name.IsInsensitiveLike("ava"MatchMode.Anywhere))


- CriterionSpecification: usa a API Criterion do NHibernate para adicionar os criterios.

new CriterionSpecification<Instructor>(Property.ForName("Name").Like("ciclano"MatchMode.Anywhere))


- NoRestrictionsSpecification: não adiciona nenhum critério. Útil para criação de Specifications condicionais.

Operador With

With é um operador adicional que inclui um critério a uma propriedade de uma entidade sem a necessidade de fazer um Join usando o QueryOver.

Exemplo:
    var list = _repository.Find(
        courseWithVotes.By().With(x => x.Instructor, instructorByExample.By(new Instructor() { Name = "fulano" })))

Liste todos os 'cursos com votos' com o 'instrutor que tem o nome parecido com fulano'.

*Atualmente, o operador With não tem suporte a propriedades IEnumerable.

Conclusão

QueryOverSpecification é uma simples biblioteca para construir classes com critérios para consultas em base de dados usando NHibernate de forma a aumentar o reuso e diminuir a complexidade das consultas sem problemas de performance.

As interfaces criadas como ICourseWithVotes, são conceitos e regras vindos dos requisitos e dos casos de uso e que podem conviver sem dependências com frameworks dentro da camada de modelo (domain), deixando a implementação das specifications separada do modelo, preferivelmente em outro namespace e/ou assembly.



Referências

Source no codeplex: http://queryoverspec.codeplex.com/SourceControl/BrowseLatest
Nuget: http://nuget.org/packages/Naskar.QueryOverSpec