Trabalhar com Cache pode ser uma dor de cabeça até certo ponto, quando você não entende exatamente o que está sendo feito. Muita das vezes, para tentar aumentar a produtividade de um projeto e entregá-lo mais rápido, você acaba apenas copiando configurações de cache da internet, mesmo sem entender o que elas fazem e por onde passam.

O problema de trabalhar de forma mecânica, sem entender o que está sendo feito é quando ocorrem problemas e você não sabe nem por onde começar a análise.

Este artigo tratará de um conceito muito discutido, mas que também traz diversas dúvidas e acaba tornando-se complexo quando não entendido de forma correta, o Hibernate Cache. Estudaremos aqui as três formas de cache que o Hibernate utiliza: Cache do Nível L1, QueryCache e Cache do Nível L2.

O que é Cache e porque utilizar

Antes de iniciar explicando conceitos mais técnicos sobre configurações e comportamentos de Cache no Hibernate, é essencial que você entenda porque está fazendo isso e não simplesmente porque fica mais “bonito” ou porque todo mundo usa.

Sendo mais generalista, o conceito de Cache, na computação como um todo, veio para aumentar a performance de tarefas, diminuindo o acesso ao I/O (Dispositivos de Entrada e Saída, como o HD). O Cache de dados em memória (memória cache), evita que o processador tenha que acessar o HD (Hard Disk) toda vez que precisa de alguma informação para realizar cálculos na ULA (Unidade Lógica e Aritmética), por exemplo. Depois de um certo tempo esses dados são removidos da cache para que novos entrem (pois a cache não é infinita, como já imaginávamos). É por isso que ao acessar pela primeira vez um programa no seu sistema operacional ele demora muito mais do que na segunda vez.

O conceito de Cache no Hibernate não foge a regra. O Hibernate realiza Cache dos dados vindos do banco de dados para evitar acessos desnecessários ao banco, pois acessar ao banco é muito mais custoso do que acessar a memória, pois o acesso ao banco depende de diversos fatores tais como: disponibilidade da rede, velocidade de tráfego da rede, mecanismos de busca otimizados no banco de dados e etc. Tudo isso pode fazer com que uma simples consulta torne-se uma grande dor de cabeça.

A função do Cache no Hibernate é em primeira instância acessar a base recuperando os dados solicitados e gravando em Cache. Já na segunda vez esse acesso será direto em Cache, o que aumentará muito a performance da aplicação.

Hibernate Cache

Iremos agora explicar o funcionamento de cada uma das caches a partir desta seção. É importante saber que para ter um melhor entendimento do funcionamento da Cache no Hibernate é necessário entender o funcionamento do ciclo de vida de um Session, ou seja, como ele é criado, até quando ele permanece “vivo”, o que tem dentro dele e etc. Isso foge do escopo deste artigo.

Cache L1 ou First Level Cache

O Cache L1 está ligado diretamente ao Session do Hibernate, ou seja, o Cache L1 funciona durante todo o ciclo de vida do objeto Session, consequentemente “nasce” e “morre” com ele. Pelo fato do Cache L1 ser interno a um objeto Session, ele não pode ser acessado de outros Sessions criados pelo Session Factory.

A Figura 1 explica o fluxo de funcionamento deste.

Figura 1. Hibernate Cache L1

Temos aqui quatro objetos na Figura 1:

  1. Database: Representa a figura do banco de dados.
  2. First-Level Cache: Represente o Cache L1 do Hibernate.
  3. Session Object: Representa a Sessão atual do Hibernate.
  4. Client: Representa o Cliente que solicitará as requisições.

Perceba que o Cliente não se comunica com o Cache L1, ele apenas conhece a existência do Session e o mesmo comunica-se com o Cache L1. Quando o cliente solicita um objeto do banco de dados (através do Session), sempre este primeiro consulta o Cache L1 para verificar se este objeto já foi carregado, evitando assim o acesso ao banco, caso contrário, ele irá buscá-lo no banco e depois armazenar no Cache L1, assim na segunda vez que tentarmos retorná-lo, não será mais feito acesso ao banco.

Vamos ver um exemplo disso através de um trecho de código presente na Listagem 1.

