JPA 2: lazyloading do hibernate e weaving do eclipselink

No primeiro artigo sobre Hibernate e EclipseLink falamos sobre curiosidades gerais na diferença entre as duas implementações. A discussão nos comentários foi bem interessante, inclusive tratando sobre JDO/JPA por meio do DataNucleus, vale a pena ler. Agora, vamos para algo mais específico, que tem um impacto maior no dia a dia do desenvolvedor. Só lembrando o cenário dos testes: EclipseLink 2.6.0, Hibernate 4.3.8, MySQL, Ubuntu, em ambiente Java SE.

1 – O que acontece ao persistir uma Conta? 

Conta c = new Conta();
c.setNumero(25);
c.setTitular("Raphael");
entityManager.getTransaction().begin();

entityManager.persist(c); //linha 5

System.out.println(entityManager.contains(c));//linha 6
System.out.println(c.getId()); //linha 7

entityManager.getTransaction().commit();

System.out.println(c.getId()); //linha 9

Hibernate:

Após a execução da linha 5, um comportamento possível é disparar um INSERT em Conta. Isso varia um pouco, pois depende de qual estratégia foi utilizada para gereção automática das PK’s. Ele poderia apenas fazer um select na SEQUENCE para descobrir o próximo valor para a chave primária.

Após chamar o método PERSIST, o Hibernate coloca o objeto no estado MANAGED, logo o retorno do método é true.

Após a linha 7, ao inserir o objeto no banco de dados o framework já popula o ID do objeto referenciado pela variável “c”, caso o banco esteja vazio, este valor seria 1 por exemplo.

EclipseLink:

Após a execução da linha 5, há um comportamento mais curioso, pois o eclipseLink não faz nada.

Já na linha 6, a despeito de não ter feito nada na linha anterior, aqui a resposta é TRUE, pois ele coloca o objeto no estado MANAGED.

Com relação à linha 7, como ele ainda não foi ao banco de dados, o resultado aqui é null, que pode deixar o programador um pouco confuso, afinal ele está gerenciado. Mas afinal, quando ele vai ao banco de dados? Pode ser após um flush ou commit.

Após o commit, o EclipseLink gera duas SQL’s

[EL Fine]: sql: 2015-04-20 18:12:23.239--ClientSession(1175146719)--Connection(485701373)--INSERT INTO CONTA (NUMERO, TITULAR) VALUES (?, ?)
bind => [25, Raphael 25]
[EL Fine]: sql: 2015-04-20 18:12:23.268--ClientSession(1175146719)--Connection(485701373)--SELECT LAST_INSERT_ID()

Agora sim ele imprime o ID correto gerado pela Conta, mas pra isso ele teve que fazer um outro select.

2 – Como fica o cache de segundo nível usando o @Cacheable?

Esta foi uma outra novidade do JPA 2, o cache de segundo nível foi especificado. Basicamente temos que anotar a Entidade que deve ser cacheada usando @Cacheable. Ainda existe a possibilidade de fazer uma combinação com os valores da tag shared-cache-mode no persistence.xml.

Então vamos anotar a entidade Conta e executar o código abaixo:


 EntityManager em = emf.createEntityManager();
 EntityManager em2 = emf.createEntityManager();

 Conta conta = em.find(Conta.class, 1); //linha 3

 System.out.println(em2.getEntityManagerFactory().getCache().contains(Conta.class, 1)); //linha 4

 Conta conta2 = em2.find(Conta.class, 1); //linha 5

 System.out.println(conta == conta2); //linha 6

EclipseLink:

Após executar a linha 3, o EclipseLink dispara um SELECT.

Já na linha 4, apesar de ser um EntityManager diferente do que foi utilizado para pesquisar, o em2 já consegue identificar que existe uma Conta com ID 1 cacheada, retornando TRUE.

Como Conta já está cacheada, a linha 5 não dispara nenhum select.

Por fim, na linha 6 o resultado é FALSE, pois apesar do segundo objeto ter sido recuperado do cache, ainda são objetos diferentes.

