¿Cuál es la forma correcta de volver a adjuntar objetos separados en Hibernate?


186

Tengo una situación en la que necesito volver a adjuntar objetos separados a una sesión de hibernación, aunque un objeto de la misma identidad PUEDE ya existir en la sesión, lo que causará errores.

En este momento, puedo hacer una de dos cosas.

  1. getHibernateTemplate().update( obj ) Esto funciona si y solo si un objeto no existe en la sesión de hibernación. Se lanzan excepciones indicando que un objeto con el identificador dado ya existe en la sesión cuando lo necesito más tarde.

  2. getHibernateTemplate().merge( obj ) Esto funciona si y solo si existe un objeto en la sesión de hibernación. Se producen excepciones cuando necesito que el objeto esté en una sesión posterior si uso esto.

Dados estos dos escenarios, ¿cómo puedo adjuntar sesiones genéricamente a los objetos? No quiero usar excepciones para controlar el flujo de la solución de este problema, ya que debe haber una solución más elegante ...

Respuestas:


181

Por lo tanto, parece que no hay forma de volver a conectar una entidad separada obsoleta en JPA.

merge() empujará el estado obsoleto a la base de datos y sobrescribirá cualquier actualización intermedia.

refresh() no se puede invocar en una entidad separada.

lock() no se puede invocar en una entidad separada, e incluso si se pudiera, y se volvió a conectar la entidad, llamando 'lock' con el argumento 'LockMode.NONE', lo que implica que está bloqueando, pero no bloqueando, es la pieza más intuitiva del diseño de API Que he visto

Entonces estás atrapado. Hay un detach()método, pero no attach()o reattach(). Un paso obvio en el ciclo de vida del objeto no está disponible para usted.

A juzgar por la cantidad de preguntas similares sobre JPA, parece que incluso si JPA afirma tener un modelo coherente, ciertamente no coincide con el modelo mental de la mayoría de los programadores, que han sido maldecidos para perder muchas horas tratando de entender cómo conseguirlo. JPA para hacer las cosas más simples y terminar con código de administración de caché en todas sus aplicaciones.

Parece que la única forma de hacerlo es descartar su entidad separada obsoleta y hacer una consulta de búsqueda con la misma identificación, que golpeará el L2 o el DB.

Mik


1
Me pregunto si hay una razón por la cual la especificación JPA no permite refresh()en entidades separadas. Mirando a través de la especificación 2.0 no veo ninguna justificación; solo que no está permitido.
FGreg

11
Esto definitivamente NO es exacto. De 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.se puede encontrar más en la sección 9.3.2
cwash

Los objetos persistentes funcionan muy bien, el indicador sucio se establece en función del delta entre la carga inicial y los valores en el momento de descarga (). Los objetos separados necesitan, y actualmente no tienen esta funcionalidad. La forma de hibernar para hacerlo es agregar un hash / id adicional para los objetos separados. Y mantenga una instantánea del último estado del objeto separado disponible, tal como lo hacen para los objetos persistentes. Para que puedan aprovechar todo ese código existente y hacerlo funcionar para objetos separados. De esta manera, como señaló @mikhailfranco, no "empujaremos el estado obsoleto a la base de datos y sobrescribiremos las actualizaciones intermedias"
tom

2
De acuerdo con Hibernate, javadoc (pero no JPA), lock(LockMode.NONE)de hecho se puede invocar en un objeto transitorio, y vuelve a conectar la entidad a la sesión. Ver stackoverflow.com/a/3683370/14379
seanf

lock no funcionó para mí: java.lang.IllegalArgumentException: entidad que no está en el contexto de persistencia en org.hibernate.internal.SessionImpl.lock (SessionImpl.java:3491) en org.hibernate.internal.SessionImpl.lock (SessionImpl. java: 3482) en com.github.vok.framework.DisableTransactionControlEMDelegate.lock (DB.kt)
Martin Vysny

32

Todas estas respuestas pierden una distinción importante. update () se usa para (re) adjuntar su gráfico de objeto a una sesión. Los objetos que le pasas son los que se gestionan.

merge () en realidad no es una API (re) adjunta. Observe que merge () tiene un valor de retorno? Esto se debe a que le devuelve el gráfico administrado, que puede no ser el gráfico que le pasó. merge () es una API JPA y su comportamiento se rige por la especificación JPA. Si el objeto que pasa a merge () ya está administrado (ya asociado con la sesión), entonces ese es el gráfico con el que trabaja Hibernate; el objeto pasado es el mismo objeto devuelto por merge (). Sin embargo, si el objeto que pasa en merge () está separado, Hibernate crea un nuevo gráfico de objeto que se administra y copia el estado de su gráfico separado en el nuevo gráfico administrado. Nuevamente, todo esto está dictado y gobernado por la especificación JPA.

