Hibernate Statistics e pitadas de performance com JPA

No Javaneiros 2012 falei sobre Hibernate Statistics, uma ferramenta muito bacana e que poderia ser mais utilizada.

No meu livro eu comento sobre performance: cache de segundo nível, cache de consulta e várias formas de evitar problemas como “N+1 queries“.

Mas como sabermos o que de fato o Hibernate está fazendo na nossa aplicação? Habilitar o hibernate.show_sql e analisar o console? Por mais que seja uma boa ideia utilizar boas práticas como lazy load, é complicado acompanhar as mudanças na aplicação quando mudamos um mapeamento. Será que o feeling é suficiente para medir as melhorias ou pioras do sistema? E como saber se a mudança não é psicológica, contando as consultas jogadas no console? Para isso podemos utilizar as estatísticas do hiberntate.

O código da aplicação está disponível no github, mas vamos analisar as partes do projeto aqui.

Antes de mais nada precisamos habilitar as estatísticas do hibernate através da propriedade hibernate.generate_statistics como pode ser visto no arquivo persistence.xml.

...
<property name="hibernate.generate_statistics" value="true" />
...



Persistindo

Agora podemos analisar o primeiro exemplo:

public class InsercaoTest {

	private EntityManager em;
	private Statistics statistics;

	@Before
	public void iniciaTransacao(){
		em = JpaUtil.getEntityManager();
		statistics = JpaUtil.getStatistics(em);
		em.getTransaction().begin();
	}

	@After
	public void abortaTransacao(){
		em.getTransaction().rollback();
		statistics.clear();
	}

	@Test
	public void deveInserirUmObjetoSimples(){

		Marca volks = new Marca("Volks");

		em.persist(volks);

		Assert.assertEquals(1, statistics.getEntityInsertCount());

	}
}

É possível ver o básico, que ao inserir um objeto, o método getEntityInsertCount() mostra o que foi exatamente isso que ocorreu. Agora se salvarmos dois objetos em cascata, veremos que a contagem muda:

	@Test
	public void deveInserirUmObjetoComCascade(){

		Marca volks = new Marca("Volks");
		Modelo fusca = new Modelo("Fusca", volks);

		em.persist(fusca);

		Assert.assertEquals(2, statistics.getEntityInsertCount());

	}

Olhando o código fonte podemos ver que no mapeamento definimos que ao salvar o Modelo, salvaremos também a Marca.

<h2>Buscando</h2>
<pre>
Vamos considerar agora um outro teste, agora analisando como o hibernate busca os dados.

1
	@Test
	public void deveBuscarUmObjetoPorId(){

		Marca marca = new Marca("Volks");

		em.persist(marca);
		em.clear();

		Marca volks = em.find(Marca.class, marca.getId());

		Assert.assertEquals(1, statistics.getEntityLoadCount());

		System.out.println("faz qualquer coisa");

		Marca volks2 = em.find(Marca.class, marca.getId());

		Assert.assertEquals(1, statistics.getEntityLoadCount());
	}

Reparem no em.clear(), ele é muito importante no teste pois sem isso todos os objetos estarão no cache, e assim as estatísticas responderão sempre zero.

Também é interessante imaginarmos que onde tem esse println, em uma aplicação real poderia ter muuuuita coisa. O mais importante desse teste é termos a certeza que mesmo que o objeto seja buscado por id um zilhão de vezes dentro do mesmo EntityManager, não teremos mais do que uma busca ao banco.



OpenSessionInView

É importante notarmos que na web, geralmente temos um mesmo EntityManager durante toda uma requisição, graças ao padrão OpenSessionInView. Então por mais que vários serviços, DAOs e conversores (JSF) sejam utilizados, não teremos pesquisas desnecessárias a um objeto quando o buscamos por id. Agora para isso funcionar para qualquer busca precisamos de cache de segundo nível e cache de consulta habilitados.



Eager vs Lazy

