Als Object-Relational Mapping (kurz ORM) bezeichnen wir in der Softwareentwicklung die Abbildung der Objekte aus einer objektorientierten Programmiersprache auf ein relationales Datenbankschema. In der Praxis ist diese objektrelationale Abbildung in einer Vielzahl von Projekten zu bewerkstelligen, da die Entscheidung für eine objektorientierte Programmiersprache als auch für ein relationales Datenbanksystem sehr verbreitet ist. Zwischen dem objektorientierten und dem relationalen Datenmodell ergeben sich gewisse Unterschiede, die als Object-Relational Impedance Mismatch bezeichnet werden. Zwei typische Unterschiede zwischen objektorientiertem und relationalem Modell sind die Abbildung von N:N-Beziehungen und die Abbildung von Vererbungsbeziehungen.

Die Abweichungen zwischen den Modellen werden am Beispiel der folgenden Abbildung verdeutlicht.

Einem ORM-Framework kommt nun die Aufgabe zu, die Abbildung zwischen objektorientiertem und relationalem Modell auf Ebene der Programmiersprache durch Annotationen (oder früher durch XML-Konfigurationsdateien) zu ermöglichen. Es ergeben sich allgemein folgende Anforderungen an ein ORM-Framework:

Für viele aktuelle Programmiersprachen existieren ORM-Frameworks, wie z.B. das Entity Framework für .NET-Sprachen, Doctrine für PHP oder SQLAlchemy für Python. In Java gibt es durch JSR 317 und JSR 338 eine standardisierte API für die objektrelationale Abbildung im Package javax.persistence, die sogenannte Java Persistence API (JPA). Für die JPA existieren verschiedene Implementierungen: die Referenzimplementierung EclipseLink, OpenJPA und insbesondere Hibernate. Hibernate ist seit vielen Jahren in der professionellen Praxis am stärksten verbreitet und hat selbst maßgeblich zur Weiterentwicklung der JPA beigetragen. Jede dieser Implementierungen unterstützt die geläufigen relationalen Datenbanksysteme (u.a. MariaDB, MySQL, PostgreSQL, Oracle, Microsoft SQL Server) sowie ihre z.T. spezifischen SQL-Funktionen und -Datentypen. Die Verwendung der JPA kann also dazu beitragen, den Lock-in-Effekt zugunsten des Datenbanksystem-Herstellers, der üblicherweise mit der Entscheidung für ein konkretes Datenbanksystem einhergeht, abzuschwächen.

Wenn die JPA in einem Projekt eingesetzt werden soll, wird als Abhängigkeit eine JPA-Implementierung (z.B. EclipseLink JPA) und ein JDBC-Treiber für das verwendete Datenbanksystem (z.B. MariaDB Java Client) benötigt. JPA baut intern auf den Grundlagen von JDBC (Java Database Connectivity) auf, um dem Entwickler eine API mit höherem Abstraktionsgrad anzubieten. Um die Unterschiede zwischen der JDBC-API und der JPA-API zu verdeutlichen, werden im Folgenden die Grundlagen von JDBC wiederholt.

Java Database Connectivity (JDBC)

JDBC ist die seit langer Zeit standardisierte API für den Zugriff auf relationale Datenbanksysteme aus Java. JDBC ist implementiert im Java-Modul java.sql. Es gibt JDBC-Treiber von nahezu jedem Hersteller eines relationalen Datenbanksystems (z.B. MariaDB, MySQL, PostgreSQL, Oracle, Microsoft SQL Server). Der typische Ablauf eines Zugriffs mittels JDBC auf die Datenbank erfolgt in folgenden Schritten:

Der Verbindungsaufbau zur Datenbank (DB) erfolgt über die Klasse java.sql.DriverManager. Es wird implizit auf dem Java-Klassenpfad des Projekts nach einem passenden JDBC-Treiber für das angegebene Protokoll in der URL zum Verbindungsaufbau gesucht. Das im folgenden Code-Beispiel angegebene Protokoll jdbc:mariadb kann z.B. durch den Treiber org.mariadb.jdbc.Driver bedient werden.