Listagem 1. Exemplo Cache L1 em funcionamento

    //(1)Abre uma Session do Hibernate
      Session session = HibernateUtil.getSessionFactory().openSession();
      session.beginTransaction();
       
      //(2)busca o departamento pelo primeira vez
      DepartmentEntity department = (DepartmentEntity) session.load(DepartmentEntity.class, new Integer(1));
      System.out.println(department.getName());
       
      //(3)busca o mesmo departamento novamente
      department = (DepartmentEntity) session.load(DepartmentEntity.class, new Integer(1));
      System.out.println(department.getName());
       
      session.getTransaction().commit();
      HibernateUtil.shutdown();
       
      Saida:
       
      Hibernate: select department0_.ID as ID0_0_, department0_.NAME as NAME0_0_ from DEPARTMENT department0_ where department0_.ID=?
      Human Resource
      Human Resource

Temos acima três pontos importantes que explicam com mais detalhes o fluxo da Figura 1.

  1. No primeiro ponto é aberto um Session e o Cache L1 também já é ativado automaticamente para esse novo Session.
  2. No segundo ponto é feita a primeira busca do Departamento no banco de dados e como este objeto ainda não foi armazenado no Cache, uma consulta é disparada no banco de dados. Você pode ver isso através do log gerado pela Hibernate:
    select department0_.ID as ID0_0_, department0_.NAME as NAME0_0_ from DEPARTMENT department0_ where department0_.ID=?.
  3. No terceiro e último ponto tentamos buscar novamente o mesmo Departamento no banco de dados, então o Session busca ele no Cache L1 e como o mesmo já foi armazenado lá, ele apenas é recuperado sem disparar uma nova consulta ao banco.

Lembre-se que o Cache L1 não é compartilhado em Sessions distintas, ou seja, o mesmo objeto Departamento para um SessionX é diferente para um SessionY. Vamos ver um exemplo na Listagem 2.

Listagem 2. Exemplo de Cache L1 Com 2 Sessions

  //(1) Abre uma Session
         Session session1 = HibernateUtil.getSessionFactory().openSession();
         session.beginTransaction();
         
         //(1) Abre outra Session, diferente da primeira
         Session session2 = HibernateUtil.getSessionFactory().openSession();
         sessionTemp.beginTransaction();
         try
         {
             //(2) Busca o departamento pela primeira vez na SESSION 1
             DepartmentEntity department = (DepartmentEntity) session1.load(DepartmentEntity.class, new Integer(1));
             System.out.println(department.getName());
              
             //(3) Busca o departamento novamente na SESSION 1
             department = (DepartmentEntity) session1.load(DepartmentEntity.class, new Integer(1));
             System.out.println(department.getName());
              
             //(2) Busca o departamento pelo primeira vez na SESSION 2
             department = (DepartmentEntity) session2.load(DepartmentEntity.class, new Integer(1));
             System.out.println(department.getName());
         }
         finally
         {
             session1.getTransaction().commit();
             HibernateUtil.shutdown();
              
             session2.getTransaction().commit();
             HibernateUtil.shutdown();
         }
         
         Saida:
         
         Hibernate: select department0_.ID as ID0_0_, department0_.NAME as NAME0_0_ from DEPARTMENT department0_ where department0_.ID=?
         Human Resource
         Human Resource
         
         Hibernate: select department0_.ID as ID0_0_, department0_.NAME as NAME0_0_ from DEPARTMENT department0_ where department0_.ID=?
         Human Resource

O código acima demonstra que ao tentarmos buscar o mesmo objeto em um Session diferente, o acesso ao banco é feito, pois ele não existe no Cache L1 desta Session.

Mas e se você quiser forçar que um objeto seja buscado diretamente no banco de dados, mesmo que ele esteja em Cache, como fazer? A resposta é rápida: removendo ele do Cache L1, assim você forçará o Session a consultar diretamente o banco de dados.

Existem dois métodos disponíveis no objeto Session que podem fazer essa tarefa: evict() e clear().

O evict remove apenas um objeto da Cache L1, ou seja, imagine que você tem dentro do Cache L1 cinco objetos de diferentes tipos, mas gostaria que apenas o objeto do tipo Departamento fosse consultado diretamente no banco, então você faria um “evict” deste objeto, o que ia limpá-lo da Cache L1 e depois faria um “load” novamente.

O clear remove todos os objetos presentes no Cache L1, ou seja, se você tiver cinco objetos, os cinco serão removidos, forçando a busca direta no banco na próxima vez que um load for realizado. Vamos ver um exemplo disso para ficar mais claro na Listagem 3.

