Usar consultas de critério em um aplicativo de dados Spring

Usar consultas de critério em um aplicativo de dados Spring

1. Introdução

O Spring Data JPA fornece muitas maneiras de lidar com entidades, incluindoquery methodseJPQL queries customizado. No entanto, às vezes precisamos de uma abordagem mais programática: por exemploCriteria API ouQueryDSL.

Criteria API offers a programmatic way to create typed queries, o que nos ajuda a evitar erros de sintaxe. Ainda mais, quando o usamos com a API Metamodel, ele faz verificações em tempo de compilação se usamos os nomes e tipos de campos corretos.

No entanto, ele tem suas desvantagens: temos que escrever uma lógica detalhada inchada com o código padrão.

Neste tutorial, veremos como podemos implementar nossa lógica DAO personalizada usando consultas de critérios e como o Spring ajuda a reduzir o código clichê.

2. Aplicação de amostra

Para fins de simplicidade, nos exemplos, implementaremos a mesma consulta de várias maneiras: localizando livros pelo nome do autor e o título contendoString.

A entidadeBook para isso se parece com isto:

@Entity
class Book {

    @Id
    Long id;
    String title;
    String author;

    // getters and setters

}

Porque queremos manter as coisas simples, não usamos Metamodel API neste tutorial.

3. Classe@Repository

Como sabemos, no modelo de componente Springwe should place our data access logic in @Repository beans. Obviamente, essa lógica pode usar qualquer implementação, por exemplo, API de critérios.

Para fazer isso, precisamos apenas de uma instânciaEntityManager, que podemos autowire:

@Repository
class BookDao {

    EntityManager em;

    // constructor

    List findBooksByAuthorNameAndTitle(String authorName, String title) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery cq = cb.createQuery(Book.class);

        Root book = cq.from(Book.class);
        Predicate authorNamePredicate = cb.equal(book.get("author"), authorName);
        Predicate titlePredicate = cb.like(book.get("title"), "%" + title + "%");
        cq.where(authorNamePredicate, titlePredicate);

        TypedQuery query = em.createQuery(cq);
        return query.getResultList();
    }

}

O código acima segue um fluxo de trabalho da API de critérios padrão:

  • Primeiro, obtemos uma referênciaCriteriaBuilder, que podemos usar para criar diferentes partes da consulta

  • UsandoCriteriaBuilder, criamos umCriteriaQuery<Book>, que descreve o que queremos fazer na consulta. Além disso, declara o tipo de uma linha no resultado

  • ComCriteriaQuery<Book>, declaramos o ponto de partida da consulta (entidadeBook) e o armazenamos na variávelbook para uso posterior

  • Em seguida, comCriteriaBuilder, criamos predicados contra nossa entidadeBook. Observe que esses predicados ainda não têm efeito

  • Aplicamos ambos os predicados ao nossoCriteriaQuery.CriteriaQuery.where(Predicate…) combina seus argumentos em umand lógico. Este é o ponto em que vinculamos esses predicados à consulta

  • Depois disso, criamos uma instânciaTypedQuery<Book> de nossoCriteriaQuery

  • Finalmente, retornamos todas as entidadesBook correspondentes

Observe que, como marcamos a classe DAO com@Repository,Spring enables exception translation para esta classe.

4. Estendendo o repositório com métodos personalizados

Terautomatic custom queries é um recurso poderoso do Spring Data. No entanto, às vezes precisamos de uma lógica mais sofisticada, que não podemos criar com métodos de consulta automática.

Podemos implementar essas consultas em classes DAO separadas (como na seção anterior).

Além disso,if we want a @Repository interface to have a method with a custom implementation, we can use composable repositories.

A interface personalizada é assim:

interface BookRepositoryCustom {
    List findBooksByAuthorNameAndTitle(String authorName, String title);
}

E a interface@Repository:

interface BookRepository extends JpaRepository, BookRepositoryCustom {}

