Solucionado (ver solução)
Solucionado
(ver solução)
15
respostas

Relacionamentos bidirecionais e o comportamento LAZY

Olá Pessoal,

Desejo entender um pouco mais o comportamento do entityManager.merge(Object o).

Por que ao executar o código a seguir, a coleção de movimentações armazenadas no DB não é carregada (valor null)?

Conta conta = new Conta();

conta.setTitular("Maria Silva");
conta.setNumero("01010");
conta.setAgencia("1011");
conta.setBanco("033 - BANCO SANTANDER");
conta.setId(12);

Conta contaManaged = entityManager.merge(conta);

for (Movimentacao movimentacao : contaManaged.getMovimentacoes()) {
    System.out.println("Movimentacao " +
    movimentacao.getId() + " - " + 
    movimentacao.getDescricao());
}

As entidades Conta e Movimentacao são definidas como nos exercícios.

Grata, Rita

15 respostas

Olá Rita,

Verifique se você possui movimentações associadas ao ID 12 de Conta no seu banco.

Abraço

Oi Paulo,

muito obrigada pela resposta.

Sim. No banco de dados estão mantidas algumas movimentações associadas a Conta id=12.

O que que não existe até o momento da sincronização (merge) é o fetch das movimentações (conta.getMovimentacoes()).

A conta até o momento do merge não possui as movimentações em memória (o list está vazio, visto que a busca é lazy e até então não precisei da coleção).

Imaginei que, ao executar o merge, e então ter uma Conta (contaManaged) de volta ao Persist Context eu poderia realizar o contaManaged.getMovimentacao() e iterar a lista.

Eu sei que se após o merge e o commit, se eu executar um refresh ou find, e então o getMovimentacoes, a coleção é resgatada do banco.

Tem mais alguma dica do porquê a entidade retornada pelo merge não realiza a consulta da coleção?

Abraço, Rita

Rita, você poderia colocar aqui o código das duas classes? Sei que falou que está igual ao exercício, mas só quero dar uma olhada para tirar umas dúvidas.

Abraço

Oi Paulo, segue o código:

Classe Conta:

package br.com.alura.financas.modelo;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;

@Entity
public class Conta {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String titular;
    private String numero;
    private String banco;
    private String agencia;

    @OneToMany(mappedBy = "conta")
    private List<Movimentacao> movimentacoes = new ArrayList<Movimentacao>(); //Tambem fiz o teste sem inicializar o atributo.

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getTitular() {
        return titular;
    }

    public void setTitular(String titular) {
        this.titular = titular;
    }

    public String getNumero() {
        return numero;
    }

    public void setNumero(String numero) {
        this.numero = numero;
    }

    public String getBanco() {
        return banco;
    }

    public void setBanco(String banco) {
        this.banco = banco;
    }

    public String getAgencia() {
        return agencia;
    }

    public void setAgencia(String agencia) {
        this.agencia = agencia;
    }

    public List<Movimentacao> getMovimentacoes() {

        return movimentacoes;
    }

    public void setMovimentacoes(List<Movimentacao> movimentacoes) {
        if (this.movimentacoes == null)
            this.movimentacoes = movimentacoes;
        else {
            this.movimentacoes.retainAll(movimentacoes);
            this.movimentacoes.addAll(movimentacoes);
        }
    }
    public void removeChild(Movimentacao child) {
        movimentacoes.remove(child);
        child.setConta(null);
    }
    public void addChild(Movimentacao child) {
        movimentacoes.add(child);
        child.setConta(this);
    }
}

Movimentação:

package br.com.alura.financas.modelo;

import java.math.BigDecimal;
import java.util.Calendar;

import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

@Entity
public class Movimentacao {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String descricao;

    private BigDecimal valor;

    @Enumerated(EnumType.STRING)
    private TipoMovimentacao tipo;

    @Temporal(TemporalType.DATE)
    private Calendar data;

    @ManyToOne
    private Conta conta;


    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public String getDescricao() {
        return descricao;
    }
    public void setDescricao(String descricao) {
        this.descricao = descricao;
    }
    public BigDecimal getValor() {
        return valor;
    }
    public void setValor(BigDecimal valor) {
        this.valor = valor;
    }
    public TipoMovimentacao getTipo() {
        return tipo;
    }
    public void setTipo(TipoMovimentacao tipo) {
        this.tipo = tipo;
    }
    public Calendar getData() {
        return data;
    }
    public void setData(Calendar data) {
        this.data = data;
    }
    public Conta getConta() {
        return conta;
    }
    public void setConta(Conta conta) {
        this.conta = conta;
    }    
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((id == null) ? 0 : id.hashCode());
        return result;
    }    
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Movimentacao other = (Movimentacao) obj;
        if (id == null) {
            if (other.id != null)
                return false;
        } else if (!id.equals(other.id))
            return false;
        return true;
    }    
}