En términos de una estrategia genérica para "asegurarse de que esta entidad se administre, o hacer que se administre", depende de si desea tener en cuenta los datos aún no insertados. Suponiendo que lo haga, use algo como

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

Observe que usé saveOrUpdate () en lugar de update (). Si no desea que los datos aún no insertados se manejen aquí, use update () en su lugar ...


3
Esta es la respuesta correcta a esta pregunta: caso cerrado.
cwash

2
Session.contains(Object)controles por referencia. Si ya hay otra entidad que representa la misma fila en la sesión y pasa una instancia separada, obtendrá una excepción.
djmj

Como Session.contains(Object)comprobaciones por referencia, si hay otra entidad que representa la misma fila en la sesión, devolverá falso y lo actualizará.
AxelWass

19

Respuesta no diplomática: Probablemente esté buscando un contexto de persistencia extendido. Esta es una de las principales razones detrás de Seam Framework ... Si tiene dificultades para usar Hibernate en Spring en particular, consulte esta pieza de los documentos de Seam.

Respuesta diplomática: Esto se describe en los documentos de Hibernate . Si necesita más aclaraciones, eche un vistazo a la Sección 9.3.2 de Java Persistence with Hibernate llamada "Trabajar con objetos separados". Yo fuertemente recomiendo consigo este libro, si está haciendo algo más que CRUD con Hibernate.


55
De seamframework.org : "Red Hat ha detenido el desarrollo activo de Seam 3". El enlace "este documento de Seam" también está muerto.
obispo

14

Si está seguro de que su entidad no ha sido modificada (o si acepta que se perderá alguna modificación), puede volver a adjuntarla a la sesión con bloqueo.

session.lock(entity, LockMode.NONE);

No bloqueará nada, pero obtendrá la entidad del caché de la sesión o (si no se encuentra allí) la leerá desde la base de datos.

Es muy útil para evitar LazyInitException cuando se navega por relaciones de entidades "antiguas" (de la HttpSession, por ejemplo). Primero "vuelve a adjuntar" la entidad.

El uso de get también puede funcionar, excepto cuando obtiene la herencia asignada (que ya arrojará una excepción en getId ()).

entity = session.get(entity.getClass(), entity.getId());

2
Me gustaría volver a asociar una entidad con la sesión. Desafortunadamente, Session.lock(entity, LockMode.NONE)falla con la excepción que dice: no se pudo reasociar la colección transitoria no inicializada. ¿Cómo puede superar esto?
dma_k

1
De hecho, no tenía toda la razón. El uso de lock () vuelve a asociar su entidad, pero no las otras entidades vinculadas a ella. Entonces, si hace entity.getOtherEntity (). GetYetAnotherEntity (), puede tener una excepción LazyInit. La única forma en que sé superar eso es usar find. entity = em.find (entity.getClass (), entity.getId ();
John Rizzo

No hay un Session.find()método API. Quizás te refieres Session.load(Object object, Serializable id).
dma_k

11

Como esta es una pregunta muy común, escribí este artículo , en el que se basa esta respuesta.

Estados de la entidad

JPA define los siguientes estados de entidad:

Nuevo (transitorio)

Se considera que un objeto recién creado que nunca se ha asociado con un Hibernate Session(también conocido como Persistence Context) y que no está asignado a ninguna fila de la tabla de la base de datos está en el estado Nuevo (Transitorio).

Para ser persistente, necesitamos llamar explícitamente el EntityManager#persistmétodo o hacer uso del mecanismo de persistencia transitiva.

Persistente (Gestionado)

Una entidad persistente se ha asociado con una fila de la tabla de la base de datos y está siendo administrada por el Contexto de persistencia actualmente en ejecución. Cualquier cambio realizado en dicha entidad se detectará y propagará a la base de datos (durante el tiempo de descarga de la sesión).

Con Hibernate, ya no tenemos que ejecutar instrucciones INSERT / UPDATE / DELETE. Hibernate emplea un estilo de trabajo transaccional de reescritura y los cambios se sincronizan en el último momento responsable, durante el Sessiontiempo de descarga actual .

Separado

Una vez que el contexto de persistencia actualmente en ejecución se cierra, todas las entidades administradas previamente se separan. Los cambios sucesivos ya no se rastrearán y no se realizará una sincronización automática de la base de datos.

Transiciones de estado de entidad

Puede cambiar el estado de la entidad utilizando varios métodos definidos por la EntityManagerinterfaz.

Para comprender mejor las transiciones de estado de la entidad JPA, considere el siguiente diagrama:

Transiciones de estado de entidad JPA

Al usar JPA, para volver a asociar una entidad separada a una activa EntityManager, puede usar la operación de fusión .

Al usar la API nativa de Hibernate, además de merge, puede volver a conectar una entidad separada a una sesión de Hibernate activa utilizando los métodos de actualización, como se demuestra en el siguiente diagrama:

Transiciones de estado de entidad hibernada

Fusionar una entidad separada

La fusión va a copiar el estado de la entidad separada (fuente) en una instancia de entidad administrada (destino).

Considere que hemos persistido en la siguiente Bookentidad, y ahora la entidad está separada ya EntityManagerque la que se usó para persistir la entidad se cerró:

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

    entityManager.persist(book);

    return book;
});