Além disso, temos que modificar nossa classe DAO anterior para implementarBookRepositoryCustome renomeá-la paraBookRepositoryImpl:

@Repository
class BookRepositoryImpl implements BookRepositoryCustom {

    EntityManager em;

    // constructor

    @Override
    List findBooksByAuthorNameAndTitle(String authorName, String title) {
        // implementation
    }

}

Quando declaramosBookRepository como uma dependência, Spring encontraBookRepositoryImpl e o usa quando chamamos os métodos personalizados.

Digamos que queremos selecionar quais predicados usar em nossa consulta. Por exemplo, quando não queremos encontrar os livros por autor e título, precisamos apenas que o autor corresponda.

Existem várias maneiras de fazer isso, por exemplo, aplicando um predicado apenas se o argumento passado não fornull:

@Override
List findBooksByAuthorNameAndTitle(String authorName, String title) {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery cq = cb.createQuery(Book.class);

    Root book = cq.from(Book.class);
    List predicates = new ArrayList<>();

    if (authorName != null) {
        predicates.add(cb.equal(book.get("author"), authorName));
    }
    if (title != null) {
        predicates.add(cb.like(book.get("title"), "%" + title + "%"));
    }
    cq.where(predicates.toArray(new Predicate[0]));

    return em.createQuery(cq).getResultList();
}

No entanto, essa abordagemmakes the code hard to maintain, especialmente se tivermos muitos predicados e quisermos torná-los opcionais.

Seria uma solução prática externalizar esses predicados. Com as especificações da JPA, podemos fazer exatamente isso; e ainda mais.

5. Usando especificações JPA

Spring Data introduziu a interfaceorg.springframework.data.jpa.domain.Specification para encapsular um único predicado:

interface Specification {
    Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder cb);
}

Podemos fornecer métodos para criar instânciasSpecification:

static Specification hasAuthor(String author) {
    return (book, cq, cb) -> cb.equal(book.get("author"), author);
}

static Specification titleContains(String title) {
    return (book, cq, cb) -> cb.like(book.get("title"), "%" + title + "%");
}

Para usá-los, precisamos que nosso repositório estendaorg.springframework.data.jpa.repository.JpaSpecificationExecutor<T>:

interface BookRepository extends JpaRepository, JpaSpecificationExecutor {}

Esta interfacedeclares handy methods to work with specifications. Por exemplo, agora podemos encontrar todas as instânciasBook com o autor especificado com este one-liner:

bookRepository.findAll(hasAuthor(author));

Infelizmente, não recebemos nenhum método para o qual podemos passar vários argumentosSpecification. Em vez disso, obtemos métodos utilitários na interfaceorg.springframework.data.jpa.domain.Specification.

Por exemplo, combinando duas instânciasSpecification com lógico e:

bookRepository.findAll(where(hasAuthor(author)).and(titleContains(title)));

No exemplo acima,where() é um método estático da classeSpecification.

Desta forma, podemos tornar nossas consultas modulares. Além disso, não tivemos que escrever o boilerplate da Criteria API: o Spring o forneceu para nós.

Observe que isso não significa que não teremos mais que escrever padrões de critérios; esta abordagem só é capaz de lidar com o fluxo de trabalho que vimos: selecionar entidades que satisfaçam as condições fornecidas.

Uma consulta pode ter muitas estruturas sem suporte, por exemplo, agrupamento, retorno de uma classe diferente da qual estamos selecionando ou subconsultas.

6. Conclusão

Neste tutorial, vimos três maneiras de usar consultas de critério em nosso aplicativo Spring:

  • criar uma classe DAO é a maneira mais direta e flexível

  • estendendo uma interface@Repository para integração perfeita com consultas automáticas

  • usando predicados em instânciasSpecification para tornar os casos simples mais limpos e menos prolixos

Como de costume, os exemplos estão disponíveisover on GitHub.