Connection conn = DriverManager.getConnection("jdbc:mariadb://localhost:3306/test?user=foo&password=pass");
conn.setAutoCommit(false);

Der URL können initial Konfigurationsparameter mitgegeben werden, z.B. die Credentials für den DB-User (user=foo&password=pass), eine Zeitzone (serverTimezone=CET), eine Einschränkung auf verschlüsselte Kommunikation mit der DB (useSSL=true) oder der Hinweis, dass auch mehrere SQL-Statements gleichzeitig gesendet werden können (allowMultiQueries=true). Diese Parameter sind z.T. spezifisch für ein konkretes Datenbanksystem und werden in der jeweiligen Hersteller-Dokumentation beschrieben, z.B. URL-Parameter für MariaDB. Wenn die Verbindung zur Datenbank gelingt, wird ein java.sql.Connection-Objekt erzeugt. Mittels des Connection-Objekts können Statement-Objekte erzeugt werden, über die beliebige SQL-Statements ausgeführt werden können. Die SQL-Statements können dynamisch als String zusammengestellt werden, wobei darauf zu achten ist, SQL-Injections zu vermeiden. Insbesondere Anführungszeichen müssen escaped werden, damit über diese nicht der angedachte Zweck eines SQL-Statement verändert werden kann.

Statement stmt = conn.createStatement();
stmt.execute("CREATE TABLE department(id INT KEY AUTO_INCREMENT, label VARCHAR(50))");
stmt.execute("CREATE TABLE person(id INT KEY AUTO_INCREMENT, name VARCHAR(50), dept_id INT)");;

Im nächsten Code-Beispiel werden mehrere SQL-Statements zu einer Transaktion gebündelt, die erst mit dem abschließenden Commit (Zeile 9) beendet wird. Eine Transaktion ist insbesondere dann sinnvoll, wenn ein komplexes Objekt persistiert werden soll, das im relationalen Modell Insert- bzw. Update-Statements für mehrere voneinander abhängige Datensätze in verschiedenen Tabellen verursacht. Im Code-Beispiel soll eine Abteilung (Department) inkl. der ihr zugeordneten Personen gespeichert werden, wobei die jeweiligen Ids automatisch durch das Datenbanksystems vergeben werden.

Department d = new Department("R&D");
d.getStaff().add( new Person("Hendricks") );
d.getStaff().add( new Person("Gilfoyle") );

int deptId = stmt.executeUpdate("INSERT INTO department(label) VALUES('" + d.getLabel() + "')", Statement.RETURN_GENERATED_KEYS);
for (Person p : d.getStaff()) {
	stmt.execute("INSERT INTO person(name, dept_id) VALUES('" + p.getName() + "', " + deptId + ")");
}
conn.commit();

Das folgende Code-Beispiel zeigt ein PreparedStatement, das zum einen bei wiederholter Ausführung effizient ist, da das SQL-Statement vorab geparst und validiert werden kann, und zum anderen mehr Sicherheit bietet, da durch die typsichere Parameterwertzuweisung SQL-Injections verhindert werden.

PreparedStatement prepStmt = conn.prepareStatement(
  "SELECT p.id, p.name FROM person p JOIN department d ON p.dept_id = d.id WHERE d.label = ?");
prepStmt.setString(1, "R&D");
ResultSet res = prepStmt.executeQuery();

Über das zurückgegebene ResultSet einer SQL-Anfrage kann iteriert werden, um die einzelnen Datensätze zu verarbeiten. Die Werte aus dem ResultSet werden wieder in einer objektorientierten Datenstruktur zusammengeführt – wie im folgenden Code-Beispiel anhand der Klasse Person (Zeilen 2-3) exemplarisch gezeigt wird. Bei einem großen Projekt mit einer Vielzahl von Klassen ist dieses Mapping zwischen objektorientiertem und relationalem Modell mit viel Aufwand verbunden. Ein ORM-Framework automatisiert diesen Schritt, indem es intern zunächst leere Objekte instantiiert (Default-Konstruktur erforderlich) und deren Attribute dann mittels der Reflection API befüllt.