Listagem 3. Usando evict() e clear()

  Session session = HibernateUtil.getSessionFactory().openSession();
         session.beginTransaction();
         try
         {
             //busca o departamento pela primeira vez
             DepartmentEntity department = (DepartmentEntity) session.load(DepartmentEntity.class, new Integer(1));
             System.out.println(department.getName());
              
             //busca o departamento pela segunda vez
             department = (DepartmentEntity) session.load(DepartmentEntity.class, new Integer(1));
             System.out.println(department.getName());
              
             //limpa o departamento do cache l1
             session.evict(department);
             //session.clear();
              
             //busca o departamento pela terceira vez, buscará o mesmo no banco de dados
             //pois foi removido da CACHE L1
             department = (DepartmentEntity) session.load(DepartmentEntity.class, new Integer(1));
             System.out.println(department.getName());
         }
         finally
         {
             session.getTransaction().commit();
             HibernateUtil.shutdown();
         }
                  
         Saida:
                  
         Hibernate: select department0_.ID as ID0_0_, department0_.NAME as NAME0_0_ from DEPARTMENT department0_ where department0_.ID=?
         Human Resource
         Human Resource
         
         Hibernate: select department0_.ID as ID0_0_, department0_.NAME as NAME0_0_ from DEPARTMENT department0_ where department0_.ID=?
         Human Resource

Vamos ver agora alguns pontos importantes que devem ser fixados sobre o Cache L1 antes de passamos para o próximo tipo de Cache.

  1. O Cache L1 é habilitado por padrão no Hibernate e você não pode desabilitá-lo.
  2. O objeto Session sempre consulta o Cache L1 antes de ir ao banco de dados.
  3. O Cache L1 está associado somente a um objeto Session, outros objetos não podem “enxergá-lo”.
  4. Quando um load é executado pela primeira vez, a consulta é direta no banco, pois o objeto não existe no Cache.

Query Cache

Agora veremos uma “extensão” da Cache L1, o Query Cache. Perceba que todos os objetos da L1 foram carregados através do método “load()”, disponível no objeto Session, isso porque ele faz a consulta ao Cache L1 antes de ir no banco de dados.

Mas o “load()” só nos é útil quando temos a chave/id do objeto em questão, caso contrário teríamos que executar uma query através do comando “session.createQuery()” e você terá uma grande surpresa: O Cache L1 não funcionará para este caso.

Ao realizar o “createQuery()”, o mesmo vai direto no banco sem importa-se com o Cache L1, trazendo um grande problema: O Cache L1 torna-se inútil. Para contornar esse problema é que surgiu o Query Cache.

O Query Cache armazena no Cache L1 os ID's dos resultados retornados, ou seja, é como se você tivesse executando diversos “load()” em vários objetos. Vamos ver um exemplo na Listagem 4.

Listagem 4. Usando Query Cache

  Session session = getSessionFactory().openSession();
          Transaction tx = session.beginTransaction();
          Query query = session.createQuery("from Person p where p.id=1");
          query.setCacheable(true);
          Iterator it = query.list().iterator();
          while (it.hasNext ()){
             Person p = (Person) it.next();
             System.out.println(p.getFirstName());
          }
          query = session.createQuery("from Person p where p.id=1");
          query.setCacheable(true);
          it = query.list().iterator();
          while (it.hasNext ()){
             Person p = (Person) it.next();
             System.out.println(p.getFirstName());
          }
          tx.commit();
          session.close();

O ponto de atenção acima é a linha “query.setCacheable(true)” que diz que o resultado desta query deve ficar em cache.

Além de setar a query como “cacheable”, você precisa mudar uma configuração do seu arquivo de configuração do Hibernate, conforme a Listagem 5.

Listagem 5. Habilitando o Query Cache

  <property name="hibernate.cache.use_query_cache">true</property>
  

Com esses dois pontos a sua aplicação está pronta para usar o Query Cache e evitar consultar demasiadas ao banco.

Cache L2 ou Second-Level Cache

Diferente do Cache L1, o Cache L2 está disponível para todos os objetos Sessions criados pelo SessionFactory. Imagine, por exemplo, que você tem uma tabela no banco de dados com as configurações da aplicação que deverão ser mantidas durante todo o ciclo da mesma e nunca serão alteradas. Você pode utilizar o Cache L2 para manter essas configurações disponíveis a todas as outras Sessions, evitando que você precise consultar o banco cada vez que precisar de uma configuração.

O Cache L2 está ligado ao objeto SessionFactory e a todo seu ciclo de vida, ou seja, ele “nasce” e “morre” no mesmo momento deste.

Figura 2. Cache L2

Na Figura 2 percebemos que foi adicionada uma camada a mais de comunicação entre o Cache L1 e o Banco, isso significa que o Cache L1 comunica-se com o Cache L2 e este faz a comunicação com o banco de dados.

