Buscar
Social
Ofertas laborales ES

Foro sobre Java EE > OptimisticLockException inesperada

En una aplicación Java SE con Derby y JPA 2.1 (EclipseLink v2.5), me estoy encontrando una excepción de bloqueo que no entiendo. Se produce en un formulario Swing con un JTable cuyo modelo lee en memoria datos de una tabla. Cuando el usuario hace clic en una de las filas de la tabla, los valores se muestran en unos campos JTextField debajo de la tabla para permitir su edición y, seguidamente, el usuario puede pulsar un botón Save para guardarlos (también podría eliminarlos o pasar a otra fila diferente y descartar los cambios).

La tabla se llena con un método así cada vez que cambia otra selección en el formulario, lo cual es bastante frecuente (hablamos de una especie de diccionario inglés-español; el usuario selecciona una entrada de la lista de términos en inglés, obtiene en la tabla conflictiva las traducciones aprobadas y puede modificarlas, eliminarlas o añadir más):


TypedQuery<GlsTranslation> glstQuery = entityManager.createNamedQuery("GlsTranslation.findByEntryAndLocale",
GlsTranslation.class);
glosTranslationTableModel.clearAll();

glstQuery.setParameter("glseid", ge); // Una clave externa de la tabla
glstQuery.setParameter("l10nid", l); // Otra clave externa;
// Ambas reducen los resultados a unas pocas filas (entre 0 y 5, normalmente)
glosTranslationTableModel.addAll(glstQuery.getResultList());
clearTranslationDetailFields();
}

Y cuando se pulsa el botón Save tras modificar datos de la fila seleccionada de la tabla, se ejecuta este otro:


int index = glstTable.convertRowIndexToModel(glstTable.getSelectedRow());

// Obtenemos una referencia a la entidad que vamos a modificar desde el modelo,
GlsTranslation gt = glosTranslationTableModel.getElement(index);
// Nos aseguramos de que contenga lo más reciente en la base de datos
entityManager.refresh(gt);
// Modificamos...
gt.setValue(glstValueField.getText());
gt.setComment(glstCommentTextArea.getText());
gt.setLastUpdate(new Date());
try {
// Y guardamos; aquí se da la excepción
entityManager.getTransaction().commit();
// Estamos en una transacción abierta permanentemente;
// la cerramos al hacer commit y abrimos una nueva
entityManager.getTransaction().begin();
glosTranslationTableModel.fireTableRowsUpdated(index, index);
} catch (Exception ex) {
Logger.getLogger(GlsEntryGuiManager.class.getName()).log(Level.SEVERE, null, ex);
}

Como veis (si es que no estoy equivocado), trato de minimizar el tiempo que pasa desde que leo la entidad desde la base de datos hasta que aplico los cambios. De todas formas:

1) La aplicación es Java SE con un Derby Embedded, por lo que no hay más usuarios concurrentes

2) El error se da con un solo formulario abierto ni tareas de fondo, por lo que no hay concurrencia interna en la aplicación. No hay más que un EntityManager activo cuando obtengo el error.

El error es:


ADVERTENCIA:
Local Exception Stack:
Exception [EclipseLink-5006] (Eclipse Persistence Services - 2.5.0.v20130507-3faac2b): org.eclipse.persistence.exceptions.OptimisticLockException
Exception Description: The object [glossarymanager.model.GlsTranslation[ id=116 ]] cannot be updated because it has changed or been deleted since it was last read.
Class> net.localizethat.glossarymanager.model.GlsTranslation Primary Key> 116
at org.eclipse.persistence.exceptions.OptimisticLockException.objectChangedSinceLastReadWhenUpdating(OptimisticLockException.java:144)
at org.eclipse.persistence.descriptors.VersionLockingPolicy.validateUpdate(VersionLockingPolicy.java:790)
(...)
at org.eclipse.persistence.internal.jpa.transaction.EntityTransactionImpl.commit(EntityTransactionImpl.java:132)
at net.localizethat.glossarymanager.gui.GlsEntryGuiManager.saveGlstButtonActionPerformed(GlsEntryGuiManager.java:777)

La entidad tiene un campo de versión correctamente anotado:


@Entity
@Table(name = "APP.GLSTRANSLATION", uniqueConstraints = {
@UniqueConstraint(columnNames = {"GLSTVALUE"})})
@XmlRootElement
@NamedQuery(name = "GlsTranslation.findByEntryAndLocale", query = "SELECT gt FROM GlsTranslation gt WHERE gt.glseId = :glseid AND gt.l10nId = :l10nid"),
public class GlsTranslation implements Serializable {
private static final long serialVersionUID = 1L;
@TableGenerator(name="GLSTRANSLATION", schema="APP", table="COUNTERS",
pkColumnName="ENTITY", valueColumnName="VALUE", allocationSize = 5)
@Id
@GeneratedValue(strategy= GenerationType.TABLE, generator="GLSTRANSLATION")
@Basic(optional = false)
@Column(name = "ID", nullable = false)
private Integer id;
(...)
@Column(name = "ENTITYVERSION")
@Version
private int entityVersion;

Estoy totalmente perdido, lo reconozco. Lo que he leído sugiere que lo estoy haciendo bien, pero imagino que estoy omitiendo algún detalle. Agradezco cualquier pista. :)

Gracias por anticipado.

septiembre 8, 2014 | Registered Commenterrickiees

Olvidé contestar a esto diciendo que encontré el problema, por si le sirve a alguien.

Había dos motivos. El primero lo encontré en StackOverflow.com, de refilón. Resulta que la propiedad anotada con @Version la añadí a posteriori, y aunque la definí como privada, olvidé crear el getter y el setter. :-) Sin ellos, el proveedor de JPA no podía acceder a la propiedad.

El segundo es un pelín más complejo. Como es una aplicación de escritorio supuestamente para distribuir a usuarios finales, al principio de la aplicación se ejecutan métodos de una clase que permiten crear la base de datos en sí, incluyendo su estructura, insertando valores iniciales en algunas tablas, etc. No sólo eso, también permite modificar la estructura si se produce un cambio de versión mediante la ejecución de sucesivos scripts SQL. El script de la versión actual es el que crea los campos entityVersion en las tablas relevantes, y lo hacía bien, pero...

...pero no inicializaba el campo de las filas existentes con un valor entero, por lo que valía NULL y el proveedor internamente debía de intentar hacer algo así como "entityVersion = NULL + 1". Supongo que internamente se producía una excepción de conversión de tipos, pero al ir escalando en la pila de llamadas se convertía a una OptimisticLockException y se producía la confusión completa.

A lo mejor esto de la clase que crea las tablas y las modifica lo haría JPA automáticamente incluso si voy añadiendo campos y se habría encargado de inicializar correctamente el valor de los campos, pero como no estoy seguro preferí hacerlo de manera pedestre y me ha servido para recordar lo fácil que es desesperarse cuando los mensajes de error no son claros. :-)

septiembre 18, 2014 | Registered Commenterrickiees