Programa teste:

package br.com.alura.financas.programa;

import javax.persistence.EntityManager;
import br.com.alura.financas.modelo.Conta;
import br.com.alura.financas.modelo.Movimentacao;
import br.com.alura.financas.util.JPAUtil;

public class TestaMergeUpdateCascade {

    public static void main(String[] args) {

        JPAUtil factory = new JPAUtil();

        EntityManager manager = factory.getEntityManager();

        //Entity Detached
        Conta conta = new Conta();
        conta.setTitular("Rita Lima");
        conta.setNumero("01010");
        conta.setAgencia("1011");
        conta.setBanco("033 - BANCO SANTANDER");
        conta.setId(12);

        manager.getTransaction().begin();

        Conta contaManaged = manager.merge(conta);

        for (Movimentacao movimentacao : contaManaged.getMovimentacoes()) {
            System.out.println("Movimentacao " + movimentacao.getId() + " - " + movimentacao.getDescricao());
        }

        manager.getTransaction().commit();

        manager.close();
        factory.close();
    }
}

Factory:

package br.com.alura.financas.util;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

public class JPAUtil {

    private static EntityManagerFactory factory = Persistence.createEntityManagerFactory("financas");

    @SuppressWarnings("static-access")
    public EntityManager getEntityManager() {
        return this.factory.createEntityManager();
    }

    public void close() {
        this.factory.close();
    }
}

Obrigada novamente.

Rita

Oi Rita, eu não entendi esse setMovimentaoes. Se está igual ao curso, eu não sei qual foi a motivação no curso para fazer isso. Mas tira ele daí por enquanto. Deixa o set assim:

public void setMovimentacoes(List<Movimentacao> movimentacoes) {
  this.movimentacoes = movimentacoes;
}

Faz o teste.

Agora, mesmo assim, realmente era pra funcionar. Será que o banco está com movimentações associadas mesmo? Se isso não funcionar, vou testar aqui tentando executar esse código que passou acima.

Abraço

Realmente, eu acho, que ao fazer o merge de um objeto (new) onde a lista de movimentação está null, com o lazy o hibernate considera que agora a listagem de movimentações está vazia?

Não sei se é o correto usar um transient e depois tentar fazer o merge para atualizar alguns dados somente deixando outros null.

Acredito que correto seria pedir ao JPA um objeto managed e ai setar o que for necessário. Fiz o teste com Conta teste = manager.getReference(Conta.class, 2);

Mas realmente ainda tenho dúvidas neste ponto, precisaremos sempre ter que apelar para a JPQL e forçar o fetch eager para trazer os relacionamentos?

Oi Paulo,

De fato o setMovimentacoes() estava diferente daquele sugerido no curso. Fiz as alterações sugeridas, e ainda assim não funcionou.

Segui os comentários feitos pelo Daniel. De fato, ao realizar o load do objeto, seja pelo getReference() ou find(), os relacionamentos passam a ser considerados (eager ou lazy); E então posso realizar qualquer operação sobre o objeto. Normal.

Mas minha dúvida sobre o funcionamento do merge() persiste. Tenho um objeto que está desvinculado do contexto, e desejo vinculá-lo, e com isso ter acesso a seu estado (com seus relacionamento). Imaginei que o merge seria usado para esses casos.

Talvez eu ainda não tenha compreendido bem em que situações, e como, devo usar o merge().

Obrigada Pessoal!

Oi Rita e Daniel,

O merge ele retorna o objeto no estado managed. É como se ele fizesse:

public Conta merge(Conta c) {
  Conta outraconta = em.find(c.getId(), Conta.class);
  outraconta.copiarCampos(c);
  em.update(outraconta);
  return outraconta;
}

Ou seja, o que fica managed é o objeto que ele retorna e não o que você passa para ele salvar.

Porém, existem outras restrições aos relacionamentos. Vamos tentar mais uma solução e se não der certo aí, vou tentar reproduzir o ambiente aqui, pois realmente já era para funcionar.

Faça o seguinte:

@OneToMany(mappedBy = "conta", cascade=CascadeType.MERGE)
private List<Movimentacao> movimentacoes = new ArrayList<Movimentacao>();

Usar EAGER não é uma boa, pois você vai detonar a quantidade de objetos que o sistema vai carregar e isso é ruim. Podemos fazer select c from Conta c join fetch c.movimentacoes como ultimo recurso para receber as movimentações junto com a conta, mas claro, sei que você quer carregar no merge.