while (res.next()) {
	Person p = new Person(res.getString("name"));
	p.setId(res.getInt("id"));
	System.out.println(">>> " + p.getId() + ": " + p.getName());
}

ORM mit der Java Persistence API (JPA)

Das Mapping mittels Annotationen zwischen objektorientiertem und relationalem Modell soll anhand der beiden Beispielklassen Person und Department erklärt werden, die über eine bidirektionale Assoziation (Attribut Department department in Klasse Person ↔ Attribut List<Person> staff in Klasse Department) und eine unidirektionale Assoziation (Attribut Person lead in Klasse Department) miteinander verbunden sind. Sämtliche Annotationen kommen aus dem Package javax.persistence.

public class Department {
    int id;
    String label;
    Set<Person> staff;
    Person lead;
}

public class Person {
    int id;
    String name;
    Department department;
}
CREATE TABLE department ( 
	id INT PRIMARY KEY AUTO_INCREMENT, 
	label VARCHAR(45) NOT NULL, 
	lead_id INT NOT NULL, 
	FOREIGN KEY (lead_id) REFERENCES person(id) 
);
	
CREATE TABLE person ( 
	id INT PRIMARY KEY AUTO_INCREMENT, 
	name VARCHAR(45) NOT NULL, 
	dept_id INT, 
	FOREIGN KEY (dept_id) REFERENCES department(id) 
);
import javax.persistence.Entity;
// ...
	
@Entity
public class Department {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    int id;

    @Column(name = "dept_label", nullable = false, length = 45)
    String label;

    @OneToMany(mappedBy = "department")  // bidirectional relationship
    Set<Person> staff;

    @ManyToOne @JoinColumn(name = "lead_id", nullable = false)  // unidirectional relationship
    Person lead;
}

@Entity
public class Person {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    int id;

    @Column(nullable = false, length = 45)
    String name;

    @ManyToOne @JoinColumn(name = "dept_id")  // bidirectional relationship
    Department department;
}

JPA-Persistenzkontext

Der JPA-Persistenzkontext je Session wird durch ein sogenanntes EntityManager-Objekt verwaltet. Wie in der folgenden Abbildung dargestellt wird, erfolgt in JPA jeder Lese- oder Schreibzugriff auf die Datenbank über Methoden des EntityManager. Das Pendant zum eher abstrakten Begriff EntityManager wird in Hibernate etwas sprechender als Session bezeichnet. Das Interface org.hibernate.Session erweitert dementsprechend das Interface javax.persistence.EntityManager. Der EntityManager verwaltet also sämtliche Entitäten, die während einer Session mit der Datenbank ausgetauscht werden.

Das folgende Code-Beispiel zeigt, wie ein EntityManager über eine zugehörige Fabrik erzeugt werden kann. Die Fabrik selbst ist ein Singleton, das nur einmal im Anwendungskontext benötigt wird.

EntityManagerFactory emf = Persistence.createEntityManagerFactory("mariadb-localhost");
EntityManager em = emf.createEntityManager();

Bei der Erzeugung der Fabrik wird der Name einer sogenannten Persistence Unit angegeben, hier z.B. mariadb-localhost. Per Konvention wird diese Persistence Unit in einer XML-Konfigurationsdatei namens META-INF/persistence.xml spezifiziert und vom JPA-Framework dort nachgeschlagen. Es folgt die exemplarische Spezifikation der Persistence Unit namens mariadb-localhost, in der u.a. die URL für den Verbindungsaufbau per JDBC und die Credentials für den DB-User angegeben sind. Zu berücksichtigen ist, dass die Eigenschaft <property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/> (Zeile 9) dafür sorgt, dass sämtliche Tabellen in der Datenbank bei jedem Anwendungsstart neu erzeugt werden. Das ist natürlich nur zur Entwicklungszeit eine sinnvolle Konfiguration.

Das Interface EntityManager bietet eine Vielzahl von Methoden zur Synchronisation von Entitäten mit der Datenbank über den JPA-Persistenzkontext. Die folgende Tabelle stellt eine Auswahl der grundlegenden Methoden vor.