Então seguindo o fluxo, ficaria: O Session consulta o Cache L1 que por sua vez consulta o Cache L2 que por sua vez retorna os dados ou vai ao banco realizar a consulta. Quando uma consulta é feita diretamente ao banco de dados, tanto o Cache L1 como o Cache L2 são atualizados.

Quando um objeto não existe no Cache L1 mas existe no Cache L2, este é replicado para o Cache L1, evitando uma consulta ao Cache L2. Então pense um pouco o que aconteceria se um objeto existisse no Cache L1 e no Cache L2 e você fizesse um “session.evict(objeto)”.

A resposta é: O Cache L1 seria limpa, mas o Cache L2 não. Então o mesmo seria novamente replicado do Cache L2 para o L1, evitando a consulta ao banco de dados. Neste caso teríamos que chamar o “evict” da SessionFactory. Vamos ver como na Listagem 6.

Listagem 6. Limpando objeto do Cache L2

  public void evict2ndLevelCache() {
      try {
          Map<String, ClassMetadata> classesMetadata = sessionFactory.getAllClassMetadata();
          for (String entityName : classesMetadata.keySet()) {
              logger.info("Evicting Entity from 2nd level cache: " + entityName);
              sessionFactory.evictEntity(entityName);
          }
      } catch (Exception e) {
          logger.logp(Level.SEVERE, "SessionController", "evict2ndLevelCache", "Error evicting 2nd level hibernate cache entities: ", e);
      }
  }

Limpamos o objeto do Cache L2, mas lembre-se que se ele existir ainda no Cache L1, o Cache L2 jamais será acessado. Sendo assim, se você deseja forçar a consulta ao banco, deverá limpar do Cache L1 e do Cache L2.

Como falamos anteriormente, o Cache L2 está no escopo do SessionFactory, o que faz com o mesmo seja acessado por todos os Sessions criados pelo SessionFactory (normalmente uma aplicação tem apenas uma SessionFactory). Então vejamos na Listagem 7 um exemplo de dois Sessions acessando o mesmo objeto que está no Cache L2.

Listagem 7. Dois Sessions acessando um objeto no Cache L2

  //Objeto é buscado pela primeira vez e carregado no Cache L1 e Cache L2
         DepartmentEntity department = (DepartmentEntity) session.load(DepartmentEntity.class, new Integer(1));
         System.out.println(department.getName());
         
         //Buscamos o objeto novamente, ele é retornado pelo Cache L1 pois está na mesma Session
         department = (DepartmentEntity) session.load(DepartmentEntity.class, new Integer(1));
         System.out.println(department.getName());
         
         //Limpamos o objeto da Cache L1
         session.evict(department);
                      
         //Buscamos o objeto novamente, ele é copiado do Cache L2 para o Cache L1
         department = (DepartmentEntity) session.load(DepartmentEntity.class, new Integer(1));
         System.out.println(department.getName());
         
         //Buscamos o objeto novamente, porém agora em uma nova Session. Ele é copiado do Cache L2 para o Cache L1
         department = (DepartmentEntity) anotherSession.load(DepartmentEntity.class, new Integer(1));
         System.out.println(department.getName());
         
         System.out.println(HibernateUtil.getSessionFactory().getStatistics().getEntityFetchCount());           //Prints 1
         System.out.println(HibernateUtil.getSessionFactory().getStatistics().getSecondLevelCacheHitCount());   //Prints 2
         
         Output: 1 2

Perceba que em nenhum momento limpamos o Cache L2, então neste caso o objeto sempre será retornando do Cache L2, sem consulta ao banco.

O Cache L2 mais utilizado pelo Hibernate é o EhCache, que faz todo o papel descrito acima. Para configurá-lo basta adicionar o código da Listagem 8 no seu arquivo de configuração do Hibernate.

Listagem 8. Habilitando EhCache

  <property name="hibernate.cache.provider_class">org.hibernate.cache.EhCacheProvider</property>
  <property name="hibernate.cache.use_second_level_cache">true</property>

Pronto, nosso EhCache está habilitado. Agora basta criamos um arquivo de configuração para configurarmos os parâmetros deste, o ehcache.xml. Veja a Listagem 9.

Listagem 9. ehcache.xml

  <ehcache>    
  <cache
      name="com.somecompany.someproject.domain.Country"
      maxElementsInMemory="10000"
      eternal="false"
      timeToIdleSeconds="300"
      timeToLiveSeconds="600"
      overflowToDisk="true"
  />
  </ehcache>