Divisions com Hibernate: uso avançado da Criteria API

Por Lucas Cavalcanti em 11/09/08

Existe uma operação, não muito conhecida, mas muitas vezes necessária, em bancos de dados chamada divisão (division). Essa operação representa o seguinte tipo de consulta: Selecione os alunos que fizeram todos os cursos. Selecione os autores em que todos os seus livros têm mais de 200 páginas. E assim por diante.

Esse tipo de consulta precisa de alguns recursos avançados do SQL, então antes de mostrar como implementá-la vamos ver como implementar consultas um pouco mais simples, usando a Subqueries e a DetachedCriteria, que nos possibilitam consultas bastante poderosas usando a api da Criteria.

Bom, vamos começar com três entidades: Aluno, Curso, e um relacionamento de muitos pra muitos entre eles representado pela entidade Matrícula.

Vamos pensar um pouquinho como fazer a seguinte consulta: “Selecionar todos os alunos que estejam cursando Matemática ou Português“. Pensando em banco de dados, podemos fazer um join entre Alunos e Matrículas, e selecionar as linhas em que o curso é matemática ou é português. Precisamos também evitar que a busca retorne alunos repetidos. Vamos fazer isso com Criteria, recebendo a lista dos cursos que eu quero que o aluno esteja cursando algum deles:


public List<Aluno> alunosCursandoAlgumDessesCursos(List<Curso> cursos) {
  Criteria criteria = session.createCriteria(Aluno.class);
  //join com as matrículas
  criteria.createCriteria("matriculas""m");
  
  //usando a disjunction para fazer um ’ou’ entre vários elementos
  Disjunction ou = Restrictions.disjunction();
  for (Curso curso : cursos) {
    ou.add(Restrictions.eq("m.curso", curso);
  }
  criteria.add(ou);

  //eliminando resultados repetidos
  criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
  return criteria.list();
}

Ou podemos fazer algo bem mais interessante, que é usar a restrição in, que retorna verdadeiro se a propriedade dada é igual a algum dos elementos da coleção que passarmos pra ela. Nesse caso trocaríamos o Disjunction por simplesmente:

criteria.add(Restrictions.in("m.curso", cursos));

Bem fácil! Agora vamos mudar só um pouquinho a consulta para: “Selecione todos os alunos que estiverem cursando Português E Matemática“. Poderíamos inocentemente mudar a Disjunction para Conjunction no método anterior. Mas isso não funciona! Por quê? Porque se fizermos isso, estaríamos mudando a consulta para algo do tipo: “Selecione os alunos que tenham uma matrícula que é em Português e em Matemática ao mesmo tempo“. E isso não é possível. Temos que mudar essa consulta para algo do tipo: “Selecione todos os alunos para os quais exista uma matricula no curso Português e exista uma matrícula no curso Matemática“.

Existe uma operação em SQL que faz exatamente isso: o exists. Ela retorna verdadeiro se a subconsulta que estiver depois dela retornar algum resultado. Para fazer isso precisamos então criar subconsultas em Criteria, e o jeito de fazer isso é usando a classe Subqueries, que fabrica Criterions que envolvem a criação de subconsultas.

Para usar qualquer método da Subqueries precisamos de uma DetachedCriteria. Essa DetachedCriteria é um tipo especial de Criteria que não precisa da session do hibernate para ser criada. Dentro dela temos acesso a todos os alias e propriedades da Criteria principal, e o uso é o mesmo que faríamos para Criterias normais.

Já que temos a Subqueries na mão, vamos implementar a consulta, recebendo a lista dos cursos que queremos que o aluno esteja matriculado em todos eles:


public List<Aluno> alunosCursandoTodosEssesCursos(List<Curso> cursos) {
  Criteria criteria = session.createCriteria(Aluno.class, "a");
  Conjunction e = Restrictions.conjunction();
  for (Curso c : cursos) {
    e.add(Subqueries.exists(
      DetachedCriteria.forClass(Matricula.class, "m")
        .setProjection(Projections.id())
        .add(Restrictions.eqProperty("a.id""m.aluno.id"))
        .add(Restrictions.eq("m.curso",c))));
  }
  criteria.add(e);
  return criteria.list();
}

Ou seja, queremos que exista uma matrícula do aluno da Criteria principal para cada curso da lista passada.

Mas vamos pensar no seguinte: Essa lista de cursos provavelmente veio de outra consulta no banco, por que não usar essa consulta, ao invés da lista de cursos?! O jeito de fazer isso é usando o operador division que falamos no começo do post. Ele é meio complicado de implementar, pois você tem que pensar meio ao contrário do normal. Por exemplo, para implementar a consulta “Selecione os alunos que estão matriculados em todos os cursos” precisamos transformá-la para: “Selecione os alunos para os quais não exista nenhum curso para o qual não exista matrícula desse aluno para esse curso“, ou seja: um aluno que não exista nenhum curso em que ele não esteja matriculado. É estranho mas é assim mesmo que é feito. A Subqueries também possui o método notExists, então podemos fazer a seguinte consulta, que traz os alunos que fazem todos os cursos:


public List<Aluno> alunosCursandoTodosOsCursos() {
  Criteria criteria = session.createCriteria(Aluno.class, "a");
  criteria.add(Subqueries.notExists(
      DetachedCriteria.forClass(Curso.class, "c")
        .setProjection(Projections.id())
        .add(Subqueries.notExists(
            DetachedCriteria.forClass(Matricula.class, "m")
              .setProjection(Projections.id())
              .add(Restrictions.eqProperty("m.curso.id""c.id"))
              .add(Restrictions.eqProperty("m.aluno.id""a.id")                
        ))
      ));
  return criteria.list();
}

Não é um bicho de sete cabeças, mas também não é nada trivial. O código fica meio poluído por causa das chamadas estáticas, mas se você fizer o import static dos métodos a coisa melhora um pouquinho.

As restrições que você tinha colocado para buscar a lista de cursos dos métodos anteriores, você pode colocar na DetachedCriteria de Cursos, que vai funcionar do jeito que é esperado. Por exemplo: “Selecione os alunos que estejam matriculados em um curso noturno” vira “Selecione os alunos para os quais não exista algum curso noturno em que ele não esteja matriculado“. Mais ainda: você pode colocar restrições pertinentes na DetachedCriteria da matrícula, que também vai funcionar da forma esperada. Por exemplo: “Selecione os alunos que estejam com a matricula paga em todos os cursos” vira “Selecione os alunos para os quais não existe algum curso em que não exista matrícula paga nesse curso“.

Existem muitos casos em que o operador division salva sua vida então, mesmo que ele seja meio complicadinho, é bom saber que ele existe e ter uma boa referência de como implementá-lo =).

Além da Subqueries, existe outra classe muito útil que fabrica Criterions e Projections relacionados a uma propriedade fixa: a Property. Vale a pena olhar o javadoc do hibernate e ver a quantidade de opções de consultas que temos disponíveis. Existe um bug no hibernate que te obriga a setar uma Projection nas DetachedCriterias quando usadas dentro das Subqueries, se isso não é feito o hibernate nos presenteia com uma NullPointerException.

Os 7 hábitos dos desenvolvedores Hibernate e JPA altamente eficazes

Por Paulo Silveira em 28/01/08

Essa última semana tive a oportunidade de palestrar no RioJUG sobre JPA e Hibernate, onde fui muito bem recebido pelo Guilherme Chapiewski e Magno Cavalcante. Isso ocorreu durante o treinamento de Arquitetura Java que demos para diversos desenvolvedores da Globo.com, e onde tive o prazer de conhecer alguns desenvolvedores e arquitetos, como Vitor Pellegrino, Anselmo Alves, Wesley Silva, Alexandre Gazola, Tiago Motta, entre outros. Também vi o Ettore Luglio e o Daniel Passos.


DSC01736 DSC01709
DSC01691 DSC01750

Infelizmente durante a palestra não tive tempo de mostrar muitos recursos avançados e boas práticas do Hibernate, então vou usar este espaço para tal.

Precisamos conhecer todo pontencial de qualquer ferramenta, framework ou biblioteca que vamos usar em um projeto. Uma ferramenta boa, sem o devido conhecimento, resulta em projetos atrasados, com problemas de performance e desculpas do tipo “O problema é o [Hibernate|Struts|JSP, insira sua tecnologia aqui...], que gera uma quantidade excessiva de [queries|objetos|scriptlets|...] durante [lazy loading|requisições|...]“. Isso vale em especial para ferramentas mais antigas, como JSP e Struts 1. Hoje em dia ambas possuem recursos poderosos que auxiliam em muito o desenvolvimento, mas alguns desenvolvedores acabam não se aprofundando e desconhecem esses detalhes que podem ser vitais no uso de determinadas tecnologias.

Com o Hibernate não é diferente. É muito comum as pessoas culparem o Hibernate pela queda do banco de dados, performance das queries, número de objetos em memória, LazyInitializationException, e outros inúmeros problemas os quais em sua maioria poderiam ter sido evitados com a utilização de alguns recursos, boas práticas e bons hábitos no uso desse framework.

Sem mais demora, os 7 hábitos:

Connection Pool - Usar o pool de conexões embutido com o Hibernate é um erro comum, e a própria documentação diz que você não deve usa-lo em produção! Pode acontecer até connections leak!
A Caelum teve ótimas experiências com o C3P0, e é muito fácil configurá-lo como Provider para o Hibernate.

Second Level Cache - Todos já passamos por situações em que precisamos criar caches para as linhas de banco de dados mais acessadas. Aqui temos diversos problemas: sincronismo, gasto de memória, memory leak, tamanho do cache, política de prioridade da fila (LFU, LRU, FIFO, etc), tempo de expiração e modos de invalidar o cache. Escrever um cache eficiente e seguro é um grande trabalho, imagine ainda dar suporte a um cache distribuído e que possa se aproveitar do disco rígido para não gastar tanta memória? Esse é o papel do second level cache. Você pode usá-lo com diversos providers, sendo o EhCache um dos mais conhecidos.

Query Cache - Um recurso fantástico do Hibernate. No caso de você ter queries que são executadas inúmeras vezes, você pode pedir para o Hibernate fazer o cache do resultado desta query. O interessante é que ele não vai armazenar todos os objetos resultantes, e sim apenas suas primary keys: no momento que ele precisar executar novamente aquela query, ele já tem todos os IDs resultantes, e através destes ele consulta o second level cache, sem fazer um único hit ao banco de dados! Esse cache será invalidado quando alguma das tabelas envolvidas nesta query for atualizada, ou um determinado tempo passar.

Controle do Lazy - Algumas pessoas costumam reclamar do lazy loading, dizendo que em alguns casos teria sido melhor ele carregar tudo em uma única query. Você sempre pode redefinir o comportamento desses relacionamentos quando fizer uma query, através de um eager fetch.

Stateless Session - Algumas vezes precisamos fazer um processamento em batch de objetos, ou mesmo inserir uma quantidade grande deles na base de dados. Em muitos casos uma bulk operation é o suficiente, mas se quisermos manter a Orientação a Objetos, devemos tomar cuidado com a grande quantidade de objetos que ficarão armazenados no first level cache. A StatelessSession resolve esse problema: simplesmente não há first level cache e nenhum objeto se comportará como managed, tendo praticamente o mesmo efeito que chamar entityManager.clear() a cada operação.

Open Session in View - Na arquitetura MVC, muitas vezes renderizamos em nossa view diversas entidades do nosso modelo, e essas podem ter sido carregas pelo Hibernate. Se essas entidades possues relacionamentos lazy, precisamos que a sessão esteja aberta no momento da renderização da View, caso contrário teremos uma LazyInitializaionException ou algum código macarrônico para carregar relacionamentos que nem sempre precisamos. Para isso devemos manter a session aberta através de um filtro, interceptador ou algum outro mecanismo. Isso resulta no pattern Open Session in View e também se aplica ao EntityManager.
O mesmo efeito pode ser obtido através de inversão de controle e injeção de dependências através da anotação @PersistenceContext, que é tratada por containers EJB3 e também por muitos frameworks web, como o Spring. O EJB3 ainda possui o conceito de um contexto de persistência extendido, quem é interessante em casos de conversações longas: o EntityManager usado será o mesmo enquanto aquele stateful session bean não for removido.

Evitando número de queries excessivas (n+1) - Se uma NotaFiscal possui muitos Items, e essa coleção é lazy, gastaremos duas queries para buscar a NotaFiscal e seus respectivos Itens. Mas se temos uma lista de NotaFiscal resultante de uma query, para cada NotaFiscal teremos uma nova query executada para todo getItems invocados. 1 query para listar NotaFiscal, N queries para pegar os relacionamentos: é o problema das n+1 queries. Você deve usar as configurações de batch-size e fetch-size para pedir ao Hibernate carregar as entidades/relacionamentos em blocos em vez de um em um. Você também pode utilizar o second level cache nesses relacionamentos, diminuindo consideravelmente o número de queries disparada.

Essas são apenas alguns dos hábitos, poderíamos ainda falar sobre o bom tratamento de exceções, o cuidado ao fechar todos os recursos abertos pelo Hibernate, o uso de queries nativas, o mapeamento de queries nativas para entidades através do ResultTransformer, filtros de coleções, dynamic insert e update, a criação do seu próprio tipo de persistência, e muitos outros. Conhecer bem o capítulo de performance do Hibernate é fundamental além de um bom começo.

Arquitetura e Design de Projetos Java

Por Paulo Silveira em 01/08/07

Hoje em dia são tantos os design patterns, padrões, frameworks e boas práticas, fica confuso tomar uma decisão. Como devemos desenhar nossas classes? Usar herança ou composição? Injeção de dependências? Webservices, RMI ou um simples arquivo XML? JDBC, JPA ou Hibernate? Devo usar EJB? Quando preciso e como faço um cluster?

Depois de um longo preparo da ementa, exercício e de como abordar tantos tópicos, trazemos a público um novo treinamento, o FJ-91, focado em arquitetura e design de projetos Java. O treinamento passa pelo uso correto da orientação a objetos, design de classes (patterns, domain driven design, componentização), frameworks e especificações, além de Web 2.0 e SOA.

Arquitetura e Desgin de Projetos Java



Os exercícios são apresentados de uma maneira bem diferente: fazemos checkout de inúmeros projetos construídos para este treinamento, com o objetivo de analisar o código, debater, melhorar, e testar os diferentes frameworks e arquiteturas. Desde exercícios simples como trocar herança por composição, até rodar uma aplicação com EJB em cluster. Além de conhecer muitos dos frameworks e novas tecnologias, o intuito é que as pessoas saiam com uma capacitade crítica aguçada para uma tomada de decisão.

Apesar de não ser o foco, cada capítulo apresenta testes simulados para a certificação Sun Certified Enterprise Architect, e também exercícios de modelagem e debates sobre as decisões tomadas, passando pelas três etapas da SCEA. Apesar dessa certificação ser bem antiga e ter um conteúdo ultrapassado em alguns quesitos, os outros tópicos abordados pelo treinamento estão bem mais próximos do Java EE 5.0, o que o torna qualificado para uma possível atualização da prova.

Ao final do treinamento ainda há um capítulo dedicado a tópicos sobre desenvolvimento e metodologias. Alguns papers clássicos são debatidos, como o Silver Bullet e o Mythical Man Month, além dos testes unitários e de aceitação. Ufa! 40 horas de muito trabalho.

JPA: anotações nos getters ou atributos?

Por Paulo Silveira em 25/06/07

A especificação da JPA permite que você utilize as anotações em uma Entity de duas formasou nos atributos, ou nos getters. Também diz que não devemos misturar isso, pois o comportamento resultante não é especificado. Na época em que a JPA estava em draft, havia até um atributo na anotação @Entity em que você explicitava onde iria usar as anotações.

Se temos essas duas opções, qual devemos usar? Usando o hibernate, há uma diferença fundamental entre as duas abordagens: quando e se o objeto será carregado.

Considere a seguinte classe, anotada pelos atributos:

@Entity
public class Funcionario {
  @Id
  @GeneratedValue
  private Long id;

  private String nome;

  // getters e setters
}

E agora um pequeno código para carregar, de maneira lazy, um Funcionario de id 1:

  Funcionario f = manager.getReference(Funcionario.class, 1l);
  System.out.println(f.getId());      
  System.out.println(f.getNome());

Com a propriedade show_sql valendo true, o código ao rodar produz:

--- query select executada ---
1
Paulo Silveira

Se em vez de anotarmos o atributo id com @Id e @GeneratedValue, anotarmos o método getId, obteríamos:

1
--- query select executada ---
Paulo Silveira

Em outras palavras, se no final você só usasse a chave primária (como poderia acontecer se a view fosse uma página web e caísse em uma condição particular), a query nem mesmo teria sido executada! Com a anotação diretamente no atributo, qualquer invocação de método em um objeto carregado mesmo com o getReference vai disparar a query.

É interessante sempre anotar os getters em vez dos atributos, mas vale reparar que na maioria absoluta dos casos isso não vai trazer benefícios. Até mesmo porque, em uma aplicação real, estaremos com o cache de segundo nível habilitado, tornando desnecessárias essas pequenas otimizações.

Hibernate Search com Lucene

Por Nico Steppat em 13/06/07

Há alguns meses tem um novo projeto na página do Hibernate. O Hibernate Search é uma “full text search engine” que tem sua implementação escrita em cima do Apache Lucene.

O Lucene por si só já é bem famoso pelas funcionalidades oferecidas, como buscas aproximadas, índices em memória, e velocidade. A parte mais complicada é a configuração e preparação (indexação) dos dados que são necessários para executar a busca. Aqui entra o Hibernate Search: ele te poupa desse trabalho e integra o Lucene muito bem com o Hibernate, dada a arquitetura de listeners e callbacks deste projeto. Com ele podemos buscar textualmente pelas entidades, usando as funcionalidades do Lucene sem a dor de cabeça da criação de Documents, definição dos tipos de Fields, controle de acesso concorrente aos índices e outros pequenos detalhes que aparecem ao usar o projeto diretamente. Vamos ver um exemplo simples como configurar e usar o Hibernate Search.

Anotações

Tanto nas Hibernate Annotations quanto no Hibernate Search você passa a maioria das configurações usando anotações no seu modelo de domínio. Para poder busca uma entidade você deve anota-lá com @Indexed. Usando esta anotação o Lucene irá analisar os dados da entidade e indexa-los, pois um interceptador do Hibernate fará esse trabalho por de trás dos panos. Quais dados você pretende indexar e como deve ser especificado anotando o atributo com @Field. Se você não usar nenhuma anotação nenhum dado será indexado. Além disso, é preciso anotar a chave primária com @DocumentId, como segue:


@Entity
@Indexed
public class Dvd {

  @Id  @GeneratedValue
  @DocumentId
  private Long id;

  @Field(index=Index.TOKENIZED,store=Store.YES)
  private String title;

  @Field(index=Index.TOKENIZED)
  private String description; 

        // possivelmente alguns getters e setters
}

Index

Usamos no exemplo os atributos index e store na anotação @Field. Quando Lucene indexa um campo, ele salva os dados num formato especial para ele poder executar suas buscas. Por exemplo, se você usa o título “Rambo 2″, o Lucene indexa Rambo e 2 separado. Esse tipo de indexação se chama TOKENIZED. Outros tipos são Index.NO (não será indexado e não tem como buscar) e Index.UN_TOKENIZED (não será “separado”, útil para campos considerado chaves). Essa nomenclatura é bem familiar para quem já usou o Lucene.

Store

Indica que Lucene deve gravar os dados (o título do DVD, no caso) no índice, de tal forma que esse valor possa ser recuperado integralmente diretamente pelos indices. Quando Lucene procura o título, ele pode devolver diretamente o valor, porque usamos Store.YES. É mais rápido, pois o hibernate não precisará consultar o banco para buscar aquele atributo, mas precisa mais espaço no disco e de certo forma é uma redundância de informação.

Eventos e diretórios

Hibernate Search encapsula o trabalho de indexação. Somente precisamos avisar o Hibernate Search em que momento nós desejaremos indexar as entidades em questão. Para isso existem listeners que podem ser configurados no hibernate.cfg.xml:

<session-factory>
    ...
    <event type="post-update">
            <listener class="org.hibernate.search.event.FullTextIndexEventListener"/>
    </event>
    <event type="post-insert">
            <listener class="org.hibernate.search.event.FullTextIndexEventListener"/>
    </event>
        <event type="post-delete">
            <listener class="org.hibernate.search.event.FullTextIndexEventListener"/>
    </event>
    ...
</session-factory>

Definimos 3 listeners globais. Hibernate os ativa automaticamente quando o tipo de evento acontecer. Nesse caso qualquer alteração numa entidade faz com que o Lucene altere o índice.

Além disso temos que definir um provedor que cria os arquivos e diretórios que serão usados para os índices do Lucene:

<session-factory>
   ...
   <property name="hibernate.search.default.directory_provider">
       org.hibernate.search.store.FSDirectoryProvider
   </property>
   <property name="hibernate.search.default.indexBase">
       /home/nico/workspace/mydvds/webapp/indexes
   </property>
   ...
</session-factory>

Existem outros provedores como RAMDirectoryProvider que somente grava na memória.

A busca

Configurado o Hibernate Search, vamos criar a busca. Você também usa um objeto session/query para fazer suas buscas, na verdade a sessão do Hibernate Search encapsula uma do hibernate. Para criar uma sessão usamos o método estático createFullTextSession da classe org.hibernate.search.Search e passamos a sessão do Hibernate como argumento:

FullTextSession LuceneSession = Search.createFullTextSession(hibernateSession);

Com a sessão na mão podemos criar um org.hibernate.search.FullTextQuery do hibernate search:

FullTextQuery fullTextQuery = LuceneSession.createFullTextQuery(LuceneQuery);

FullTextQuery estende Query do hibernate. Podemos invocar fullTextQuery.list() para receber a lista de objetos (neste caso dvds) baseado na query do Lucene.

Para criar essa query, usamos a api do Lucene. Ela oferece uma sintaxe simples e muito poderosa para definir nosso critério de busca. Essa sintaxe é verificada pelo QueryParser do Lucene:

String[] stopWords = new String[]{"de","do","da","dos","das","a","o","na","no","em"};    
QueryParser parser = new QueryParser("title",new StopAnalyzer(stopWords));));