TypeMethod and description
booleancontains(Object entity)
Check if the instance is a managed entity instance belonging to the current persistence context.
Tfind(Class<T> entityClass, Object primaryKey)
Find by primary key.
voidpersist(Object entity)
Make an instance managed and persistent.
Tmerge(T entity)
Merge the state of the given entity into the current persistence context.
voidremove(Object entity)
Remove the entity instance.
voidrefresh(Object entity)
Refresh the state of the instance from the database, overwriting changes made to the entity, if any.
voiddetach(Object entity)
Remove the given entity from the persistence context, causing a managed entity to become detached.
voidclear()
Clear the persistence context, causing all managed entities to become detached.

Eine Entität (= Objekt einer Entitätsklasse) ist entweder im Zustand managed, d.h. aktuell durch den JPA-Persistenzkontext zur Synchronisation mit der Datenbank verwaltet, oder im Zustand detached, d.h. losgelöst aus dem JPA-Persistenzkontext. Der EntityManager synchronisiert die Entitäten auf dem Java-Heap im Zustand managed mit deren Abbild in der relationalen Datenbank. Die dazu erforderlichen Schreibzugriffe per SQL erfolgen spätestens beim Commit einer Transaktion oder können manuell mittels der Methode flush forciert werden. Mittels der Methoden persist oder merge können Entitäten in den Zustand managed gebracht werden. Auch die Methode find gibt eine Entität im Zustand managed zurück. Mittels der Methoden detach und clear können Entitäten in den Zustand detached gebracht werden, ohne sie vorher aus der Datenbank zu löschen. Die Methode remove löscht eine Entität aus der Datenbank und bringt sie ebenfalls in den Zustand detached. Wenn die Datensätze in der Datenbank außerhalb des JPA-Kontexts verändert werden, geraten die zugehörigen Entitäten in einen inkonsistenten Zustand und können über die Methode refresh durch Lesezugriffe auf die Datenbank aktualisiert werden.

Neben dem JPA-Persistenzkontext je EntityManager/Session, der als 1st-Level-Cache angesehen werden kann, existiert i.d.R. ein gemeinsamer 2nd-Level-Cache, den sich alle EntityManager-Instanzen teilen. Zur Veranschaulichung dient die folgende Abbildung. Selbst wenn aus einer Anwender-Session noch nicht auf eine bestimmte Entität zugegriffen worden ist, kann eine Anfrage ggf. aus dem 2nd-Level-Cache bedient werden, so dass keine SQL-Anfrage an die Datenbank gesendet werden muss, falls diese Entität kürzlich in einer parallelen Session angefragt worden ist.

Das folgende Code-Beispiel zeigt wie beide Caches explizit geleert werden können.

em.clear(); // clear 1st level cache
em.getEntityManagerFactory().getCache().evictAll();  // clear 2nd level cache

Transaktionen in JPA

In dem folgenden Code-Beispiel wird ein Department-Objekt mit einem zugeordneten Person-Objekt persistiert. Zu beachten ist, dass jeder schreibende Zugriff auf die Datenbank explizit in eine Transaktion eingebettet werden muss.

Department dept = new Department("R&D");
Person person = new Person("Hendricks", dept); // person is associated with dept

em.getTransaction().begin();
em.persist(dept);
em.persist(person);
em.getTransaction().commit();

Der Versuch nur das Person-Objekt ohne das abhängige Department-Objekt zu persistieren, misslingt mit einer aussagekräftigen Fehlermeldung:

em.getTransaction().begin();
em.persist(person); // try to persist person without persisting associated department in the same transaction
em.getTransaction().commit();

>>> Exception in thread "main" javax.persistence.RollbackException: java.lang.IllegalStateException:
During synchronization a new object was found through a relationship that was not marked cascade PERSIST: model.Department@xxxxxxxx.

Eine Transaktion kann mittels em.getTransaction().rollback() abgebrochen werden. Im Fehlerfall kann eine RollbackException gefangen und behandelt werden. Es ist davon auszugehen, dass mehrere Transaktionen parallel ausgeführt werden, wenn mehrere Anwender zeitgleich mit einer Anwendung interagieren.

