Utilizando Hibernate podemos armazenar e recuperar objetos Java através do Mapeamento Objeto-Relacional (Object-Relational Mapping - ORM).
O Hibernate possui diversos utilitários e facilitadores que permitem um melhor aproveitamento da base de dados. Entre as características que o Hibernate possui é o Cache que permite a otimização da performance para as aplicações críticas.
No restante do artigo estaremos estudando mais especificamente o que é cache e os diferentes níveis disponíveis.
Hibernate Cache
Acessar uma base de dados é uma operação muito custosa, mesmo quando for uma simples consulta (query). Uma simples consulta, por exemplo, envolve uma requisição para o servidor, o banco de dados deve compilar essa consulta, rodar e retornar um resultado que por sua vez será retornado ao cliente.
A maioria das bases de dados utilizam caches para os resultados de uma consulta se estas consultas são executadas inúmeras vezes. Com isso eliminamos o I/O do disco e tempo de compilação da consulta. No entanto, isso terá pouco valor caso exista um grande número de clientes fazendo diferentes solicitações. Além disso, mesmo quando temos um cache para os resultados, isso não elimina o tempo que levamos para transmitir a informação na rede, que é considerada a parte mais relevante do atraso.
Algumas aplicações serão capazes de tirar vantagens de aplicações contidas na própria base de dados, mas isso é uma exceção e tais bases de dados possuem suas limitações. Ao invés disso, o mais apropriado é termos uma cache no cliente final que faz a conexão com a base de dados. Isto não é provido ou suportado diretamente pela API JDBC, mas o Hibernate provê uma cache de nível um, também chamado de L1 ou simplesmente de cache, através da qual todas as requisições devem passar. Uma cache de segundo nível é opcional e configurável.
A cache de nível um (L1) garante que dentro de uma session, requisições para um dado objeto da base de dados retornará sempre a mesma instância do objeto, prevenindo assim que um mesmo objeto seja carrega múltiplas vezes pelo Hibernate ou que a informação entre em conflito.
O Hibernate também oferece a possibilidade de itens na cache L1 poderem ser individualmente descartados invocando o método evict() na session para o objeto que desejamos descartar. Para descartar todos os itens na cache L1, invocamos o método clear(). Com isso podemos verificar uma grande vantagem do Hibernate em relação ao tradicional JDBC na qual sem esforço adicional os desenvolvedores ganham o beneficio de uma cache para a base de dados no lado cliente.
A Figura 1 mostra as duas caches disponíveis para a session: cache L1 onde todas as requisições devem passar, e a cache L2 opcional. A cache L1 sempre será consultada antes que qualquer tentativa seja feita para localizar um objeto na cache L2. Nesta figura também verificamos que a cache L2 é externa ao Hibernate.
Figura 1. Interação entre as Caches L1 e L2 e a Session.
Embora essa cache L2 seja acessada via session de forma transparente ao usuário do Hibernate, esta é uma interface plugável para qualquer uma das várias caches que são mantidas na mesma JVM ou numa JVM externa. Isto permite que uma cache seja compartilhada entre aplicações na mesma máquina ou mesmo entre múltiplas aplicações em múltiplas máquinas.
Caches L2 e tipo de acesso
Qualquer cache de terceiros podem ser utilizadas com o Hibernate. Uma interface org.hibernate.cache.CacheProvider é disponibilizada e deve ser implementada. O provedor da cache é então especificada através do nome da classe de implementação como valor da propriedade hibernate.cache.provider_class.
Na prática existem quatro caches excelentes que já são suportadas pelo Hibernate e serão adequadas para a maioria dos desenvolvedores, são elas: EHCache, Infinispan, OSCache e SwarmCache.
O tipo de acesso para a cache L2 pode ser configurada selecionando a opção CacheMode e aplicando o método setCacheMode(). Na Tabela 1 temos as opções disponíveis.
Modo |
Descrição |
NORMAL |
Informação é lida e escrita para a cache conforme necessário. |
GET |
Informação nunca é adicionada para a cache. |
PUT |
Informação nunca é lida da cache, mas entradas na cache sempre serão atualizadas, assim como elas serão lidas da base de dados pela session. |
REFRESH |
É o mesmo que PUT, mas a opção de configuração use_minimal_puts será ignorada se ela estiver setada. |
IGNORE |
Informação nunca é lida ou escrita na cache. |
Tabela 1. Opções para o CacheMode disponibilizadas no Hibernate.
O CacheMode não afeta a forma de acesso à cache L1. Apesar da cache L2 reduzir o acesso à base de dados, vale salientar que os seus benefícios dependem do tipo de cache e como ela será acessada.
Uma estratégia é responsável por armazenar e recuperar informações na cache. Se estivermos utilizando uma cache de segundo nível, devemos decidir para cada classe persistente e coleção, qual cache devemos utilizar. Entre essas estratégias temos: Transactional que é utilizada apenas para ler informações, Read-write que é utilizada também para apenas leitura de informações, Nonstrict-read-write que não oferece garantias de consistência entre a cache e a base de dados, e por fim temos a estratégia Read-only que é uma estratégia concorrente principalmente utilizada para informações que nunca mudam.
Na Listagem 1 temos um exemplo de como utilizar uma cache L2 para uma classe Empregado. A estratégia utilizada é read-write.
Listagem 1. Utilizando uma cache L2 para uma classe Empregado.
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="Employee" table="EMPREGADO">
<meta attribute="class-description">
Esta classe contem detalhes do Empregado.
</meta>
<cache usage="read-write"/>
<id name="id" type="int" column="id">
<generator class="native"/>
</id>
<property name="primeiroNome"
column="primeiro_nome" type="string"/>
<property name="ultimoNome" column="ultimo_nome"
type="string"/>
<property name="salario" column="salary"
type="int"/>
</class>
</hibernate-mapping>
Outra situação importante é verificarmos se o provedor do cache é compatível com todas as estratégias de concorrência. A seguinte matriz de compatibilidade irá ajuda a escolhermos a melhor combinação (Tabela 2).
Provedor/Estratégia |
Read-only |
Nonstrictread-write |
Read-write |
Transactional |
EHCache |
Sim |
Sim |
Sim |
Não |
OSCache |
Sim |
Sim |
Sim |
Não |
SwarmCache |
Sim |
Sim |
Não |
Não |
JBoss Cache |
Sim |
Não |
Não |
Sim |
Tabela 2. Estratégias suportadas pelos provedores de persistência.
Na Listagem 2 temos o arquivo de configuração hibernate.cfg.xml com o provedor de persistência EHCache configurado como o segundo nível de cache.
Listagem 2. Arquivo de configuração hibernate.cfg.xml com EHCache configurado.
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-configuration SYSTEM
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<property name="hibernate.dialect">
org.hibernate.dialect.MySQLDialect
</property>
<property name="hibernate.connection.driver_class">
com.mysql.jdbc.Driver
</property>
<property name="hibernate.connection.url">
jdbc:mysql://localhost/test
</property>
<property name="hibernate.connection.username">
root
</property>
<property name="hibernate.connection.password">
root123
</property>
<property name="hibernate.cache.provider_class">
org.hibernate.cache.EhCacheProvider
</property>
<!-- Arquivos de mapeamento XML -->
<mapping resource="Empregado.hbm.xml"/>
</session-factory>
</hibernate-configuration>
Por fim, configuramos na Listagem 3 o arquivo ehcache.xml com suas propriedades para a classe Empregado. Não podemos esquecer-nos de configurar o arquivo ehcache.xml no CLASSPATH da aplicação.
Listagem 3. Configuração do arquivo ehcache.xml para a classe Empregado.
<diskStore path="java.io.tmpdir"/>
<defaultCache
maxElementsInMemory="1000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
/>
<cache name="Empregado"
maxElementsInMemory="500"
eternal="true"
timeToIdleSeconds="0"
timeToLiveSeconds="0"
overflowToDisk="false"
/>
Por fim, devemos ativar a cache de consulta simplesmente ativando a propriedade hibernate.cache.use_query_cache="true" no arquivo de configuração. Após isso devemos setar para true o método setCacheable(Boolean). Veja a Listagem 4.
Listagem 4. Setando o método setCacheable.
Session session = SessionFactory.openSession();
Query query = session.createQuery("FROM Empregado");
query.setCacheable(true);
List users = query.list();
SessionFactory.closeSession();
Neste artigo vimos o que são caches e quais são os tipos de cache do Hibernate e como configura-las. Uma analise importante a ser feita é que na prática é sempre muito importante testarmos a performance da aplicação em condições reais. Isto irá determinar se a utilização de uma cache realmente será necessária e se precisaremos de uma ou duas caches. Também devemos analisar todas as classes e escolher a estratégia apropriada para cada uma das classes no caso de estarmos configurando cache de segundo nível.
Bibliografia
[1]Hibernate - JBoss Community, disponível em www.hibernate.org/
[2]Documentação de Referência Hibernate, disponível em https://docs.jboss.org/hibernate/core/3.6/reference/pt-BR/html/index.html
[3] Introdução ao Hibernate, disponível em https://docs.jboss.org/hibernate/orm/3.5/reference/en/html/queryhql.html
[4] Jeff Linwood and Dave Minter. An introduction to persistence using Hibernate 3.5, Second Edition. Apress.
[5] Steve Perkins. Hibernate Search by Example: Explore the Hibernate Search system and use its extraordinary search features in your own applications. Packt Publishing.