Mientras la entidad está en estado separado, la modificamos de la siguiente manera:

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

Ahora, queremos propagar los cambios a la base de datos, para que podamos llamar al mergemétodo:

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

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

    assertFalse(book == _book);
});

E Hibernate ejecutará las siguientes instrucciones 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

Si la entidad fusionada no tiene equivalente en la actual EntityManager, se obtendrá una instantánea de entidad nueva de la base de datos.

Una vez que hay una entidad administrada, JPA copia el estado de la entidad separada en la que está administrada actualmente, y durante el Contexto de persistenciaflush , se generará una ACTUALIZACIÓN si el mecanismo de verificación sucio encuentra que la entidad administrada ha cambiado.

Entonces, cuando se usa merge, la instancia del objeto separado continuará siendo separada incluso después de la operación de fusión.

Volver a unir una entidad separada

Hibernate, pero no JPA admite la reconexión a través del updatemétodo.

Un Hibernate Sessionsolo puede asociar un objeto de entidad para una fila de base de datos dada. Esto se debe a que el contexto de persistencia actúa como un caché en memoria (caché de primer nivel) y solo un valor (entidad) está asociado con una clave dada (tipo de entidad e identificador de base de datos).

Una entidad se puede volver a unir solo si no hay otro objeto JVM (que coincida con la misma fila de la base de datos) ya asociado con la Hibernación actual Session.

Teniendo en cuenta que hemos persistido en la Bookentidad y que la hemos modificado cuando la Bookentidad estaba en estado separado:

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 volver a conectar la entidad separada de esta manera:

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

    session.update(_book);

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

E Hibernate ejecutará la siguiente instrucción SQL:

-- Updating the Book entity

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

El updatemétodo requiere que unwrapla EntityManagerde una hibernación Session.

A diferencia merge, la entidad separada proporcionada se volverá a asociar con el contexto de persistencia actual y se programa una ACTUALIZACIÓN durante el vaciado, ya sea que la entidad se haya modificado o no.

Para evitar esto, puede usar la @SelectBeforeUpdateanotación Hibernate que activará una instrucción SELECT que obtuvo el estado cargado que luego utiliza el mecanismo de verificación sucio.

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

    //Code omitted for brevity
}

Cuidado con la excepción de objeto único

Un problema con el que puede ocurrir updatees si el contexto de persistencia ya contiene una referencia de entidad con la misma identificación y del mismo tipo que en el siguiente ejemplo:

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
    );
}

Ahora, al ejecutar el caso de prueba anterior, Hibernate arrojará un NonUniqueObjectExceptionporque el segundo EntityManagerya contiene una Bookentidad con el mismo identificador al que pasamos update, y el Contexto de persistencia no puede contener dos representaciones de la misma entidad.

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)

Conclusión

Es mergepreferible el método si está utilizando un bloqueo optimista, ya que le permite evitar actualizaciones perdidas. Para obtener más detalles sobre este tema, consulte este artículo .

El updatees bueno para actualizaciones por lotes, ya que puede evitar que la instrucción SELECT adicional generado por la mergeoperación, por lo tanto, reduciendo el tiempo de ejecución por lotes de actualización.


Buena respuesta. Sin @SelectBeforeUpdateembargo, me preguntaba acerca de la anotación. ¿Cuándo se activa la selección? Al llamar update, justo antes de enjuagar o realmente no importa (¿podría importar si hibernate recupera todas las entidades anotadas en una llamada antes de enjuagar)?
Andronicus