Agora podemos analisar a diferença de quando tempos um relacionamento eager e um lazy:

	@Test
	public void deveBuscarUmObjetoPorIdCarregandoRelacionamentoEager(){

		Marca volks = new Marca("Volks");
		Modelo fusca = new Modelo("Fusca", volks);

		em.persist(fusca);
		em.clear();

		Modelo modelo = em.find(Modelo.class, fusca.getId());

		Assert.assertEquals(2, statistics.getEntityLoadCount());

	}

O mapeamento @ManyToOne é eager por padrão então buscando o Modelo, a Marca vem "di grátis".

	@Test
	public void deveBuscarUmObjetoPorIdSemCarregarRelacionamentoLazy(){

		Marca volks = new Marca("Volks");
		Modelo fusca = new Modelo("Fusca", volks);

		em.persist(fusca);
		em.clear();

		Marca marca = em.find(Marca.class, volks.getId());

		Assert.assertEquals(1, statistics.getEntityLoadCount());

		System.out.println(marca.getModelos());

		Assert.assertEquals(2, statistics.getEntityLoadCount());

	}

Agora analisando um relacionamento lazy, como o @OneToMany, vemos que ele só é carregado quando precisamos.

Aqui um detalhe sobre os defaults da JPA, todo relacionamento @*ToOne é eager, e os @*ToMany são lazy.



Lazy vs Extra Lazy

Lazy é legal, mas em alguns casos a lista é carregada do banco quando na verdade precisaríamos de uma outra operação de banco. É o caso de analisarmos o tamanho de uma lista usando o método size(), com lazy normal os dados são buscados e método java simplesmente devolve seu tamanho. Aí quem não conhece o extra lazy acaba fazendo um método a mais no DAO para contar usando um count no banco, e fala: "tá vendo, falei que essa JPA é burra, cabra carregar tudo quando poderia fazer um count...".

Nenhuma ferramenta é perfeita, e em alguns casos não tem jeito e você pode precisar fazer algo que a ferramenta poderia fazer sozinha se fosse "mais esperta". Mas esse não é o caso.

@Entity
public class Concessionaria {
	...
	@OneToMany(cascade=CascadeType.ALL, mappedBy="concessionaria")
	@LazyCollection(LazyCollectionOption.EXTRA)
	private List funcionarios;
	...
}

Quando temos um extra lazy, antes de buscar os dados, o hibernate verifica se pode responder com uma consulta ao banco. Se enquadram nesse caso o size() e o contains(obj) da lista.

	@Test
	public void deveBuscarRelacionamentoAoUsarListaLazy(){

		Marca volks = new Marca("Volkswagem");
		volks.adicionaModelo(new Modelo("Gol"));
		volks.adicionaModelo(new Modelo("Polo"));
		volks.adicionaModelo(new Modelo("Fusca"));

		em.persist(volks);
		em.clear();

		Marca marca = em.find(Marca.class, volks.getId());

		Assert.assertEquals(1, statistics.getEntityLoadCount());
		Assert.assertEquals(0, statistics.getCollectionFetchCount());

		System.out.println(marca.getModelos().size());

		Assert.assertEquals(4, statistics.getEntityLoadCount());
		Assert.assertEquals(1, statistics.getCollectionFetchCount());
	}

	@Test
	public void naoDeveBuscarRelacionamentoAoUsarListaExtraLazy(){

		Concessionaria volks = new Concessionaria("Volkswagem MS 1");
		volks.adicionaFuncionario(new Funcionario("João"));
		volks.adicionaFuncionario(new Funcionario("Maria"));
		volks.adicionaFuncionario(new Funcionario("Xico"));

		em.persist(volks);
		em.clear();

		Concessionaria concessionaria = em.find(Concessionaria.class, volks.getId());

		Assert.assertEquals(1, statistics.getEntityLoadCount());
		Assert.assertEquals(0, statistics.getCollectionFetchCount());

		System.out.println(concessionaria.getFuncionarios().size());

		Assert.assertEquals(1, statistics.getEntityLoadCount());
		Assert.assertEquals(0, statistics.getCollectionFetchCount());
	}

Como o post já está bem extenso e os outros testes são ainda maiores, vamos terminando por aqui.




[momento-jabá]
Caso queira saber mais, recomendo meu livro ;)

[/momento-jabá]

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>