Kaskadierende Zugriffe auf Entitäten und Lazy Loading

Voneinander abhängige Objekte können auch als ein zusammengehöriges Aggregat behandelt werden. Dabei werden Attribute mit komplexem Datentyp (d.h. Datentyp entspricht einer anderen Entitätsklasse wie in Zeile 6 des folgenden Code-Beispiels) kaskadierend gespeichert (CascadeType.PERSIST), aktualisiert (CascadeType.MERGE), aus der Datenbank gelöscht (CascadeType.REMOVE), neu geladen (CascadeType.REFRESH) oder aus dem Persistenzkontext gelöst (CascadeType.DETACH). Dazu muss die entsprechende Assoziation zwischen den Klassen als kaskadierend hinsichtlich einer oder mehrerer Methoden des EntityManager eingestellt werden (Zeile 4).

@Entity
public class Person {

    @ManyToOne(cascade = CascadeType.ALL) // ALL includes PERSIST, MERGE, REMOVE, REFRESH, DETACH
    @JoinColumn(name = "dept_id")
    Department department;
	
	// ...
}
public class Application {

    public static void main(String[] args) {

        // connect to database
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("mariadb-localhost");
        EntityManager em = emf.createEntityManager();

        // create example objects
        Department dept = new Department("R&D");
        Person person = new Person("Hendricks", dept);

        // persist objects to database
        em.getTransaction().begin();
        em.persist(person); // persists person and implicitly associated department
        em.getTransaction().commit();

        // close session
        em.close();
    }
}

Entsprechend den Schreibzugriffen kann auch das Lesen eines komplexen Objekts aus der Datenbank schrittweise in Teilen (FetchType.LAZY) oder als Ganzes (FetchType.EAGER) erfolgen. Der Default ist das einzelne Entity-Attribute stets eager (@OneToOne- und @ManyToOne-Assoziationen) und Collection<Entity>-Attribute stets lazy geladen werden (@OneToMany- und @ManyToMany-Assoziationen). Das folgende Code-Beispiel zeigt die Klasse Department, deren Attribut lead vom Typ Person eager geladen wird, während das Attribut staff vom Typ Set<Person> lazy geladen wird. Die angegebenen FetchType-Werte entsprechen hierbei dem Default und wären daher nicht notwendig.

@Entity
public class Department {

    @ManyToOne(fetch = FetchType.EAGER)
	@JoinColumn(name = "lead_id")
    Person lead;

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "department")
    Set<Person> staff;
	
	// ...
}

Beim Lazy Loading wird das entsprechende Attribut erst bei einem späteren Zugriff befüllt. Im folgenden Code-Beispiel löst die Zeile 1 eine oder zwei SQL-Anfragen aus: zum einen wird stets der gesuchte Datensatz aus der Tabelle department gelesen, zum anderen der zugehörige Abteilungsleiter ermittelt, falls das Attribut lead in der Klasse Department gesetzt ist. Das Laden der zugeordneten Personen im Attribut staff durch eine SQL-Anfrage auf die Tabelle person erfolgt erst beim ersten Zugriff auf eine Methode dieses Attributs in Zeile 8. Bis dahin ist das Attribut nicht instantiiert (vgl. Zeilen 5-6). Im Debug-Modus kann das Lazy Loading nicht ohne Weiteres beobachtet werden, da der Debugger selbst das Laden der Attribute verursacht, um deren aktuellen Status anzeigen zu können.

Department dept = em.find(Department.class, 1); // finds person with id = 1
// causes department query: SELECT id, label, lead_id FROM department WHERE id = ?
// causes department lead query, if lead_id is not null: SELECT id, name, dept_id FROM person WHERE id = ?

System.out.println(">>> persons in department: " + dept.getStaff());
// >>> persons in department: {IndirectSet: not instantiated}

System.out.println(">>> persons in department: " + dept.getStaff().size());
// causes department staff query: SELECT id, name, dept_id FROM person WHERE dept_id = ?
// >>> persons in department: 10