Queremos procurar no campo title e passamos um objeto Analyzer. Ele é responsável por filtrar palavras insignificante na busca que foram definidos no array stopWords. Por padrão o StopAnalyzer somente conhece “stop words” do idioma inglês. É claro que o array deveria ser bem maior. Existem outros analyzers, como o BrazilianAnalyzer, que podem te ajudar bastante na tarefa de separar o que deve e como deve ser indexado.

Com o parser na mão podemos finalmente criar a query:

Query LuceneQuery = parser.parse("Deus");

procura por “deus” ou “Deus” e encontra o título “Cidade de Deus” por exemplo. Uma busca por “de” encontra nada, porque definimos “de” como uma stopword. Você pode fazer buscas muito mais interessantes, como “+cidade -Deus”, que busca por todos os dvds que possuem “cidade” mas não possuem “Deus” como título.

Interessante também é a possibilidade de fazer uma busca aproximada, as tão faladas fuzzy searches. Se você buscar por “cidadi~”, a query também devolverá o mesmo dvd como resultado da query!

O código final ficaria:

  public void search(Dvd dvdthrows ParseException {

    Session session = this.factory.getSession();
    
    FullTextSession LuceneSession = Search.createFullTextSession(session);
    
    String[] stopWords = {"de","do","da","dos","das","a","o","na","no","em"};    
    QueryParser parser = new QueryParser("title",new StopAnalyzer(stopWords));

    Query LuceneQuery = parser.parse(dvd.getTitle());
    FullTextQuery fullTextQuery = LuceneSession.createFullTextQuery(LuceneQuery, Dvd.class);    
  
    this.dvds = fullTextQuery.list();
  }

Repare que mesmo a query sendo do Hibernate Search, ela devolve uma lista de objetos managed de DVDs. Qualquer alteração deles acarretará em uma mudança no banco de dados e reindexação dos campos!

Conclusão

Essa foi simente uma introdução do Hibernate Search e Lucene. Hibernate Search facilita ainda mais a configuração e indexação pelo Lucene: possibilita, por exemplo, usar JMS para guardar os índices de maneira clusterizada, além de indexar os documentos assíncronamente. Também é simples fazer ordenações e projeções, combinações com criteria, mapeamento de relacionamento, boost factors, otimização dos índices, e muitos mais. O Paulo Silveira vai dar uma palestra entitulada “Organizando conteúdos com tags e Lucene” no evento Falando em Java da Caelum, que aborda alguns desses temas.

Obs: O codigo (sem jars) está disponível como download. Alterei a busca AJAX de DVDs da aplicação mydvds do vraptor para Hibernate Search.