Issue description
Updating or removing entities with byte array as primary key causes NullPointerException. Tested on Derby and Oracle database using Java 1.8.0_31 and 1.7.0_71 version. All ORM Hibernte 4.3 versions are affected. On 4.2.X everything works as expected.
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
public class DemoEntity implements Serializable {
@Id
@Column
private byte[] id;
@Column
private String name;
public DemoEntity() {
}
public void setId(byte[] id) {
this.id = id;
}
public byte[] getId() {
return id;
}
public void setName(String data) {
this.name = data;
}
public String getName() {
return name;
}
}
Example
Following unit tests demonstrates both issues. At the beginning of each test there are 3 records in database.
/**
* Tries to remove two records from database.
*
* Second remove operation throws NPE. It is marked as expected for
* demonstration purpose.
*/
@Test(expected = NullPointerException.class)
public void testMultipleDeletionsNPE() {
EntityManager em = null;
try {
em = prepareEntityManager();
Query q = em.createQuery("SELECT s FROM DemoEntity s", DemoEntity.class);
List<DemoEntity> results = q.getResultList();
em.remove(results.get(0));
em.remove(results.get(1));
em.getTransaction().commit();
assertEquals(1, q.getResultList().size());
} finally {
closeEntityManager(em);
}
}
java.lang.NullPointerException at org.hibernate.type.AbstractStandardBasicType.compare(AbstractStandardBasicType.java:221) at org.hibernate.action.internal.EntityAction.compareTo(EntityAction.java:171) at org.hibernate.engine.spi.ExecutableList.add(ExecutableList.java:222) at org.hibernate.engine.spi.ActionQueue.addAction(ActionQueue.java:219) at org.hibernate.event.internal.DefaultDeleteEventListener.deleteEntity(DefaultDeleteEventListener.java:299) at org.hibernate.event.internal.DefaultDeleteEventListener.onDelete(DefaultDeleteEventListener.java:160) at org.hibernate.event.internal.DefaultDeleteEventListener.onDelete(DefaultDeleteEventListener.java:73) at org.hibernate.internal.SessionImpl.fireDelete(SessionImpl.java:916) at org.hibernate.internal.SessionImpl.delete(SessionImpl.java:892) at org.hibernate.jpa.spi.AbstractEntityManagerImpl.remove(AbstractEntityManagerImpl.java:1214) at ByteArrayPKTest.testMultipleDeletionsBugged(ByteArrayPKTest.java:103) ...
/**
* Tries to update two records in database.
*
* Throws RollbackException caused by NPE during commit. It is marked as expected for
* demonstration purpose.
*/
@Test(expected = RollbackException.class)
public void testMultipleUpdatesNPE() {
EntityManager em = null;
try {
em = prepareEntityManager();
Query q = em.createQuery("select s from DemoEntity s", DemoEntity.class);
List<DemoEntity> results = q.getResultList();
results.get(0).setName("Different 0");
results.get(1).setName("Different 1");
em.getTransaction().commit();
List<DemoEntity> check = q.getResultList();
assertEquals("Different 0", check.get(0).getName());
assertEquals("Different 1", check.get(1).getName());
} finally {
closeEntityManager(em);
}
}
javax.persistence.RollbackException: Error while committing the transaction at org.hibernate.jpa.internal.TransactionImpl.commit(TransactionImpl.java:94) at ByteArrayPKTest.testMultipleUpdatesBugged(ByteArrayPKTest.java:152) ... Caused by: java.lang.NullPointerException at org.hibernate.type.AbstractStandardBasicType.compare(AbstractStandardBasicType.java:221) at org.hibernate.action.internal.EntityAction.compareTo(EntityAction.java:171) at org.hibernate.engine.spi.ExecutableList.add(ExecutableList.java:222) at org.hibernate.engine.spi.ActionQueue.addAction(ActionQueue.java:237) at org.hibernate.event.internal.DefaultFlushEntityEventListener.scheduleUpdate(DefaultFlushEntityEventListener.java:313) at org.hibernate.event.internal.DefaultFlushEntityEventListener.onFlushEntity(DefaultFlushEntityEventListener.java:160) at org.hibernate.event.internal.AbstractFlushingEventListener.flushEntities(AbstractFlushingEventListener.java:231) at org.hibernate.event.internal.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:102) at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:55) at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1218) at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:421) at org.hibernate.engine.transaction.internal.jdbc.JdbcTransaction.beforeTransactionCommit(JdbcTransaction.java:101) at org.hibernate.engine.transaction.spi.AbstractTransactionImpl.commit(AbstractTransactionImpl.java:177) at org.hibernate.jpa.internal.TransactionImpl.commit(TransactionImpl.java:77)
Problem reason
In delete/update process org.hibernate.engine.spi.ExecutableList collection is used. This is a sorted list and each add operation compares adding object to last added. In this case comparison is performed on EntityAction objects.
...
@Override
public int compareTo(Object other) {
final EntityAction action = (EntityAction) other;
final int roleComparison = entityName.compareTo( action.entityName );
if ( roleComparison != 0 ) {
return roleComparison;
}
else {
return persister.getIdentifierType().compare( id, action.id );
}
}
...
Accordingly, method EntityAction.compareTo compares wrapped primary keys of entities. Byte array is wrapped by PrimitiveByteArrayTypeDescriptor. This class calls its super constructor (AbstractTypeDescriptor) with byte[].class as argument.
...
public PrimitiveByteArrayTypeDescriptor() {
super( byte[].class, ArrayMutabilityPlan.INSTANCE );
}
...
AbstractTypeDescriptor constructor set comparator only if given class implements Comparator interface:
...
protected AbstractTypeDescriptor(Class<T> type, MutabilityPlan<T> mutabilityPlan) {
this.type = type;
this.mutabilityPlan = mutabilityPlan;
this.comparator = Comparable.class.isAssignableFrom( type )
? (Comparator<T>) ComparableComparator.INSTANCE
: null;
}
...
Byte array of course doesn't implement Comparator interface and during comparison of PrimitiveByteArrayTypeDescriptor objects their comparators are null.
Avoiding problem
Issues in both cases can be avoided by explicitly calling em.flush() after each update/delete operation. Flush clears ExecutableList collections and NPE is not thrown. Although this does not solve the problem.
/**
* Removes two records from database.
*
* Flush operation placed between removing each entity prevent NPE throwing.
*/
@Test
public void testMultipleDeletions() {
EntityManager em = null;
try {
em = prepareEntityManager();
Query q = em.createQuery("select s from DemoEntity s", DemoEntity.class);
List<DemoEntity> results = q.getResultList();
em.remove(results.get(0));
em.flush(); em.remove(results.get(1));
em.getTransaction().commit();
assertEquals(1, q.getResultList().size());
} finally {
closeEntityManager(em);
}
}
/**
* Updates two records in database.
*
* Flush operation placed between changing each entity prevents NPE throwing.
*/
@Test
public void testMultipleUpdates() {
EntityManager em = null;
try {
em = prepareEntityManager();
Query q = em.createQuery("select s from DemoEntity s", DemoEntity.class);
List<DemoEntity> results = q.getResultList();
results.get(0).setName("Different 0");
em.flush();
results.get(1).setName("Different 1");
em.getTransaction().commit();
List<DemoEntity> check = q.getResultList();
assertEquals("Different 0", check.get(0).getName());
assertEquals("Different 1", check.get(1).getName());
} finally {
closeEntityManager(em);
}
}
Issue reproduction
In order to reproduce this problem (on 4.3.X version), demonstration project has been attached.
All tests will be successful because undesirable exceptions thrown in testMultipleDeletionsNPE and testMultipleUpdatesNPE are annotated as expected but problem still exists.
In order to verify the proper behavior of version 4.2.X the hibernate-entitymanager version and the jpa provider should be changed e.g:
<persistence ...>
<persistence-unit name="DEMO" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
...
</persistence-unit>
</persistence>
...
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>4.2.17.Final</version>
<dependency>
...
In this case exceptions in testMultipleDeletionsNPE and testMultipleUpdatesNPE will not be thrown and those tests will fail.
|