OR Mapping
Ziel: Gap zwischen Objektmodell der Applikation und (meist) relationales Modell der Datenbanken überwinden
Java Persistence API (JPA)
- JPA ist ein Layer zwischen JDBC und dem Programm
- JPA ist nur eine Definition, implementiert von Providern
- Hier wird die EclipseLink-Implementierung verwendet (Hibernate wäre ein anderer Provider)
Entities
- Entities sind POJO's mit ID-Feld für PK mit default-Konstruktor
- Dürfen nicht final sein (wegen z.B. lazy-loading)
- Abbildung in DB über Annotations oder sepparates XML-File
- Annotation
@Entity
für Klasse,@Id
für PK-Feld
Beispiel
@Entity public class BankCustomer { @Id private long id; public long getId() { return id; } private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } }
persistence.xml
<persistence xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0" xmlns="http://java.sun.com/xml/ns/persistence"> <persistence-unit name="Bank" transaction-type="RESOURCE_LOCAL"> <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider> <exclude-unlisted-classes>false</exclude-unlisted-classes> <properties> <property name="javax.persistence.jdbc.driver" value="org.postgresql.Driver"/> <property name="javax.persistence.jdbc.url" value="jdbc:postgresql:jpatest"/> <property name="javax.persistence.jdbc.user" value="test"/> <property name="javax.persistence.jdbc.password" value="test"/> <property name="eclipselink.target-database" value="PostgreSQL"/> </properties> </persistence-unit> </persistence>
Entities CRUD
// SELECT EntityManagerFactory factory = Persistence.createEntityManagerFactory("Bank"); EntityManager em = factory.createEntityManager(); Query query = em.createQuery("SELECT a FROM BankAccount a"); List<BankAccount> list = query.getResultList(); for (BankAccount account : list) { System.out.println(account); } em.close(); // besser mit try-with-ressources // INSERT em.getTransaction().begin(); BankCustomer customer = new BankCustomer(); customer.setName("Bill"); em.persist(customer); // INSERT and SELECT, customer hat jetzt seine ID em.getTransaction().commit(); em.close(); // UPDATE em.getTransaction().begin(); BankAccount account = em.find(BankAccount.class, 1L); account.incBalance(100); em.getTransaction().commit(); // DELETE // nicht effizient, Java-Object wird erst durch GC aufgeräumt em.getTransaction().begin(); BankAccount account = em.find(BankAccount.class, 1L); em.remove(account); em.getTransaction().commit();
Table Mapping
@Entity @Table(name = "account") public class BankAccount { @Id @Column(name = "accountid") private long id; private double balance; // 1:1 Mapping @Column(name = "description") private String explanation; @Column(name=“ssn“, unique=true, nullable=true) private long ssn; @Column(name=“l_name“, length=200) private String lastName; //Zusatzinfo, wird als TIMESTAMP abgelegt @Temporal(TemporalType.TIMESTAMP) private Calendar creationDate; @Transient // wird nicht gemapped private String tempComments; // getter und setter }
- Weitere Attribute:
scale
,precision
Relationen
// 1:1 Mapping @Entity @Table(name = "bankcustomer") public class BankCustomer { @OneToOne(optional = true) @JoinColumn(name = "addressref") private Address address; // ... } // Bidirectional 1:1 mapping, Datenbank bleibt gleich @Entity public class Address { @OneToOne(mappedBy = "address") private BankCustomer customer; // ... } // N:1 Mapping class BankAccount { @ManyToOne(optional = false) @JoinColumn(name = "customerref") private BankCustomer customer; //... } // Bidirectional N:1 class BankCustomer { @OneToMany(mappedBy = "customer") private Collection<BankAccount> accounts; // = ... // ... } // N:M maping class BankManager { @ManyToMany @JoinTable(name = "customer_manager", joinColumns = {@JoinColumn(name = "managerref")}, inverseJoinColumns = {@JoinColumn(name = "customerref")}) private Collection<BankCustomer> customers; // = ... }
- Bei bidirektionalen Beziehungen müssen beide Seiten manuell gesetzt werden (im Gegensatz zu z.B. EF)
- Im Beispiel muss beim Hinzufügen eines
BankAccount
dieser der Liste in derBankCustomer
hinzugefügt werden
- Im Beispiel muss beim Hinzufügen eines
- Lazy-Loading ist per default aktiviert für 1:n und n:m
- Bsp: Wenn
BankCustomer
geladen werden, wird dieBankAccounts
-Liste lazy mitcustomer.getAccounts()
geladen
- Bsp: Wenn
- Eager-Loading standard bei 1:1 und n:1
- Explizit angeben mit z.B.
@ManyToMany(fetch = FetchType.EAGER)
Persistence-Context
- Ein Objekt ist neu erstellt erstmal unmanaged
- Wird es in die DB eingefügt mit
persist()
, ist es managed und somit im Persistence-Context (Objekt hat eine Id) - Wird die Verbindung geschlossen (
close()
) oder removed, sind die Objekte wieder unmanaged - Persistence-Kontext verwaltet einen Cache - Bei mehrmaliger Abfrage der gleichen Objekte werden sie aus dem Cache geladen
- Wichtig, sonst könnte es mehrere Java-Instanzen für ein DB-Objekt geben
- So gibt es in einem Kontext für ein DB-Objekt genau eine Java Instanz (Identity Map Pattern)
- Wichtig, sonst könnte es mehrere Java-Instanzen für ein DB-Objekt geben
- Geänderte Werte im Persistence-Kontext werden beim Transaction-Commit auf der DB gespeichert (Unit of Work Pattern)
EntityManager em = emf.createEntityManager(); BankCustomer c1 = em.find(BankCustomer.class, 1L); BankCustomer c2 = em.find(BankCustomer.class, 1L); assert(c1 == c2); // gleicher PC EntityManager em1 = emf.createEntityManager(); c2 = em1.find(BankCustomer.class, 1L); assert(c1 != c2); // unterschiedlicher PC
Entity Identität
- Annotation
@Id
@GeneratedValue
- generierte ID mit 4 Möglichkeiten
Typ Identity
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long accountId;
- Wird abgebildet in
SERIAL
Typ Sequence
Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "BankCustGen") @SequenceGenerator(name = "BankCustGen", sequenceName = "CustomerIdSeq", allocationSize=1) private long customerId;
Note
todo
Transitive Persistenz
- Von Persistente Objekte erreichbaren Objekte sollten auch persistent sein
Inkonsistente Bidirektionalität
- In JPA werden bidirektionale Relationen nicht in beide Richtungen automatisch gesetzt
- Muss von Hand gemacht werden
Transaktionen
EntityManager em = factory.createEntityManager(); em.getTransaction().begin(); // ... // do something... make changes // ... em.getTransaction().commit(); // or rollback()
- Isolation Level ist per default Read-Commited
- Nur Daten lesen, die eine andere Transaktion commited hat
Optimistic Concurrency
- Beim Commit einer Transaktion wird nach Konflikten geprüft
- Wenn Konflikt entstand, wird ein Rollback gemacht
Versions-Feld
- Mit jedem Update Feld hochzählen
- Beim Schreiben prüfen, ob das Feld gleich ist wie der Wert, der vorher gelesen wurde
- Wenn Feld anders, hat jemand in der Zwischenzeit den Record verändert -> Rollback
UPDATE User SET ..., version = version + 1 WHERE id = ? AND version = readVersion
- JPA wirft bei Verletzung eine
ConcurrencyLockException
Entity Locking
em.lock()
- OPTIMISTIC_FORCE_INCREMENT: Beim lock direkt direkt das Versions-Feld erhöhen, damit andere keine Commits mehr darauf machen kann, der vorher gelesen hat
Detached Entities
- Oft möchte man DB-Objekte "detachen" (Db-Context schliessen), verändern, und dann wieder in die DB schreiben
- Mit
entityManger.merge(object)
wird ein wieder in den Persistenz-Kontext geladen, dass wieder commited werden kann
Inheritance Mapping
Single Table Mapping (Table per Hierarchy)
@Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "type") public abstract class BankCustomer { @Id private String name; } @Entity @DiscriminatorValue("Retail") public class RetailBankCustomer extends BankCustomer { private int fees; } @Entity @DiscriminatorValue("Private") public class PrivateBankCustomer extends BankCustomer { private String eliteOffer; }
- Das Feld "type" ist hier diskriminator und wird in der DB verwendet, um die verschiedenen
BankCustomer
zu unterscheiden
Joined Table Mapping
Table per Class Mapping
Abfragen mit JPQL
- Sprache analog zu SQL...
- , aber operiert auf Entity Model, nicht DB-Model!
- Es können z.B. "Navigation Attributes" verwendet werden
- "Prepared Statements" mit Named Parameters
Query query = em.createQuery( "SELECT c FROM BankCustomer c WHERE c.name LIKE :customerName" ); query.setParameter("customerName", name); query.setMaxResults(1000); List<BankCustomer> list = query.getResultList();