Die Vorteile von Lazy Loading sind eine Verkürzung der initialen Ladezeit und ein zunächst geringerer Hauptspeicherbedarf. Allerdings kann das verzögerte Laden später zu einem ungewollten Zeitpunkt während der Anwenderinteraktion geschehen.

Java Persistence Query Language (JPQL)

Das EntityManager-Interface bietet neben der Methode find, um ein Objekt über seinen Primärschlüssel aus der Datenbank zu lesen, noch weitere Methoden für Datenbankanfragen.

TypeMethod and description
QuerycreateNativeQuery(String sqlString)
Create an instance of Query for executing a native SQL statement, e.g., for update or delete.
StoredProcedureQuerycreateStoredProcedureQuery(String procedureName)
Create an instance of StoredProcedureQuery for executing a stored procedure in the database.
QuerycreateQuery(String qlString)
Create an instance of Query for executing a Java Persistence query language statement.
TypedQuery<T>createQuery(String qlString, Class<T> resultClass)
Create an instance of TypedQuery for executing a Java Persistence query language statement.
TypedQuery<T>createQuery(CriteriaQuery<T> criteriaQuery)
Create an instance of TypedQuery for executing a criteria query.

Dazu zählen eine Methode für native SQL-Anfragen und eine Methode zum Aufruf von Stored Procedures. Bevorzugt werden aber Anfragen über die Java Persistence Query Language (JPQL), die ihrerseits stark an SQL angelehnt ist. JPQL orientiert sich in seiner deklarativen Syntax mit SELECT ... FROM ... WHERE ... GROUP BY ... ORDER BY ... an dem gleichen Gerüst an Schlüsselworten für eine Anfrage wie SQL. Viele Operatoren (z.B. LIKE, IN) und Aggregatfunktionen (z.B. COUNT, SUM, AVG) verhalten sich ebenfalls wie in SQL. Ein wichtiger Unterschied ist, dass in der FROM-Klausel nicht die zugrundeliegende Tabelle, sondern die objektorientierte Klasse adressiert wird. Demzufolge ist auch die Navigation entlang von Assoziationen über den Operator . möglich, welcher bei der Übersetzung in eine SQL-Anfrage implizit einen Join verursachen kann. Zur Veranschaulichung dienen die folgenden Beispiele.

-- Select all persons and their associated department label (causes a join of the person table and the department table)
SELECT p.id, p.name, p.department.label FROM Person p; 

-- Select the number of persons per department label (causes a join of the person table and the department table)
SELECT p.department.label, COUNT(p) FROM Person p GROUP BY p.department.label; 

-- Select the head of the sales department (result is of type Person)
SELECT d.lead FROM Department d WHERE d.label = "Sales";

Aus einer Java-Anwendung heraus kann JPQL in ungetypten Anfragen (javax.persistence.Query) oder getypten Anfragen (javax.persistence.TypedQuery<T>) verwendet werden. Bei getypten Anfragen entspricht die Rückgabe der JPQL-Anfrage einer vorab erstellten Klasse des objektorientierten Datenmodells. Bei einer ungetypten Anfrage ist der Typ der Rückgabe nicht definiert. Das folgende Code-Beispiel veranschaulicht eine ungetypte und eine getypte JQPL-Anfrage. Optional kann eine Anfrage parametrisiert werden (Zeilen 8-9) und hinsichtlich der Anzahl der zurückgegebenen Objekte beschränkt werden (Zeile 3). Wenn max. 1 Objekt in der Rückgabe erwartet wird, ist die Methode getSingleResult zu verwenden (Zeile 10), ansonsten die Methode getResultList (Zeile 4).

 // Untyped query
Query q = em.createQuery("SELECT p.id, p.name, p.department.label FROM Person p");
q.setMaxResults(100);
List<Object[]> result = q.getResultList();
result.forEach(object -> System.out.println( Arrays.toString(object) ));

// Typed query
TypedQuery<Person> q = em.createQuery(String jpql = "SELECT d.lead FROM Department d WHERE d.label = :dept", Person.class);
q.setParameter("dept", "Sales");
Person leadSales = q.getSingleResult();

