PersistentObjectException: entidade desanexada passada para persistir lançada pela JPA e Hibernate

237

Eu tenho um modelo de objeto persistido por JPA que contém um relacionamento muitos-para-um: um Accounttem muitos Transactions. A Transactiontem um Account.

Aqui está um trecho do código:

@Entity
public class Transaction {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @ManyToOne(cascade = {CascadeType.ALL},fetch= FetchType.EAGER)
    private Account fromAccount;
....

@Entity
public class Account {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @OneToMany(cascade = {CascadeType.ALL},fetch= FetchType.EAGER, mappedBy = "fromAccount")
    private Set<Transaction> transactions;

Eu sou capaz de criar um Accountobjeto, adicionar transações a ele e persistir o Accountobjeto corretamente. Mas, quando crio uma transação, usando uma Conta já persistente e persistindo a Transação , recebo uma exceção:

Causado por: org.hibernate.PersistentObjectException: entidade desanexada passada para persistir: com.paulsanwald.Account em org.hibernate.event.internal.DefaultPersistEventListener.onPersist (DefaultPersistEventListener.java:141)

Portanto, sou capaz de persistir um Accountque contenha transações, mas não uma transação que possua um Account. Eu pensei que isso era porque o Accountnão pode ser anexado, mas esse código ainda me dá a mesma exceção:

if (account.getId()!=null) {
    account = entityManager.merge(account);
}
Transaction transaction = new Transaction(account,"other stuff");
 // the below fails with a "detached entity" message. why?
entityManager.persist(transaction);

Como posso salvar corretamente um Transactionassociado a um Accountobjeto já persistente ?

Paul Sanwald
fonte
15
No meu caso, eu estava definindo o ID de uma entidade que estava tentando persistir usando o Entity Manager. Quando removi o setter para identificação, ele começou a funcionar bem.
Rushi Shah
No meu caso, eu não estava definindo o ID, mas havia dois usuários usando a mesma conta, um deles persistia em uma entidade (corretamente) e o erro ocorreu quando o segundo tentou manter a mesma entidade, que já era persistiu.
SergioFC 14/11/19

Respostas:

129

Esse é um problema típico de consistência bidirecional. É bem discutido neste link e também neste link.

De acordo com os artigos nos 2 links anteriores, você precisa corrigir seus levantadores nos dois lados do relacionamento bidirecional. Um exemplo de setter para o lado Um está neste link.

Um exemplo de setter para o lado Muitos está neste link.

Depois de corrigir seus setters, você deseja declarar o tipo de acesso à entidade como "Propriedade". A melhor prática para declarar o tipo de acesso "Propriedade" é mover TODAS as anotações das propriedades do membro para os getters correspondentes. Uma grande palavra de cautela é não misturar os tipos de acesso "Campo" e "Propriedade" na classe da entidade, caso contrário, o comportamento não será definido pelas especificações da JSR-317.

Sym-Sym
fonte
2
ps: a anotação @Id é a que o hibernate usa para identificar o tipo de acesso.
Diego Plentz 10/03/2015
2
A exceção é: detached entity passed to persistpor que melhorar a consistência faz com que funcione? Ok, a consistência foi reparada, mas o objeto ainda está desanexado.
Gilgamesz
1
@ Sam, muito obrigado pela sua explicação. Mas ainda não entendo. Vejo que sem setter 'especial' a relação bidirecional não é satisfeita. Mas não vejo por que o objeto foi desanexado.
Gilgamesz
28
Por favor, não poste uma resposta que responda apenas com links.
El Mac
6
Não consegue ver como essa resposta está relacionada à pergunta?
Eugen Labun 19/01/19
271

A solução é simples, basta usar o em CascadeType.MERGEvez de CascadeType.PERSISTou CascadeType.ALL.

Eu tive o mesmo problema e CascadeType.MERGEfuncionou para mim.

Espero que você esteja resolvido.

ochiWlad
fonte
5
Surpreendentemente, esse também funcionou para mim. Não faz sentido, já que o CascadeType.ALL inclui todos os outros tipos de cascata ... WTF? Eu tenho o Spring 4.0.4, o Spring Data jpa 1.8.0 e o hibernate 4.X .. Alguém tem alguma idéia de por que ALL não funciona, mas o MERGE funciona?
Vadim Kirilchuk
22
@VadimKirilchuk Isso funcionou para mim também e faz total sentido. Como a transação é PERSISTA, ela tenta PERSIST conta também e isso não funciona, pois a conta já está no banco de dados. Mas com CascadeType.MERGE, a conta é automaticamente mesclada.
Gunslinger #
4
Isso pode acontecer se você não usar transações.
lanoxx
1
outra solução: tente não inserir o objeto já persistido :)
Andrii Plotnikov
3
Obrigado cara. Não é possível evitar a inserção de objeto persistente, se você tiver uma restrição para a chave de referência NÃO ser NULA. Portanto, esta é a única solução. Mais uma vez obrigado.
makkasi
22

Remova a cascata da entidade filhaTransaction , deve ser apenas:

@Entity class Transaction {
    @ManyToOne // no cascading here!
    private Account account;
}

( FetchType.EAGERpode ser removido e também o padrão @ManyToOne)

Isso é tudo!

Por quê? Ao dizer "cascata ALL" na entidade filha, Transactionvocê exige que todas as operações do banco de dados sejam propagadas para a entidade pai Account. Se você o fizer persist(transaction), persist(account)será invocado também.

Mas apenas entidades (novas) transitórias podem ser passadas para persist( Transactionnesse caso). Os desanexados (ou outros estados não transitórios) podem não ser ( Accountneste caso, como já está no DB).

Portanto, você obtém a exceção "entidade desanexada passada para persistir" . A Accountentidade é destinada! Não é o que Transactionvocê chama persist.


Você geralmente não deseja propagar de filho para pai. Infelizmente, existem muitos exemplos de código nos livros (mesmo nos bons) e na rede, que fazem exatamente isso. Não sei por que ... Talvez às vezes simplesmente copiei várias vezes sem pensar muito ...

Adivinha o que acontece se você remove(transaction)ainda chama "cascata ALL" nesse @ManyToOne? O account(btw, com todas as outras transações!) Também será excluído do banco de dados. Mas essa não era sua intenção, era?

Eugen Labun
fonte
Só quero adicionar, se sua intenção é realmente salvar o filho junto com o pai e também excluir o pai junto com o filho, como pessoa (pai) e endereço (filho) junto com o addressId gerado automaticamente pelo DB antes de chamar save on Person, apenas faça uma chamada para salvar no endereço no seu método de transação. Dessa forma, ele seria salvo junto com o ID também gerado pelo DB. Sem impacto no desempenho, pois o Hibenate ainda faz duas consultas, estamos apenas alterando a ordem das consultas.
Vikky
Se não podemos passar nada, que valor padrão é necessário para todos os casos.
Dhwanil Patel
15

O uso da mesclagem é arriscado e complicado, portanto, é uma solução alternativa suja no seu caso. Você precisa lembrar pelo menos que, quando você passa um objeto de entidade para mesclar, ele deixa de ser anexado à transação e, em vez disso, uma nova entidade agora anexada é retornada. Isso significa que, se alguém tiver o objeto de entidade antigo ainda em sua posse, as alterações nele serão silenciosamente ignoradas e descartadas no commit.

Você não está mostrando o código completo aqui, por isso não posso verificar seu padrão de transação. Uma maneira de chegar a uma situação como essa é se você não tiver uma transação ativa ao executar a mesclagem e persistir. Nesse caso, espera-se que o provedor de persistência abra uma nova transação para cada operação JPA executada e confirme e feche-a imediatamente antes que a chamada retorne. Se esse for o caso, a mesclagem será executada em uma primeira transação e, depois que o método de mesclagem retornar, a transação será concluída e fechada e a entidade retornada será desanexada. A persistência abaixo abriria uma segunda transação e tentaria se referir a uma entidade desanexada, dando uma exceção. Sempre envolva seu código em uma transação, a menos que você saiba muito bem o que está fazendo.

Usando transações gerenciadas por contêiner, seria algo parecido com isto. Observe: isso pressupõe que o método esteja dentro de um bean de sessão e chamado via interface local ou remota.

@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void storeAccount(Account account) {
    ...

    if (account.getId()!=null) {
        account = entityManager.merge(account);
    }

    Transaction transaction = new Transaction(account,"other stuff");

    entityManager.persist(account);
}
Zds
fonte
. Eu estou enfrentando mesmo problema, eu tenho usado o @Transaction(readonly=false). Na camada de serviço, ainda im recebendo o mesmo problema,
Senthil Arumugam SP
Não posso dizer que compreendo perfeitamente por que as coisas funcionam dessa maneira, mas colocar o método persistente e exibir o mapeamento de entidades juntos em uma anotação transacional corrigiu meu problema.
Deadron
Esta é realmente uma solução melhor do que a mais votada.
Aleksei Maide
13

Provavelmente, nesse caso, você obteve seu accountobjeto usando a lógica de mesclagem e persisté usado para persistir novos objetos e ele se queixará se a hierarquia estiver tendo um objeto já persistido. Você deve usar saveOrUpdatenesses casos, em vez de persist.

dan
fonte
3
é JPA, então acho que o método análogo é .merge (), mas isso me dá a mesma exceção. Para ser claro, a transação é um novo objeto, a conta não.
21312 Paul Sanwald
@PaulSanwald Usando mergeno transactionobjeto que você obter o mesmo erro?
dan
na verdade, não, eu falei mal. se eu imergir (transação), a transação não será mantida.
Paul Sanwald 13/11
@PaulSanwald Hmm, você tem certeza de que transactionnão foi persistente? Como você checou. Observe que mergeestá retornando uma referência ao objeto persistente.
dan
o objeto retornado por .merge () tem um ID nulo. Além disso, estou fazendo um .findAll () depois e meu objeto não está lá.
Paul Sanwald
12

Não passe id (pk) para o método persistente ou tente o método save () em vez de persist ().

Angad Bansode
fonte
2
Bom conselho! Mas somente se o ID for gerado. Caso esteja atribuído, é normal definir o ID.
Aldian
isso funcionou para mim. Além disso, você pode usar TestEntityManager.persistAndFlush()para tornar uma instância gerenciada e persistente e sincronizar o contexto de persistência com o banco de dados subjacente. Retorna a entidade de origem original
Anyul Rivas
7

Como essa é uma pergunta muito comum, escrevi este artigo , no qual essa resposta se baseia.

Para corrigir o problema, siga estas etapas:

1. Removendo a associação de filhos em cascata

Portanto, você precisa remover o @CascadeType.ALLda @ManyToOneassociação. Entidades filhas não devem cascatear com associações pai. Somente entidades pai devem cascatear para entidades filho.

@ManyToOne(fetch= FetchType.LAZY)

Observe que eu defino o fetchatributo como FetchType.LAZYporque a busca ansiosa é muito ruim para o desempenho .

2. Defina os dois lados da associação

Sempre que você tem uma associação bidirecional, é necessário sincronizar os dois lados usando addChilde removeChildmétodos na entidade pai:

public void addTransaction(Transaction transaction) {
    transcations.add(transaction);
    transaction.setAccount(this);
}

public void removeTransaction(Transaction transaction) {
    transcations.remove(transaction);
    transaction.setAccount(null);
}

Para mais detalhes sobre por que é importante sincronizar as duas extremidades de uma associação bidirecional, confira este artigo .

Vlad Mihalcea
fonte
4

Na definição da sua entidade, você não está especificando o @JoinColumn para o Accountassociado a um Transaction. Você vai querer algo assim:

@Entity
public class Transaction {
    @ManyToOne(cascade = {CascadeType.ALL},fetch= FetchType.EAGER)
    @JoinColumn(name = "accountId", referencedColumnName = "id")
    private Account fromAccount;
}

EDIT: Bem, acho que seria útil se você estivesse usando a @Tableanotação em sua classe. Heh. :)

NemesisX00
fonte
2
sim, acho que não é isso, mesmo assim, adicionei @JoinColumn (name = "fromAccount_id", referencedColumnName = "id") e não funcionou :).
Paul Sanwald
Sim, eu normalmente não uso um arquivo xml de mapeamento para mapear entidades para tabelas, então, normalmente, assumo que seja baseado em anotação. Mas se eu tivesse que adivinhar, você está usando um hibernate.xml para mapear entidades para tabelas, certo?
NemesisX00
não, estou usando JPA de dados de primavera, portanto, tudo é baseado em anotação. Eu tenho uma anotação "mappedBy", do outro lado: @OneToMany (cascade = {CascadeType.ALL}, busca = FetchType.EAGER, mappedBy = "fromAccount") #
12262 Paul Sanwald
4

Mesmo que suas anotações sejam declaradas corretamente para gerenciar adequadamente o relacionamento um para muitos, você ainda poderá encontrar essa exceção precisa. Ao adicionar um novo objeto filho,, Transactiona um modelo de dados anexado, você precisará gerenciar o valor da chave primária - a menos que não deva . Se você fornecer um valor de chave primária para uma entidade filha declarada da seguinte maneira antes de chamar persist(T), você encontrará essa exceção.

@Entity
public class Transaction {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
....

Nesse caso, as anotações estão declarando que o banco de dados gerenciará a geração dos valores da chave primária da entidade após a inserção. Fornecer um você mesmo (como por meio do configurador do ID) causa essa exceção.

Alternativamente, mas efetivamente o mesmo, esta declaração de anotação resulta na mesma exceção:

@Entity
public class Transaction {
    @Id
    @org.hibernate.annotations.GenericGenerator(name="system-uuid", strategy="uuid")
    @GeneratedValue(generator="system-uuid")
    private Long id;
....

Portanto, não defina o idvalor no código do aplicativo quando ele já estiver sendo gerenciado.

dan
fonte
4

Se nada ajudar e você ainda estiver recebendo essa exceção, revise seus equals()métodos - e não inclua a coleção filho. Especialmente se você tiver uma estrutura profunda de coleções incorporadas (por exemplo, A contém Bs, B contém Cs etc.).

No exemplo de Account -> Transactions:

  public class Account {

    private Long id;
    private String accountName;
    private Set<Transaction> transactions;

    @Override
    public boolean equals(Object obj) {
      if (this == obj)
        return true;
      if (obj == null)
        return false;
      if (!(obj instanceof Account))
        return false;
      Account other = (Account) obj;
      return Objects.equals(this.id, other.id)
          && Objects.equals(this.accountName, other.accountName)
          && Objects.equals(this.transactions, other.transactions); // <--- REMOVE THIS!
    }
  }

No exemplo acima, remova as transações dos equals()cheques. Isso ocorre porque o hibernate implica que você não está tentando atualizar um objeto antigo, mas passa um novo objeto para persistir sempre que alterar o elemento na coleção filho.
É claro que essas soluções não se encaixam em todos os aplicativos e você deve projetar cuidadosamente o que deseja incluir nos métodos equalse hashCode.

FazoM
fonte
1

Talvez seja o bug do OpenJPA. Ao reverter, redefine o campo @Version, mas o pcVersionInit permanece verdadeiro. Eu tenho um AbstraceEntity que declarou o campo @Version. Eu posso solucionar isso redefinindo o campo pcVersionInit. Mas não é uma boa ideia. Eu acho que não funciona quando a entidade persistir em cascata.

    private static Field PC_VERSION_INIT = null;
    static {
        try {
            PC_VERSION_INIT = AbstractEntity.class.getDeclaredField("pcVersionInit");
            PC_VERSION_INIT.setAccessible(true);
        } catch (NoSuchFieldException | SecurityException e) {
        }
    }

    public T call(final EntityManager em) {
                if (PC_VERSION_INIT != null && isDetached(entity)) {
                    try {
                        PC_VERSION_INIT.set(entity, false);
                    } catch (IllegalArgumentException | IllegalAccessException e) {
                    }
                }
                em.persist(entity);
                return entity;
            }

            /**
             * @param entity
             * @param detached
             * @return
             */
            private boolean isDetached(final Object entity) {
                if (entity instanceof PersistenceCapable) {
                    PersistenceCapable pc = (PersistenceCapable) entity;
                    if (pc.pcIsDetached() == Boolean.TRUE) {
                        return true;
                    }
                }
                return false;
            }
Yaocl
fonte
1

Você precisa definir a transação para cada conta.

foreach(Account account : accounts){
    account.setTransaction(transactionObj);
}

Ou pode ser suficiente (se apropriado) para definir os IDs como nulos em muitos aspectos.

// list of existing accounts
List<Account> accounts = new ArrayList<>(transactionObj.getAccounts());

foreach(Account account : accounts){
    account.setId(null);
}

transactionObj.setAccounts(accounts);

// just persist transactionObj using EntityManager merge() method.
stakahop
fonte
1
cascadeType.MERGE,fetch= FetchType.LAZY
James Siva
fonte
1
Oi James, e bem-vindo, você deve tentar evitar respostas apenas de código. Indique como isso resolve o problema indicado na pergunta (e quando é aplicável ou não, nível da API etc.).
Maarten Bodewes
Revisores de VLQ: consulte meta.stackoverflow.com/questions/260411/… . respostas somente de código não merecem exclusão, a ação apropriada é selecionar "Parece OK".
Nathan Hughes
1

Minha resposta baseada em JPA do Spring Data: simplesmente adicionei uma @Transactionalanotação ao meu método externo.

Por que funciona

A entidade filha foi imediatamente desanexada porque não havia um contexto ativo da Hibernate Session. O fornecimento de uma transação Spring (Data JPA) garante a presença de uma sessão de hibernação.

Referência:

https://vladmihalcea.com/a-beginners-guide-to-jpa-hibernate-entity-state-transitions/

JJ Zabkar
fonte
0

No meu caso, eu estava confirmando a transação quando o método persist foi usado. Ao alterar o método persistir para salvar , ele foi resolvido.

H.Ostwal
fonte
0

Se as soluções acima não funcionarem apenas uma vez, comente os métodos getter e setter da classe de entidade e não defina o valor de id. (Chave primária) Então isso funcionará.

Shubham
fonte
0

@OneToMany (mappedBy = "xxxx", cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REMOVE}) funcionou para mim.

SateeshKasaboina
fonte
0

Resolvido salvando o objeto dependente antes do próximo.

Isso aconteceu comigo porque eu não estava definindo o ID (que não foi gerado automaticamente). e tentando salvar com relação @ManytoOne

Shahid Hussain Abbasi
fonte