Hibernate: Só colocando as configurações acima, o cache de segundo nível não funciona, o Hibernate dispara dois selects. Nesse, o desenvolvedor ainda terá que configurar um provider de cache como EHCache ou Infinispan.

3 – Como é o comportamento Lazy? conta.getMovimentacoes() e movimentacao.getConta()

Sabemos que por default o relacionamento @OneToMany é Lazy, podendo trazer problemas de LazyInializationException e N+1 queries quando não for bem utilizado. Há várias soluções para o problema como OpenSessionInView, queries planejas ou até mesmo alterar o comportamento para EAGER. Vamos ao código

 Conta conta = em.find(Conta.class, 1);
 System.out.println(conta.getTitular());
 System.out.println(conta.getMovimentacoes()); //linha 3

Hibernate: Executa duas queries. Uma para Conta e outra para buscar as movimentações da Conta.

EclipseLink: Se o toString de Movimentacao tiver sido implementado, ele dispara dois select’s, caso contrário, dispara apenas um SELECT em Conta e depois irá imprimir: {IndirectList: not instantiated}

Vamos então alterar o comportamento default.


 @OneToMany(mappedBy = "conta", fetch=FetchType.EAGER)
 // @OneToMany(mappedBy = "conta")
 private List<Movimentacao> movimentacoes;

Hibernate: Executa uma query que faz o left outer join, já trazendo Conta e suas respectivas movimentações.

EclipseLink: Agora, mesmo que o toString não esteja implementado, ele já busca as movimentações, porém ele faz DOIS selects.

[EL Fine]: sql: 2015-04-20 19:19:36.642--ServerSession(706277948)--Connection(156127720)--SELECT ID, NUMERO, TITULAR FROM CONTA WHERE (ID = ?)
bind => [1]
[EL Fine]: sql: 2015-04-20 19:19:36.656--ServerSession(706277948)--Connection(156127720)--SELECT ID, VALOR, CONTA_ID FROM MOVIMENTACAO WHERE (CONTA_ID = ?)</em>
bind => [1]

Apesar de termos mudado o comportamento default, colocando EAGER, o EclipseLink resolveu simplesmente ignorar este fato e ainda continua sendo LAZY“. Esta seria uma conclusão óbvia, mas não é bem assim. Na verdade, o eclipseLink está sim obedecendo o EAGER, basta comentar a linha 3 e ainda assim as movimentacoes serão retornadas, todavia ainda são feitas as duas consultas.

Outro exemplo, agora pesquisando por Movimentação.

 Movimentacao mov = em.find(Movimentacao.class, 1);
 System.out.println(mov.getValor());

 // lembrando que:
 @ManyToOne
 private Conta conta;

Hibernate: Faz somente uma query trazendo Movimentacao e sua respectiva conta, afinal é EAGER por default um relacionamento @ManyToOne

EclipseLink: Faz dois selects, busca movimentacao depois conta.

[EL Fine]: sql: 2015-04-20 19:31:15.621--ServerSession(1956710488)--Connection(2140396878)--SELECT ID, VALOR, CONTA_ID FROM MOVIMENTACAO WHERE (ID = ?)</em>
bind => [44]
[EL Fine]: sql: 2015-04-20 19:31:15.635--ServerSession(1956710488)--Connection(2140396878)--SELECT ID, NUMERO, TITULAR FROM CONTA WHERE (ID = ?)
bind => [1]

Por que no EAGER o EclipseLink faz duas SELECT’s e o Hibernate somente uma? Bom, deixo um trecho do artigo sobre JPA da WikiBooks. A recomendação é fazer uma JOIN FETCH ou poderíamos utilizar @JoinFetch.

One common misconception is that EAGER means that the relationship should be join fetched, i.e. retrieved in the same SQL SELECT statement as the source object. Some JPA providers do implement eager this way. However, just because something is desired to be loaded does not mean that it should be join fetched.

E se alterássemos o relacionamento para Lazy? @ManyToOne(fetch=FetchType.LAZY)

Hibernate: Faz apenas uma query, buscando apenas Movimentação, afinal agora só busca Conta quando for utilizada.

EclipseLink: Continua fazendo duas queries. Só que ele gera a seguinte mensagem.