Als Alternative zu einer deklarativen JPQL-Anfrage, die als String an die Methode createQuery übergeben wird, gibt es noch die sogenannte Criteria API, über die eine JQPL-Anfrage imperativ zusammengestellt werden kann. Zum Vergleich wird die letztere der beiden obigen JQPL-Anfragen im nächsten Code-Beispiel mittels der Criteria API formuliert. Für Entwickler, die SQL-Anfragen gewohnt sind, erscheint der Code, der durch die Criteria API entsteht, vergleichsweise unübersichtlich.

// Above typed query in Criteria API
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery cq = cb.createQuery(Person.class);
Root a = cq.from(Department.class);
ParameterExpression<String> p = cb.parameter(String.class);
cq.select(a.get("lead")).where(cb.equal(a.get("label"), p));
TypedQuery<Person> q = em.createQuery(cq);
q.setParameter(p, "Sales");
Person leadSales = q.getSingleResult();

Nebenläufige Transaktionen in JPA

Bei parallelen Anwender-Sessions mit schreibenden Datenbankzugriffen sind Transaktionen, die die ACID-Eigenschaften erhalten, erforderlich, um insbesondere sogenannte Lost Updates zu vermeiden. Die folgende Abbildung zeigt ein potentielles Lost Update, da beide Transaktionen parallel zueinander dieselbe Entität verändern möchten.

Voraussetzung für die Konflikterkennung bei nebenläufigen Transaktionen in JPA ist ein Ganzzahl-Attribut in der betroffenen Entity-Klasse, das mit der Annotation @Version versehen ist. Über dieses Attribut gelingt es dem JPA-Framework unterschiedliche Versionen einer Entität voneinander zu unterscheiden. Bezüglich des Sperrverhaltens ist der Default sogenanntes Optimistic Locking. Hierbei ist die Annahme, dass Konflikte zwischen nebenläufigen Transaktionen selten auftreten. Konflikte werden nicht vorab durch das Setzen expliziter Sperren verhindert, sondern erst zum Zeitpunkt des Commit erkannt. Eine Transaktion wird beim Commit ggf. zurückgerollt, falls sie Entitäten gelesen oder verändert hat, deren Zustand während der Transaktionslaufzeit durch eine andere Transaktion verändert und schneller erfolgreich committed worden ist. Es entsteht also ein Wettlauf zum Commit, der zu kurzen Transaktionen diszipliniert. Das folgende Code-Beispiel für Optimistic Locking zeigt, wie eine Transaktion (EntityManager em1) durch eine andere Transaktion (EntityManager em2) überholt wird und dadurch beim Commit auf eine RollbackException (verursacht durch eine OptimisticLockException) stößt.

Alternativ kann beim Zugriff auf eine Entität explizit eine exklusive Sperre gesetzt werden, die bis zur Freigabe der Sperre beim Commit verhindert, dass andere Transaktionen die gesperrte Entität verändern können. Dieses Sperrverhalten wird als Pessimistic Locking bezeichnet. Es schränkt die mögliche Parallelität stark ein. Bei einer extensiven Verwendung von Sperren sollten Grundlagen aus dem Datenbankbereich wie das 2-Phasen-Sperrprotokoll berücksichtigt werden. Das folgende Code-Beispiel für Pessimistic Locking zeigt, wie eine durch eine Sperre verzögerte Transaktion (EntityManager em2) auf eine RollbackException (verursacht durch einen Lock-Wait-Timeout) stoßen kann.

// create 2 concurrent sessions
EntityManagerFactory emf = Persistence.createEntityManagerFactory("...");
EntityManager em1 = emf.createEntityManager();
EntityManager em2 = emf.createEntityManager();

// create managed object
em1.getTransaction().begin();
Department a = new Department("A");
a = em1.merge(a);
em1.getTransaction().commit();

// provoke concurrency conflict: transaction 1 starts, but is not committed
em1.getTransaction().begin();
Department a1 = em1.find(Department.class, a.getId());
a1.setLabel("A1");