La @SelectBeforeUpdateactiva la SELECT durante el contexto de persistencia flushoperación. Consulte el getDatabaseSnapshotmétodo enDefaultFlushEntityEventListener para más detalles.
Vlad Mihalcea

10

Regresé a JavaDoc org.hibernate.Sessiony encontré lo siguiente:

Las instancias transitorias pueden hacerse persistentes llamando save(), persist()o saveOrUpdate(). Las instancias persistentes pueden hacerse transitorias llamando delete(). Cualquier instancia devuelta por un método get()o load()es persistente. Instancias separadas pueden hacerse persistente llamando update(), saveOrUpdate(), lock()o replicate(). El estado de una instancia transitoria o separada también puede hacerse persistente como una nueva instancia persistente mediante una llamada merge().

Por lo tanto update(), saveOrUpdate(), lock(), replicate()y merge()son las opciones propuestas.

update(): Lanzará una excepción si hay una instancia persistente con el mismo identificador.

saveOrUpdate(): Guardar o actualizar

lock(): Obsoleto

replicate(): Persiste el estado de la instancia separada dada, reutilizando el valor del identificador actual.

merge(): Devuelve un objeto persistente con el mismo identificador. La instancia dada no se asocia con la sesión.

Por lo tanto, lock()no debe usarse de inmediato y, en función del requisito funcional, se puede elegir uno o más de ellos.


7

Lo hice de esa manera en C # con NHibernate, pero debería funcionar de la misma manera en 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();
        }
    }
}

Se llamó a First Lock en cada objeto porque Contains siempre fue falso. El problema es que NHibernate compara objetos por identificación y tipo de base de datos. Contiene utiliza el equalsmétodo, que se compara por referencia si no se sobrescribe. Con ese equalsmétodo funciona sin excepciones:

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;
}

4

Session.contains(Object obj) comprueba la referencia y no detectará una instancia diferente que represente la misma fila y que ya esté asociada a ella.

Aquí mi solución genérica para Entidades con una propiedad de identificador.

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);
}

Este es uno de los pocos aspectos de .Net EntityFramework que me gustan, las diferentes opciones de conexión con respecto a las entidades modificadas y sus propiedades.


3

Se me ocurrió una solución para "actualizar" un objeto del almacén de persistencia que tendrá en cuenta otros objetos que ya pueden estar conectados a la sesión:

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);
}

2

Lo sentimos, parece que no puedo agregar comentarios (¿todavía?).

Usando Hibernate 3.5.0-Final

Mientras que el Session#lockmétodo de esta obsoleto, el Javadoc no sugieren el uso Session#buildLockRequest(LockOptions)#lock(entity)y si usted asegurarse de que sus asociaciones tienen cascade=lock, los perezosos de carga no es un problema tampoco.

Entonces, mi método de conexión se parece un poco

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

Las pruebas iniciales sugieren que funciona de maravilla.


2

Quizás se comporta ligeramente diferente en Eclipselink. Para volver a adjuntar objetos separados sin obtener datos obsoletos, generalmente hago:

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

y como opcional un segundo paso (para invalidar las cachés):

em.refresh(obj)

1

intente getHibernateTemplate (). replicate (entidad, ReplicationMode.LATEST_VERSION)


1

En la publicación original, hay dos métodos, update(obj)y merge(obj)se menciona que funcionan, pero en circunstancias opuestas. Si esto es realmente cierto, entonces, ¿por qué no probar para ver si el objeto ya está en la sesión primero y luego llamar update(obj)si es así? De lo contrario, llame merge(obj).

La prueba de existencia en la sesión es session.contains(obj). Por lo tanto, creo que el siguiente pseudocódigo funcionaría:

if (session.contains(obj))
{
    session.update(obj);
}
else 
{
    session.merge(obj);
}

2
contiene () las comprobaciones se comparan por referencia, pero las funciones de hibernación funcionan por ID de base de datos. session.merge nunca será llamado en su código.
Verena Haunschmid

1

para volver a conectar este objeto, debe usar merge ();

este método acepta en el parámetro que su entidad se separó y devuelve una entidad que se adjuntará y volverá a cargar desde la base de datos.

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

0

llamar primero merge () (para actualizar la instancia persistente), luego bloquear (LockMode.NONE) (para adjuntar la instancia actual, no la que devuelve merge ()) parece funcionar para algunos casos de uso.


0

La propiedad hibernate.allow_refresh_detached_entityhizo el truco para mí. Pero es una regla general, por lo que no es muy adecuado si desea hacerlo solo en algunos casos. Espero que ayude.

Probado en Hibernate 5.4.9

SessionFactoryOptionsBuilder



Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.