[EL Warning]: metadata: 2015-04-20 19:48:33.847--ServerSession(706277948)--Reverting the lazy setting on the OneToOne or ManyToOne attribute [conta] for the entity class [class br.modelo.Movimentacao] since weaving was not enabled or did not occur.

Para habilitarmos o Lazy no EclipseLink temos que usar a propriedade weaving. Há vários posts comentando esta situação. Para habilitar o weaving, temos que passar no vm arguments o parâmetro -javaagent:eclipselink.jar

Agora ao executar o código com o EclipseLink, o Lazy é acionado e apenas um select é disparado.

[EL Fine]: sql: 2015-04-20 19:51:32.242--ServerSession(603856241)--Connection(1937575946)--SELECT ID, VALOR, CONTA_ID FROM MOVIMENTACAO WHERE (ID = ?)</em>
bind => [44]

Aqui há um outro artigo que discute os mapeamentos Hibernate X EclipseLink, inclusive usando o OpenJPA também.

Finalizando, escolher qual implementação deve ser usada, por incrível que pareça, não é uma tarefa fácil. Apesar da resposta rápida ser Hibernate, cada framework traz vantagens e desvantagens. A dica é tentar seguir o máximo possível a API para facilitar a portabilidade no futuro (mas sabemos que isso é meio utópico). Uma outra diferença é com relação ao tratamento Multi-Tenancy, entretanto, esse fica para outro post. Não deixem de comentar!

Tags:

8 Comentários

  1. Rafael Ponte 09/06/2015 at 09:09 #

    Excelente post, Lacerda!

    É o que costumo dizer para meus alunos e outros desenvolvedores que trabalho no dia a dia: por usar JPA você só deveria trocar os JARs e EM TEORIA tudo funcionaria de forma idêntica, mas NA PRÁTICA não é bem assim. E você mostrou muito bem isso nesse post!

    Uma dica bacana que você comentou foi sobre como o EAGER carrega o relacionamento. Eu aprendi as duras isso e também costumo explicar em sala de aula, mas é exatamente como você disse: você define QUAL relacionamento será carregado de forma ansiosa, porém COMO ele será carregada depende do provider (ou via select ou join).

    Enfim, entender essa diferença é crucial para evitar problemas de performance na aplicação. O pior é que ao trabalhar com JPA+Hibernate precisamos nos especializar no Hibernate; ao trabalhar com JPA+EclipseLink precisamos também nos especializar nela – o que deveria ser independente de provedor no final não é.

    Um abraço e parabéns por esse excelente post!

  2. Alexandre Aquiles 10/06/2015 at 16:12 #

    Rafael, muito bom! Muitas vezes confundimos funcionalidades do Hibernate com JPA. Os seus posts estão ajudando a elucidar o que é o que.

  3. Fernando 12/06/2015 at 10:04 #

    Cara muto bom os dois post ate agora. Foi bem explicado, como sempre vcs da caelum mandando super bem em tudo que envolve ensino.

  4. Fabricio Vallim 12/06/2015 at 14:53 #

    Ótimo post. Continue com essa série de JPA. =)

  5. Raphael Lacerda 14/06/2015 at 19:25 #

    Valeu galera!

    É isso aí, ainda tem mais um na fila por vir!

  6. Rogerio J. Gentil 17/06/2015 at 11:21 #

    Já passei pela situação do Eclipselink retornar null ao tentar obter o id da entidade depois de chamar o método persist. Na época não entendi a questão e resolvi de outra forma. Se esta publicação tivesse saído naquela época acho que não teria apanhado tanto. =)

    Em relação a suposta utopia que você mencionou @Raphael Lacerda, qual seria(m) o(s) problema(s)? Acho que isto vale um post, pois muitas vezes ouço as pessoas dizendo que seguem a especificação em função de uma possível substituição de framework de persistência.

  7. Raphael 06/07/2015 at 11:38 #

    Rogério, os problemas são essas nuances que envolvem o POST 1 e o POST 2

    Ainda tem um terceiro elemento por vir aí que é a questão do MultiTenancy

Deixe uma resposta