Não deu certo com o merge, ainda recebo null ao fazer o get das movimentações.

for (Movimentacao movimentacao : contaManaged.getMovimentacoes()) { ---

Exception in thread "main" java.lang.NullPointerException at br.com.caelum.financas.teste.TestaMergeUpdateCascade.main(TestaMergeUpdateCascade.java:31)

@OneToMany(mappedBy="conta", cascade=CascadeType.MERGE)
    private List<Movimentacao> movimentacoes;

Somente consegui usando getReference. Outra dúvida, não seria melhor usar Criterias ao invés de JPQL ou HQL?

Olá Daniel e Rita

Essa confusão que surgiu sobre o funcionamento do merge é por causa da spec da JPA.

If X is a detached entity, the state of X is copied onto a pre-existing managed entity instance X'
of the same identity or a new managed copy X' of X is created.

Então quando executamos o código:

public class TestaMergeUpdateCascade {

    public static void main(String[] args) {

        JPAUtil factory = new JPAUtil();

        EntityManager manager = factory.getEntityManager();

        //Entity Detached
        Conta conta = new Conta();
        conta.setTitular("Rita Lima");
        conta.setNumero("01010");
        conta.setAgencia("1011");
        conta.setBanco("033 - BANCO SANTANDER");
        conta.setId(12);

        manager.getTransaction().begin();

        Conta contaManaged = manager.merge(conta);

        for (Movimentacao movimentacao : contaManaged.getMovimentacoes()) {
            System.out.println("Movimentacao " + movimentacao.getId() + " - " + movimentacao.getDescricao());
        }

        manager.getTransaction().commit();

        manager.close();
        factory.close();
    }
}

A variável contaManaged realmente está no estado managed da JPA, porém como é dito no texto da spec, essa entidade managed possui o estado copiado da entidade transient com id que foi passado como argumento para o merge. Então como a lista de movimentações da variável conta ainda não foi inicializada (está null), a entidade managed devolvida pelo merge também terá a lista de movimentações nula.

Sobre a Jpql vs Criteria, a pergunta do Daniel, essas duas apis são ferramentas feitas para resolver problemas específicos da aplicação. Quando queremos executar sempre a mesma query no banco de dados, é melhor utilizar a JPQL (ou HQL), pois o código com a criteria é mais complicado. Já no caso em que temos que implementar uma query mais dinâmica, é melhor utilizar a criteria.

Show de bola! Valeu pela explicação da minha pergunta, agora caiu a ficha, sempre ficava em dúvida de quando fazer usando criteria ou quando usar hql. No caso do null, foi o que eu imaginava, mas não sabia o motivo, então a documentação da JPA explica o pepino. :)

Olá Rita

Sua dúvida foi resolvida?

Olá Victor, Minha dúvida foi esclarecida. Muito obrigada.

Sinto muito pela demora em responser, estive envolvida e outras atividades nas ultimas semanas.

Bom...

Considerando sua explicação, assim como as dicas do Paulo, observei que, ao fazer uso do merge associado com diferentes estratégias de mapeamento (uso ou não de Cascade, Cascade com orphanRemoval), podem surgir alguns efeitos indesejados ou mesmo "inconsistências" entre o estado da entidade e suas associações, uma vez a entidade no estado managed, e o que existe no banco.

A melhor prática no caso de Relacionamentos Bidirecionais é então fazer o find ou getReference da entidade para garantir que os estados da entidade (Conta) e suas associações (Movimentacao) do Persistence Context estão sincronizados com os valores no banco, e só então aplicar as alterações (set, removeChild, etc.)?

Mais uma vez obrigada,

Abraço,

Rita

solução!

Oi Rita

Como você deduziu, dentro da JPA, geralmente é melhor utilizar objetos no estado Managed para trabalhar com o banco de dados para manter uma consistência maior dos dados.

Só para exemplificar, imagine que configuramos um cascade remove na lista de movimentações da conta, mas na JPA essa operação é implementada no memória da aplicação e não no banco de dados, então internamente o que vai acontecer na quando apagamos a conta é algo parecido com o código abaixo:

for(Movimentacao m in c.getMovimentacoes()){
   em.remove(m);
}

Veja que se nós não tivermos uma entidade gerenciada, o c.getMovimentações() não vai realmente remover as movimentações do banco de dados, pois elas podem não estar carregadas na memória.

Oi Victor,

Um super obrigada.

Sua explicação me ajudou bastante no entendimento sobre a JPA.