// transaction 2 overtakes transaction 1
em2.getTransaction().begin();
Department a2 = em2.find(Department.class, a.getId());
a2.setLabel("A2");
em2.getTransaction().commit();

// transaction 1 is cancelled, if @Version attribute exists in entity class
try {
	em1.getTransaction().commit();
} catch (RollbackException e) {
	e.printStackTrace();
}
>>> javax.persistence.RollbackException caused by: javax.persistence.OptimisticLockException
The object [model.Department@xxxxxxxx] cannot be updated because it has changed or been deleted since it was last read.
// create 2 concurrent sessions
EntityManagerFactory emf = Persistence.createEntityManagerFactory("...");
EntityManager em1 = emf.createEntityManager();
EntityManager em2 = emf.createEntityManager();

// create managed object
em1.getTransaction().begin();
Department a = new Department("A");
a = em1.merge(a);
em1.getTransaction().commit();

// provoke concurrency conflict: transaction 1 starts, but is not committed
em1.getTransaction().begin();
Department a1 = em1.find(Department.class, a.getId(), LockModeType.PESSIMISTIC_READ); // lock entity
a1.setLabel("A1");

// transaction 2 overtakes transaction 1
em2.getTransaction().begin();
em2.createNativeQuery("SET innodb_lock_wait_timeout = 3").executeUpdate(); // decrease lock-wait-timeout (default is 50 s)
Department a2 = em2.find(Department.class, a.getId());
a2.setLabel("A2");

// commit of transaction 2 is blocked and cancelled after lock-wait-timeout, as transaction 1 has set a lock
try {
	em2.getTransaction().commit();
} catch (RollbackException e) {
	e.printStackTrace();
}

// finally commit transaction 1
em1.getTransaction().commit();
>>> javax.persistence.RollbackException caused by: java.sql.SQLException
Lock wait timeout exceeded; try restarting transaction.

Vererbung in JPA

Zur Abbildung von Vererbungsbeziehungen im relationalen Modell gibt es drei geläufige Ansätze, die in der JPA als Single Table (InheritanceType.SINGLE_TABLE), Joined Subclass (InheritanceType.JOINED) und Table per Class (InheritanceType.TABLE_PER_CLASS) bezeichnet werden. Die Vor- und Nachteile der unterschiedlichen Abbildungen von Vererbungshierarchien sollen im Folgenden erläutert werden.

Entitätsklassen in REST-Ressourcen

Wenn die Objekte der Entitätsklassen direkt über eine mit JAX-RS gebaute REST-API ausgegeben werden (ohne sie explizit in DTO-Klassen zu transformieren), ist darauf zu achten, dass Zyklen bei bidirektionalen Beziehungen unterbrochen werden. Im folgenden Code-Beispiel soll eine Anfrage an den Endpunkt /departments/<id> nicht die Personen ausgeben, die einer Abteilung zugeordnet sind. Demzufolge ist das Attribut Set<Person> staff mit der Annotation @JsonIgnore versehen (Zeilen 10-11 in Klasse Department). Auf der anderen Seite dieser Beziehung in der Klasse Person ist das Attribut Department department exemplarisch mit der Annotation @JsonIgnoreProperties({"label", "staff"}) versehen (Zeilen 10-11 in Klasse Person). Dadurch wird die Ausgabe unter dem Pfad /persons/<id> zwar die Abteilung einer Person beinhalten, aber nur mit den Attributen, die nicht explizit ausgeschlossen werden.

@Entity
public class Department {

    @Id
    int id;

    String label;

    @OneToMany(mappedBy = "department")  // bidirectional relationship
	@JsonIgnore
    Set<Person> staff;
}
@Entity
public class Person {

    @Id
    int id;

    String name;

    @ManyToOne @JoinColumn(name = "dept_id")  // bidirectional relationship
	@JsonIgnoreProperties({"label", "staff"})
    Department department;
}

Die gezeigten Code-Beispiele zum objekt-relationalen Mapping mittels JPA finden sich im Verzeichnis /remote/orm des Modul-Repository.