Java - JPA - anotación @Version

118

¿Cómo funciona la @Versionanotación en JPA?

Encontré varias respuestas cuyo extracto es el siguiente:

JPA usa un campo de versión en sus entidades para detectar modificaciones simultáneas en el mismo registro de almacén de datos. Cuando el tiempo de ejecución de JPA detecta un intento de modificar simultáneamente el mismo registro, lanza una excepción a la transacción que intenta confirmarse en último lugar.

Pero todavía no estoy seguro de cómo funciona.


También a partir de las siguientes líneas:

Debe considerar los campos de versión inmutables. Cambiar el valor del campo tiene resultados indefinidos.

¿Significa que deberíamos declarar nuestro campo de versión como final?

Yatendra Goel
fuente
1
Todo lo que hace es Actualización / comprobación de la versión en cada consulta de actualización: UPDATE myentity SET mycolumn = 'new value', version = version + 1 WHERE version = [old.version]. Si alguien actualizó el registro, old.versionya no coincidirá con el de la base de datos y la cláusula where evitará que se realice la actualización. Las 'filas actualizadas' serán 0, que JPA puede detectar para concluir que ocurrió una modificación concurrente.
Stijn de Witt

Respuestas:

189

¿Pero todavía no estoy seguro de cómo funciona?

Digamos que una entidad MyEntitytiene una versionpropiedad anotada :

@Entity
public class MyEntity implements Serializable {    

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @Version
    private Long version;

    //...
}

En la actualización, el campo anotado con @Versionse incrementará y se agregará a la WHEREcláusula, algo como esto:

UPDATE MYENTITY SET ..., VERSION = VERSION + 1 WHERE ((ID = ?) AND (VERSION = ?))

Si la WHEREcláusula no coincide con un registro (porque la misma entidad ya ha sido actualizada por otro hilo), el proveedor de persistencia arrojará un OptimisticLockException.

¿Significa que deberíamos declarar nuestro campo de versión como final?

No, pero podría considerar hacer que el colocador esté protegido, ya que se supone que no debe llamarlo.

Pascal Thivent
fuente
5
Obtuve una excepción de puntero nulo con este fragmento de código, porque Long se inicializa como nulo, probablemente debería inicializarse explícitamente con 0L.
Markus
2
No confíe en el desempaquetado automático longo simplemente en llamarlo longValue(). Necesita nullcontroles explícitos . Inicializarlo explícitamente usted mismo no lo haría, ya que se supone que este campo lo administra el proveedor de ORM. Creo que se establece 0Len la primera inserción en la base de datos, por lo que si está configurado en nullesto, le indica que este registro aún no se ha conservado.
Stijn de Witt
¿Evitará esto que se cree una entidad (que se intente) crear más de una vez? Me refiero a que supongo que un mensaje de tema JMS desencadena la creación de una entidad y hay varias instancias de una aplicación que escucha el mensaje. Solo quiero evitar el error de violación de restricción única.
Manu
Lo siento, me doy cuenta de que este es un hilo antiguo, pero ¿@Version también tiene en cuenta la caché sucia en entornos agrupados?
Shawn Eion Smith
3
Si usa Spring Data JPA, puede ser mejor usar un contenedor en Longlugar de una primitiva longpara el @Versioncampo, porque SimpleJpaRepository.save(entity)probablemente tratará un campo de versión nula como un indicador de que la entidad es nueva. Si la entidad es nueva (como indica que la versión es nula), Spring llamará em.persist(entity). Pero si la versión tiene un valor, llamará em.merge(entity). Esta es también la razón por la que un campo de versión que se declara como un tipo de contenedor debe dejarse sin inicializar.
rdguam
33

Aunque la respuesta de @Pascal es perfectamente válida, desde mi experiencia, encuentro que el siguiente código es útil para lograr un bloqueo optimista:

@Entity
public class MyEntity implements Serializable {    
    // ...

    @Version
    @Column(name = "optlock", columnDefinition = "integer DEFAULT 0", nullable = false)
    private long version = 0L;

    // ...
}

¿Por qué? Porque:

  1. El bloqueo optimista no funcionará si el campo anotado con @Versionse establece accidentalmente en null.
  2. Dado que este campo especial no es necesariamente una versión comercial del objeto, para evitar una confusión, prefiero nombrar dicho campo como algo parecido optlocka version.

Primer punto no importa si utiliza la aplicación única APP para insertar datos en la base de datos, como proveedor JPA hará cumplir 0de @versioncampo durante la creación. Pero casi siempre también se utilizan sentencias SQL simples (al menos durante las pruebas unitarias y de integración).

G. Demecki
fuente
Lo llamo u_lmod(usuario modificado por última vez) donde el usuario puede ser un humano o un proceso / entidad / aplicación definida (automatizada) (por lo que almacenar adicionalmente también u_lmod_idpodría tener sentido). (En mi opinión, es un metaatributo empresarial muy básico). Por lo general, almacena su valor como FECHA (HORA) (es decir, información sobre el año ... milis) en UTC (si es importante almacenar la "ubicación" de la zona horaria del editor, entonces con la zona horaria). además, es muy útil para escenarios de sincronización DWH.
Andreas Dietrich
7
Creo que bigint en lugar de un entero debería usarse en el tipo db para que coincida con un tipo largo de Java.
Dmitry
Buen punto Dmitry, gracias, aunque cuando se trata de versionar en mi humilde opinión, realmente no importa.
G. Demecki
9

Cada vez que se actualiza una entidad en la base de datos, el campo de versión aumentará en uno. Cada operación que actualice la entidad en la base de datos se agregará WHERE version = VERSION_THAT_WAS_LOADED_FROM_DATABASEa su consulta.

Al verificar las filas afectadas de su operación, el marco jpa puede asegurarse de que no haya una modificación simultánea entre la carga y la persistencia de su entidad porque la consulta no encontraría su entidad en la base de datos cuando su número de versión se ha incrementado entre la carga y la persistencia.

Stefanglase
fuente
No a través de JPAUpdateQuery, no lo hace. Entonces, "Cada operación que actualice la entidad en la base de datos habrá agregado WHERE version = VERSION_THAT_WAS_LOADED_FROM_DATABASE a su consulta" no es verdadera.
Pedro Borges
2

Versión utilizada para garantizar que solo se actualice una vez por vez. El proveedor de JPA verificará la versión, si la versión esperada ya aumentó, entonces alguien más ya actualizó la entidad, por lo que se lanzará una excepción.

Entonces, actualizar el valor de la entidad sería más seguro, más optimista.

Si el valor cambia con frecuencia, entonces podría considerar no usar el campo de versión. Por ejemplo, "una entidad que tiene un campo contador, que aumentará cada vez que se acceda a una página web".

uudashr
fuente
0

Solo agrego un poco más de información.

JPA administra la versión bajo el capó por usted, sin embargo, no lo hace cuando actualiza su registro a través de JPAUpdateClause, en tales casos, debe agregar manualmente el incremento de la versión a la consulta.

Lo mismo se puede decir sobre la actualización a través de JPQL, es decir, no un simple cambio en la entidad, sino un comando de actualización de la base de datos, incluso si se hace mediante hibernación.

Pedro

Pedro Borges
fuente
3
¿Qué es JPAUpdateClause?
Karl Richter
Buena pregunta, la respuesta simple es: mal ejemplo :) JPAUpdateClause es específico de QueryDSL, piense en ejecutar una actualización con JPQL en lugar de simplemente afectar el estado de una entidad en el código.
Pedro Borges