Die JPA (Java Persistence API) ist seit geraumer Zeit eine der innovativsten Neuerungen in der Enterprise-Java-Welt. Geprägt durch verschiedene Technologien sind viele elegante Lösungen für wiederkehrende Problemstellungen in eine Spezifikation eingeflossen. Hibernate ist eine sehr verbreitete Open-Source-Lösung, die die JSR-220 und JSR-317 implementiert. Eine essentielle Verbesserung von JPA gegenüber J2EE Entity Beans ist die Unterstützung von Vererbung. Zum Bedauern vieler Entwickler existiert jedoch in Bezug auf Vererbung ein schwerwiegender Bug in Hibernate, welcher zwar bereits bekannt ist, aber leider nicht korrigiert wurde, obwohl die Lösung dafür bereits existiert.
Die Manipulation von Persistent Entities ist generell ohne Weiteres möglich. Problematisch sind Update/Delete-Abfragen, welche in der JP-QL (Java Persistence Query Language) formuliert sind. Datensatzmanipulationen per QL sind z.B. dann notwendig, wenn man mit der Datenbankzeit arbeiten möchte.
Szenario
Das folgende Beispiel soll einen Sachverhalt darstellen, in dem Datensätze nicht gelöscht, sondern nur als ungültig markiert werden dürfen. Das dient dem Erhalt der Historie und ist in vielen Geschäftsbereichen gesetzliche Vorschrift. (Man hätte diesen Sachverhalt mit einer @MappedSuperclass performanter lösen können, das Beispiel dient lediglich dem Verständnis.)
Klasse AbstractUndeletable:
package org.acme.persistence; import java.util.Date; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.Inheritance; import javax.persistence.InheritanceType; import javax.persistence.SequenceGenerator; import javax.persistence.Table; import javax.persistence.Temporal; import javax.persistence.TemporalType; @Entity @Table(name = "UNDELETABLE") @Inheritance(strategy = InheritanceType.JOINED) public abstract class AbstractUndeletable { @SequenceGenerator(name = "UndeletableIdGenerator", sequenceName = "UNDELETABLE_ID_GEN") @GeneratedValue(generator = "UndeletableIdGenerator") @Id @Column(name = "UNDELETABLE_PK") private Long id; @Temporal(TemporalType.TIMESTAMP) @Column(name = "INVALID_SINCE") private Date invalidSince; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public Date getInvalidSince() { return invalidSince; } public void setInvalidSince(Date invalidSince) { this.invalidSince = invalidSince; } }
Klasse MyUndeletable:
package org.acme.persistence; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Table; @Entity @Table(name = "MY_UNDELETABLE") public class MyUndeletable extends AbstractUndeletable { @Column(name = "SOME_PROPERTY", nullable = false) private String someProperty; public String getSomeProperty() { return someProperty; } public void setSomeProperty(String someProperty) { this.someProperty = someProperty; } }
Mit einer Data-Access-EJB werden Zugriffe auf Entitäten abstrahiert.
Schnittstelle DataAccessObject:
package org.acme.ejb; import java.util.List; public interface DataAccessObject { <E> E persist(E entity); <E> E find(Class<E> clazz, Object id); <E> List<? extends E> find(String query, boolean namedQuery); void remove(Object entity); }
Klasse DataAccessObjectBean:
package org.acme.ejb; import java.util.List; import javax.ejb.Local; import javax.ejb.Stateless; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import org.acme.persistence.AbstractUndeletable; @Stateless @Local(DataAccessObject.class) public class DataAccessObjectBean implements DataAccessObject { @PersistenceContext private EntityManager em; public <E> E persist(E entity) { em.persist(entity); return entity; } @Override public <E> E find(Class<E> clazz, Object id) { return em.find(clazz, id); } @SuppressWarnings("unchecked") public <E> List<? extends E> find(String query, boolean namedQuery) { return em.createQuery(query).getResultList(); } public void remove(Object entity) { if (entity instanceof AbstractUndeletable) { String query = "UPDATE %s e SET e.invalidSince = CURRENT_TIMESTAMP WHERE e.id = %d"; query = String.format(query, entity.getClass().getSimpleName(), ((AbstractUndeletable) entity).getId()); em.createQuery(query).executeUpdate(); } else em.remove(entity); } }
Der Fehler
Interessant ist die Methode DataAccessObjectBean.remove(Object), welche prüft, ob die zu löschende Entität vom Typ AbstractUndeletable ist. In einem solchen Fall wird nur ein JP-QL-Update-Statement ausgeführt. Kommt es dazu, wird vom darunterliegenden RDBMS ein Fehler generiert, welcher bei Apache Derby so aussieht:
ERROR 42X03: Column name ‚UNDELETABLE_PK‘ is in more than one table in the FROM list.
Übersetzt heißt das so viel wie: Der Spaltenname UNDELETABLE_PK kann einer in der Abfrage verwendeten Tabelle nicht eindeutig zugeordnet werden.
Als Folge dessen wird von Hibernate eine Exception geworfen:
org.hibernate.exception.SQLGrammarException: could not insert/select ids for bulk update
Der Grund des Fehlers liegt ein der fehlerhaften Generierung eines Statements beim Update-Vorgang:
insert into session.HT_MY_UNDELETABLE select myundeleta0_.UNDELETABLE_PK as UNDELETABLE_PK from MY_UNDELETABLE myundeleta0_ inner join UNDELETABLE myundeleta0_1_ on myundeleta0_.UNDELETABLE_PK=myundeleta0_1_.UNDELETABLE_PK where UNDELETABLE_PK=1
Der Spaltenname UNDELETABLE_PK wird sowohl in der Tabelle UNDELETABLE, sowie auch in MY_UNDELETABLE verwendet, aber in der Where-Klausel keinem der vorhandenen Aliase zugordnet. Als Konsequenz ist der oben erwähnte Fehler generiert worden. Der Issue-Tracker-Eintrag ist hier zu finden.
Gegenmaßnahmen
Um dem entgegen zu wirken kann man entweder sein Datenmodell überarbeiten oder eine alternative Implementierung der Spezifikationen verwenden. Ersteres wird man wahrscheinlich ausschließen wollen, da das Konzept des Datenmodells in einer Enterprise-Applikation in vielen Fällen gut durchdacht ist und in anderen Formen einen erhöhten Komplexitätsgrad ausweist. EclipseLink ist eine Alternative zu Hibernate und in der Version 2.2.0.v20110202-r8913 kann das oben abgebildete Szenario fehlerfrei gehandhabt werden. Die Integration in einen JEE-Applikationsserver ist extrem einfach, mehr dazu hier. Das ist gerade für JBoss interessant, da dieser Server in der Version 5.1.0.GA die fehlerhafte Hibernate 3.3.1.GA Bibliothek mit ausliefert.
Fazit
Das erläuterte Fehlverhalten kann man durchaus als Blocker betrachten, da es die Entwickler zum Umdenken bei der Umsetzung zwingt und man nicht den gewünschten – durch die Spezifikationen als möglich definierten – Weg gehen kann.