Tuesday, May 26, 2015

Java e NoSQL: usando o MongoDB com Spring Data e QueryDSL

Interessante como algumas tecnologias conseguem ser úteis e flexíveis. Esse é um dos pontos que eu mais admiro no Spring Data. É possível usar seus recursos, conceito e o modelo de componentes em diferentes mecanismos de persistência. Nesse post eu volto a escrever sobre o Spring Data e o QueryDSL, mas em outro contexto, minha idéia é demonstrar como usar essas tecnologias para persistir dados no MongoDB.

Como referência vou utilizar um projeto que compartilhei no Github. Trata-se de uma aplicacação simples com uma página de listagem e pesquisa com filtros variados. A simulação de um cadastro de Mercadoria, um pojo marcado com @Entity. Anotação do Morphia, tecnologia para mapeamento objeto em documento, fazendo o meio campo entre objetos Java e coleções do MongoDB. Na verdade o QueryDSL utiliza o Morphia para gerar a estrutura de consultas para o MongoDB. O plugin do QueryDSL para gerar a estrutura de consultas, por entidade, utiliza a API do Morphia. Veja as configurações do plugin APT do QueryDSL no pom.xml ou build.gradle.

O papel do QueryDSL nesse projeto é pontual, criar o Predicate com as condições variadas para a consulta das coleções no MongoDB. A consulta varia de acordo com os filtros preenchidos pelo formulário de pesquisa (lista de mercadorias). O código a seguir, da classe MercadoriaQuery, demonstra como construir o Predicate:
  ...
  public static Predicate whereByCriterio(FiltrosPesquisaMercadoria filtros) {
    QMercadoria mercadoria = QMercadoria.mercadoria;
    BooleanBuilder builder = new BooleanBuilder();
    if (!Strings.isNullOrEmpty(filtros.getDescricaoMercadoria())) {
      builder.and(mercadoria.descricao.startsWithIgnoreCase(
        filtros.getDescricaoMercadoria()));
    }
    if (!Strings.isNullOrEmpty(filtros.getNomeMercadoria())) {
      builder.and(mercadoria.nome.startsWithIgnoreCase(
        filtros.getNomeMercadoria()));
    }
    if (filtros.getPrecoDe() != null && filtros.getPrecoDe() > 0) {
      builder.and(mercadoria.preco.goe(filtros.getPrecoDe()));
    }
    if (filtros.getPrecoAte() != null && filtros.getPrecoAte() > 0) {
      builder.and(mercadoria.preco.loe(filtros.getPrecoAte()));
    }
    if (!Strings.isNullOrEmpty(filtros.getCategoria())) {
      builder.and(mercadoria.categoria.descricao.startsWithIgnoreCase(
        filtros.getCategoria()));
    }
    return builder;
  } 
  ...

Além das condições para construir a consulta, é importante notar outra característica sobre front-end: a lista de mercadorias apresenta os registros organizados por página. O Spring Data é o responsável por resolver a paginação da consulta com MongoDB. Na classe CriteriaUtil, definos o método para criar o Pageable, com informações da página, quantidade de registros e campo de ordenação. Além disso, essa classe também define um método para montar uma expressão like p/ o MongoDB via QueryDSL.
public final class CriteriaUtil {

  public static Pageable buildPageRequest(int page, int rows, 
      String sortBy) {
    Sort sort = new Sort(Sort.Direction.ASC, sortBy);
    return new PageRequest(page, rows, sort);
  }

  public static BooleanExpression likeWithLowerCase(
      StringExpression field, String expression) {
    String filterValue = String.format("%s%s",expression.toLowerCase(), "%");
    return field.toLowerCase().like(filterValue);
  }
}

Olhando para o principal componente do Spring Data, temos o repositório MercadoriaRepository. Um contrato que define as operações de persistência sobre a coleção Mercadoria. Parecido com o que acontece com o JPA o repositório extende MongoRepository, interface com as operações de persistência (CRUD) de uma determinada coleção. Já outra extensão, de QueryDSLPredicateExecutor, permite que repositório tenha a capacidade de realizar consultas aplicando Predicates do QueryDSL. Veja o código do repositório:
@Repository
public interface MercadoriaRepository
  extends MongoRepository<Mercadoria, Long>, 
    QueryDslPredicateExecutor<Mercadoria> {

  List<Mercadoria> findByDescricaoLike(String descricao);

  @Query(value="{ 'quantidade': { $gte: ?0 } }")
  List<Mercadoria> findByQuantidadeEqualOrGreather(
    Integer quantidade);

  default List<CategoriaGroup> groupByCategorias(
      MongoTemplate template) {
    Aggregation agg = newAggregation(
      group("categoria.descricao").count().as("qtdMercadorias"),
      project("qtdMercadorias").and("categoria.descricao").previousOperation(),
      sort(Sort.Direction.DESC, "qtdMercadorias"));
    AggregationResults<CategoriaGroup> groupResults = 
      template.aggregate(agg, Mercadoria.class, CategoriaGroup.class);
    return groupResults.getMappedResults();
  }
}

O Spring Data constrói o proxy que implementa o contrato repostiório, de forma transparente para o restante da aplicação.

Além dos métodos padrões, das duas interfaces citadas, defino três consultas customizadas:

  • findByDescricaoLike(String): o Spring Data gera o código da consulta, respeitando o nome do método. Nesse caso usando como filtro o campo descricao e operador de consulta like, aplicando o argumento informado. Esse um recurso do Spring Data, independente do MongoDB, funciona tanto em banco de dados NoSQL quanto em relacionais.
  • findByQuantidadeEqualOrGreather(Integer): nesse caso o Spring Data implementa o método usando a consulta definida via anotação @Query.
  • groupByCategorias: método utiliza API do Spring Data para gerar consultas com funções agregadoras do MongoDB (link). Um contador simples de Mercadorias, agrupados por Categoria, um campo (embedded) do documento.


Na controller MercadoriaController, defino diversos métodos que operam sobre o repositório. O destaque fica para o método list, que é acionado para consultar as Mercadorias de acordo com os filtros do formulário de pesquisa.
  ...
  @RequestMapping(method = RequestMethod.GET)
  public PesquisaMercadorias list(FiltrosPesquisaMercadoria filtros) {
    Pageable page = buildPageRequest(filtros.getPagina(), 
      filtros.getLinhas(), filtros.getOrdem());

    Predicate predicate = whereByCriterio(filtros);
    long total = repository.count(predicate);
    List<Mercadoria> mercadorias = 
      Lists.newArrayList(repository.findAll(predicate, page));
    return new PesquisaMercadorias(total, mercadorias);
  }
  ...

No startup da aplicação realizo o setup dos dados, acesse e analise o código da classe Application.

Outros detalhes sobre o projeto:

  • O build e as dependências do projeto são controladas pelo Maven e/ou Gradle;
  • Spring MVC atua como framework web;
  • JQuery e Foundation no front-end da aplicação;


www.yaw.com.br

No comments: