Qual é a maneira correta de reconectar objetos desanexados no Hibernate?

186

Eu tenho uma situação em que preciso anexar novamente objetos desanexados a uma sessão de hibernação, embora um objeto da mesma identidade PODE já existir na sessão, o que causará erros.

Agora, eu posso fazer uma de duas coisas.

  1. getHibernateTemplate().update( obj ) Isso funciona se e somente se um objeto ainda não existir na sessão de hibernação. Exceções são lançadas informando que um objeto com o identificador fornecido já existe na sessão quando eu precisar mais tarde.

  2. getHibernateTemplate().merge( obj ) Isso funciona se e somente se houver um objeto na sessão de hibernação. Exceções são lançadas quando eu preciso que o objeto esteja em uma sessão posteriormente, se eu usar isso.

Dados esses dois cenários, como anexar genericamente sessões a objetos? Não quero usar exceções para controlar o fluxo da solução desse problema, pois deve haver uma solução mais elegante ...

Stefan Kendall
fonte

Respostas:

181

Portanto, parece que não há como reconectar uma entidade desatualizada obsoleta na JPA.

merge() enviará o estado obsoleto para o banco de dados e substituirá as atualizações intermediárias.

refresh() não pode ser chamado em uma entidade desanexada.

lock() não pode ser chamado em uma entidade desanexada e, mesmo que pudesse, e reconectou a entidade, chamando 'lock' com o argumento 'LockMode.NONE', implicando que você está bloqueando, mas não bloqueando, é a parte mais contra-intuitiva do design da API Que eu já vi.

Então você está preso. Existe um detach()método, mas não attach()ou reattach(). Uma etapa óbvia no ciclo de vida do objeto não está disponível para você.

A julgar pelo número de perguntas semelhantes sobre o JPA, parece que, mesmo que o JPA afirme ter um modelo coerente, certamente não corresponde ao modelo mental da maioria dos programadores, que foram amaldiçoados a perder muitas horas tentando entender como obter O JPA faz as coisas mais simples e acaba com o código de gerenciamento de cache em todos os aplicativos.

Parece que a única maneira de fazer isso é descartar sua entidade desatualizada e fazer uma consulta de localização com o mesmo ID, que atingirá o L2 ou o DB.

Mik

mikhailfranco
fonte
1
Gostaria de saber se existe uma razão pela qual a especificação JPA não permite refresh()entidades desanexadas? Examinando a especificação 2.0, não vejo justificativa; apenas que não é permitido.
FGreg 24/10/2013
11
Definitivamente, isso NÃO é preciso. Do JPwH: *Reattaching a modified detached instance* A detached instance may be reattached to a new Session (and managed by this new persistence context) by calling update() on the detached object. In our experience, it may be easier for you to understand the following code if you rename the update() method in your mind to reattach()—however, there is a good reason it’s called updating.Mais informações podem ser encontradas na seção 9.3.2
cwash 26/02/14
Objetos persistentes funcionam muito bem, o sinalizador sujo é definido com base no delta entre a carga inicial e o (s) valor (es) no tempo de descarga (). Objetos desanexados precisam e não possuem essa funcionalidade no momento. A maneira de o hibernar fazer isso é adicionar um hash / ID adicional para objetos desanexados. E mantenha disponível uma captura instantânea do último estado do objeto desanexado, da mesma forma que para objetos persistentes. Assim, eles podem aproveitar todo o código existente e fazê-lo funcionar para objetos desanexados. Desta forma, como @mikhailfranco observou, não "empurraremos o estado obsoleto para o banco de dados e substituiremos as atualizações intermediárias"
tom
2
De acordo com o javadoc do Hibernate (mas não o JPA), lock(LockMode.NONE)pode de fato ser chamado em um objeto transitório, e ele anexa novamente a entidade à sessão. Veja stackoverflow.com/a/3683370/14379
seanf
O bloqueio não funcionou para mim: java.lang.IllegalArgumentException: entidade que não está no contexto de persistência em org.hibernate.internal.SessionImpl.lock (SessionImpl.java:3491) em org.hibernate.internal.SessionImpl.lock (SessionImpl. java: 3482) em com.github.vok.framework.DisableTransactionControlEMDelegate.lock (DB.kt)
Martin Vysny
32

Todas essas respostas perdem uma distinção importante. update () é usado para (re) anexar seu gráfico de objeto a uma Session. Os objetos que você passa são os que são gerenciados.

merge () não é realmente uma API de (re) anexo. Observe que merge () tem um valor de retorno? Isso ocorre porque ele retorna o gráfico gerenciado, que pode não ser o gráfico que você passou nele. merge () é uma API da JPA e seu comportamento é controlado pelas especificações da JPA. Se o objeto que você passar para mesclar () já estiver gerenciado (já associado à Sessão), esse é o gráfico com o qual o Hibernate trabalha; o objeto passado é o mesmo objeto retornado de merge (). Se, no entanto, o objeto que você passar para mesclar () for desanexado, o Hibernate cria um novo gráfico de objeto que é gerenciado e copia o estado do seu gráfico desanexado para o novo gráfico gerenciado. Novamente, tudo isso é ditado e regido pelas especificações da JPA.

Em termos de uma estratégia genérica para "garantir que essa entidade seja gerenciada ou gerenciada", isso depende de que você também queira considerar os dados ainda não inseridos. Supondo que sim, use algo como

if ( session.contains( myEntity ) ) {
    // nothing to do... myEntity is already associated with the session
}
else {
    session.saveOrUpdate( myEntity );
}

Observe que usei saveOrUpdate () em vez de update (). Se você não deseja que os dados ainda não inseridos sejam tratados aqui, use update () em vez disso ...

Steve Ebersole
fonte
3
Esta é a resposta certa para esta pergunta - caso encerrado!
Cwash
2
Session.contains(Object)verificações por referência. Se já houver outra Entidade representando a mesma linha na sessão e você passar uma instância desanexada, você receberá uma exceção.
djmj
Como Session.contains(Object)verificações por referência, se houver outra Entidade representando a mesma linha na sessão, ela retornará false e a atualizará.
AxelWass
19

Resposta não diplomática: você provavelmente está procurando um contexto de persistência estendido. Essa é uma das principais razões por trás do Seam Framework ... Se você está tendo dificuldades para usar o Hibernate no Spring, em particular, consulte esta parte dos documentos do Seam.

Resposta diplomática: Isso é descrito nos documentos do Hibernate . Se você precisar de mais esclarecimentos, consulte a Seção 9.3.2 da Persistência do Java com o Hibernate, denominada "Trabalhando com objetos desanexados". Eu recomendo fortemente que você obtenha este livro se estiver fazendo algo mais que CRUD com o Hibernate.

cwash
fonte
5
Do seamframework.org : "O desenvolvimento ativo do Seam 3 foi interrompido pela Red Hat." O link "esta parte dos documentos do Seam" também está morto.
badbishop
14

Se você tem certeza de que sua entidade não foi modificada (ou se você concorda que alguma modificação será perdida), você pode anexá-la novamente à sessão com bloqueio.

session.lock(entity, LockMode.NONE);

Ele não bloqueia nada, mas obtém a entidade do cache da sessão ou (se não for encontrada lá) a lê no DB.

É muito útil evitar LazyInitException quando você está navegando em relações de entidades "antigas" (da HttpSession, por exemplo). Você primeiro "anexa novamente" a entidade.

O uso de get também pode funcionar, exceto quando você mapear a herança (que já lançará uma exceção no getId ()).

entity = session.get(entity.getClass(), entity.getId());
John Rizzo
fonte
2
Eu gostaria de associar novamente uma entidade à sessão. Infelizmente, Session.lock(entity, LockMode.NONE)falha com a exceção dizendo: não foi possível reassociar a coleção transitória não inicializada. Como pode superar isso?
precisa saber é o seguinte
1
Na verdade, eu não estava completamente certa. O uso de lock () reconecta sua entidade, mas não as outras entidades vinculadas a ela. Portanto, se você fizer entity.getOtherEntity (). GetYetAnotherEntity (), poderá ter uma exceção LazyInit. A única maneira que sei superar é usar o find. entidade = em.find (entity.getClass (), entity.getId ();
John Rizzo
Não há Session.find()método de API. Talvez você queira dizer Session.load(Object object, Serializable id).
dma_k
11

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

Estados da entidade

A JPA define os seguintes estados da entidade:

Novo (transitório)

Um objeto recém-criado que nunca foi associado a um Hibernate Session(aka Persistence Context) e não está mapeado para nenhuma linha da tabela de banco de dados é considerado no estado Novo (Transitório).

Para nos tornarmos persistentes, precisamos chamar explicitamente o EntityManager#persistmétodo ou fazer uso do mecanismo de persistência transitiva.

Persistente (gerenciado)

Uma entidade persistente foi associada a uma linha da tabela de banco de dados e está sendo gerenciada pelo Contexto de Persistência atualmente em execução. Qualquer alteração feita em uma entidade será detectada e propagada no banco de dados (durante o tempo de liberação da sessão).

Com o Hibernate, não precisamos mais executar instruções INSERT / UPDATE / DELETE. O Hibernate emprega um estilo de trabalho de write-behind transacional e as alterações são sincronizadas no último momento responsável, durante o Sessiontempo de liberação atual .

Independente

Depois que o contexto de persistência em execução no momento é fechado, todas as entidades gerenciadas anteriormente ficam desanexadas. As alterações sucessivas não serão mais rastreadas e nenhuma sincronização automática de banco de dados ocorrerá.

Transições de estado da entidade

Você pode alterar o estado da entidade usando vários métodos definidos pela EntityManagerinterface.

Para entender melhor as transições de estado da entidade JPA, considere o seguinte diagrama:

Transições de estado da entidade JPA

Ao usar o JPA, para reassociar uma entidade desanexada a uma ativa EntityManager, você pode usar a operação de mesclagem .

Ao usar a API nativa do Hibernate, além de merge, você pode reconectar uma entidade desanexada a uma sessão ativa do Hibernate usando os métodos de atualização, conforme demonstrado no diagrama a seguir:

Transições de estado de entidade do Hibernate

Mesclando uma entidade desanexada

A mesclagem copiará o estado da entidade desanexada (origem) para uma instância da entidade gerenciada (destino).

Considere que persistimos a Bookentidade a seguir e agora a entidade é desanexada, pois o EntityManagerque foi usado para persistir a entidade foi fechada:

Book _book = doInJPA(entityManager -> {
    Book book = new Book()
    .setIsbn("978-9730228236")
    .setTitle("High-Performance Java Persistence")
    .setAuthor("Vlad Mihalcea");

    entityManager.persist(book);

    return book;
});

Enquanto a entidade está no estado desanexado, nós a modificamos da seguinte maneira:

_book.setTitle(
    "High-Performance Java Persistence, 2nd edition"
);

Agora, queremos propagar as alterações no banco de dados, para que possamos chamar o mergemétodo:

doInJPA(entityManager -> {
    Book book = entityManager.merge(_book);

    LOGGER.info("Merging the Book entity");

    assertFalse(book == _book);
});

E o Hibernate irá executar as seguintes instruções SQL:

SELECT
    b.id,
    b.author AS author2_0_,
    b.isbn AS isbn3_0_,
    b.title AS title4_0_
FROM
    book b
WHERE
    b.id = 1

-- Merging the Book entity

UPDATE
    book
SET
    author = 'Vlad Mihalcea',
    isbn = '978-9730228236',
    title = 'High-Performance Java Persistence, 2nd edition'
WHERE
    id = 1

Se a entidade mesclada não tiver equivalente no atual EntityManager, uma nova captura instantânea da entidade será buscada no banco de dados.

Uma vez que existe uma entidade gerenciada, a JPA copia o estado da entidade desanexada para a que atualmente é gerenciada e, durante o Contexto de Persistênciaflush , um UPDATE será gerado se o mecanismo de verificação sujo descobrir que a entidade gerenciada foi alterada.

Portanto, ao usar merge, a instância do objeto desanexado continuará desanexada, mesmo após a operação de mesclagem.

Anexando novamente uma entidade desanexada

O Hibernate, mas não o JPA, oferece suporte à recolocação através do updatemétodo.

Um Hibernate Sessionpode associar apenas um objeto de entidade para uma determinada linha do banco de dados. Isso ocorre porque o contexto de persistência atua como um cache na memória (cache de primeiro nível) e apenas um valor (entidade) é associado a uma determinada chave (tipo de entidade e identificador de banco de dados).

Uma entidade pode ser reconectada apenas se não houver outro objeto da JVM (correspondente à mesma linha do banco de dados) já associado ao Hibernate atual Session.

Considerando que mantivemos a Bookentidade e a modificamos quando a Bookentidade estava no estado desanexado:

Book _book = doInJPA(entityManager -> {
    Book book = new Book()
    .setIsbn("978-9730228236")
    .setTitle("High-Performance Java Persistence")
    .setAuthor("Vlad Mihalcea");

    entityManager.persist(book);

    return book;
});

_book.setTitle(
    "High-Performance Java Persistence, 2nd edition"
);

Podemos reconectar a entidade desanexada assim:

doInJPA(entityManager -> {
    Session session = entityManager.unwrap(Session.class);

    session.update(_book);

    LOGGER.info("Updating the Book entity");
});

E o Hibernate executará a seguinte instrução SQL:

-- Updating the Book entity

UPDATE
    book
SET
    author = 'Vlad Mihalcea',
    isbn = '978-9730228236',
    title = 'High-Performance Java Persistence, 2nd edition'
WHERE
    id = 1

O updatemétodo requer que você entre unwrapno EntityManagerHibernate Session.

Diferentemente merge, a entidade desanexada fornecida será reassociada ao contexto de persistência atual e um UPDATE é agendado durante a liberação, independentemente de a entidade ter modificado ou não.

Para evitar isso, você pode usar a @SelectBeforeUpdateanotação Hibernate, que acionará uma instrução SELECT que buscou o estado carregado, que é usado pelo mecanismo de verificação suja.

@Entity(name = "Book")
@Table(name = "book")
@SelectBeforeUpdate
public class Book {

    //Code omitted for brevity
}

Cuidado com a NonUniqueObjectException

Um problema que pode ocorrer updateé se o contexto de persistência já contiver uma referência de entidade com o mesmo ID e do mesmo tipo, como no exemplo a seguir:

Book _book = doInJPA(entityManager -> {
    Book book = new Book()
    .setIsbn("978-9730228236")
    .setTitle("High-Performance Java Persistence")
    .setAuthor("Vlad Mihalcea");

    Session session = entityManager.unwrap(Session.class);
    session.saveOrUpdate(book);

    return book;
});

_book.setTitle(
    "High-Performance Java Persistence, 2nd edition"
);

try {
    doInJPA(entityManager -> {
        Book book = entityManager.find(
            Book.class,
            _book.getId()
        );

        Session session = entityManager.unwrap(Session.class);
        session.saveOrUpdate(_book);
    });
} catch (NonUniqueObjectException e) {
    LOGGER.error(
        "The Persistence Context cannot hold " +
        "two representations of the same entity",
        e
    );
}

Agora, ao executar o caso de teste acima, o Hibernate lançará um NonUniqueObjectExceptionporque o segundo EntityManagerjá contém uma Bookentidade com o mesmo identificador que passamos update, e o Contexto de Persistência não pode conter duas representações da mesma entidade.

org.hibernate.NonUniqueObjectException:
    A different object with the same identifier value was already associated with the session : [com.vladmihalcea.book.hpjp.hibernate.pc.Book#1]
    at org.hibernate.engine.internal.StatefulPersistenceContext.checkUniqueness(StatefulPersistenceContext.java:651)
    at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.performUpdate(DefaultSaveOrUpdateEventListener.java:284)
    at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.entityIsDetached(DefaultSaveOrUpdateEventListener.java:227)
    at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.performSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:92)
    at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.onSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:73)
    at org.hibernate.internal.SessionImpl.fireSaveOrUpdate(SessionImpl.java:682)
    at org.hibernate.internal.SessionImpl.saveOrUpdate(SessionImpl.java:674)

Conclusão

O mergemétodo deve ser preferido se você estiver usando o bloqueio otimista, pois permite evitar atualizações perdidas. Para mais detalhes sobre este tópico, consulte este artigo .

A updateé bom para atualizações em lote como ele pode impedir que a instrução SELECT adicional gerado pela mergeoperação, reduzindo assim o tempo de execução de atualização em lote.

Vlad Mihalcea
fonte
Boa resposta. Eu estava pensando sobre a @SelectBeforeUpdateanotação embora. Quando a seleção é acionada? Na chamada update, imediatamente antes da liberação ou isso realmente não importa (pode ser importante se o hibernate buscar todas as entidades anotadas em uma chamada antes da liberação)?
Andronicus
O @SelectBeforeUpdatedispara o SELECT durante a flushoperação de contexto de persistência . Confira o getDatabaseSnapshotmétodo noDefaultFlushEntityEventListener para mais detalhes.
Vlad Mihalcea 26/06
10

Voltei ao JavaDoc org.hibernate.Sessione encontrei o seguinte:

Instâncias transitórias podem se tornar persistentes chamando save(), persist()ou saveOrUpdate(). Instâncias persistentes podem se tornar transitórias chamando delete(). Qualquer instância retornada por um método get()ou load()é persistente. Instâncias destacadas podem ser feitas persistentes chamando update(), saveOrUpdate(), lock()ou replicate(). O estado de uma instância transitória ou desanexada também pode se tornar persistente como uma nova instância persistente chamando merge().

Assim update(), saveOrUpdate(), lock(), replicate()e merge()são as opções de candidatos.

update(): Gerará uma exceção se houver uma instância persistente com o mesmo identificador.

saveOrUpdate(): Salve ou atualize

lock(): Descontinuada

replicate(): Persiste o estado da instância desanexada especificada, reutilizando o valor atual do identificador.

merge(): Retorna um objeto persistente com o mesmo identificador. A instância fornecida não se associa à sessão.

Portanto, lock()não deve ser usado imediatamente e, com base no requisito funcional, um ou mais deles podem ser escolhidos.

Amitabha Roy
fonte
7

Fiz dessa maneira em C # com NHibernate, mas deve funcionar da mesma maneira em Java:

public virtual void Attach()
{
    if (!HibernateSessionManager.Instance.GetSession().Contains(this))
    {
        ISession session = HibernateSessionManager.Instance.GetSession();
        using (ITransaction t = session.BeginTransaction())
        {
            session.Lock(this, NHibernate.LockMode.None);
            t.Commit();
        }
    }
}

O primeiro bloqueio foi chamado em todos os objetos porque Contains sempre era falso. O problema é que o NHibernate compara objetos por ID e tipo de banco de dados. Contém usa o equalsmétodo, que é comparado por referência, se não for substituído. Com esse equalsmétodo, ele funciona sem nenhuma exceção:

public override bool Equals(object obj)
{
    if (this == obj) { 
        return true;
    } 
    if (GetType() != obj.GetType()) {
        return false;
    }
    if (Id != ((BaseObject)obj).Id)
    {
        return false;
    }
    return true;
}
Verena Haunschmid
fonte
4

Session.contains(Object obj) verifica a referência e não detectará uma instância diferente que represente a mesma linha e já esteja anexada a ela.

Aqui minha solução genérica para Entidades com uma propriedade identificadora.

public static void update(final Session session, final Object entity)
{
    // if the given instance is in session, nothing to do
    if (session.contains(entity))
        return;

    // check if there is already a different attached instance representing the same row
    final ClassMetadata classMetadata = session.getSessionFactory().getClassMetadata(entity.getClass());
    final Serializable identifier = classMetadata.getIdentifier(entity, (SessionImplementor) session);

    final Object sessionEntity = session.load(entity.getClass(), identifier);
    // override changes, last call to update wins
    if (sessionEntity != null)
        session.evict(sessionEntity);
    session.update(entity);
}

Esse é um dos poucos aspectos do .Net EntityFramework que eu gosto, as diferentes opções de anexação relacionadas às entidades alteradas e suas propriedades.

djmj
fonte
3

Eu vim com uma solução para "atualizar" um objeto do armazenamento de persistência que será responsável por outros objetos que já podem estar anexados à sessão:

public void refreshDetached(T entity, Long id)
{
    // Check for any OTHER instances already attached to the session since
    // refresh will not work if there are any.
    T attached = (T) session.load(getPersistentClass(), id);
    if (attached != entity)
    {
        session.evict(attached);
        session.lock(entity, LockMode.NONE);
    }
    session.refresh(entity);
}
WhoopP
fonte
2

Desculpe, mas não consigo adicionar comentários (ainda?).

Usando o Hibernate 3.5.0-Final

Considerando que o Session#lockmétodo esta depreciado, o javadoc não sugerir o uso Session#buildLockRequest(LockOptions)#lock(entity)e se você se certificar de suas associações têm cascade=lock, o carregamento lento não é um problema também.

Então, meu método de anexação se parece um pouco com

MyEntity attach(MyEntity entity) {
    if(getSession().contains(entity)) return entity;
    getSession().buildLockRequest(LockOptions.NONE).lock(entity);
    return entity;

Os testes iniciais sugerem que funciona um tratamento.

Gwaptiva
fonte
2

Talvez ele se comporte um pouco diferente no Eclipselink. Para reconectar objetos desanexados sem obter dados obsoletos, geralmente:

Object obj = em.find(obj.getClass(), id);

e, opcionalmente, uma segunda etapa (para invalidar caches):

em.refresh(obj)
Hartmut P.
fonte
1

tente getHibernateTemplate (). replicate (entity, ReplicationMode.LATEST_VERSION)

Pavitar Singh
fonte
1

No post original, existem dois métodos, update(obj)e merge(obj)que são mencionados ao trabalho, mas em circunstâncias opostas. Se isso for realmente verdade, por que não testar para ver se o objeto já está na sessão primeiro e depois ligar update(obj)se estiver, caso contrário, chame merge(obj).

O teste para a existência na sessão é session.contains(obj). Portanto, acho que o seguinte pseudocódigo funcionaria:

if (session.contains(obj))
{
    session.update(obj);
}
else 
{
    session.merge(obj);
}
John DeRegnaucourt
fonte
2
As verificações contém () são comparadas por referência, mas as funções de hibernação funcionam pelo ID do banco de dados. session.merge nunca será chamado em seu código.
Verena Haunschmid
1

para anexar novamente esse objeto, você deve usar merge ();

esse método aceita no parâmetro sua entidade desanexada e retorna uma entidade será anexada e recarregada do banco de dados.

Example :
    Lot objAttach = em.merge(oldObjDetached);
    objAttach.setEtat(...);
    em.persist(objAttach);
Ryuku
fonte
0

chamar primeiro merge () (para atualizar a instância persistente) e depois bloquear (LockMode.NONE) (para anexar a instância atual, não a retornada por merge ()) parece funcionar em alguns casos de uso.

cheesus
fonte
0

Propriedade hibernate.allow_refresh_detached_entity fez o truque para mim. Mas é uma regra geral, portanto, não é muito adequado se você deseja fazê-lo apenas em alguns casos. Espero que ajude.

Testado no Hibernate 5.4.9

SessionFactoryOptionsBuilder

Radeck
fonte
-6
try getHibernateTemplate().saveOrUpdate()
Ben Hammond
fonte