Objekt-relationales Mapping Hibernate Vorlesung 12 Programmieren II Dr. Klaus Höppner Hochschule Darmstadt – Sommersemester 2009 1 / 35 Objekt-relationales Mapping Hibernate Objekt-relationales Mapping Hibernate 2 / 35 Objekt-relationales Mapping Hibernate Objekt-relationales Mapping Mit JDBC kann Java sich mit relationalen Datenbanken verbinden. Hierbei entsprechen • jeder Datensatz einer Zeile in einer Datenbanktabelle und • die Attribute eines Datensatzes den Tabellenspalten. Daher ist es prinzipiell möglich, eine Abbildung zwischen den Datensätzen in einer Tabelle und klassischen Java-Objekten (POJO – plain old Java objects herzustellen. Eine solche Abbildung nennt man objekt-relationales Mapping (ORM). Handout S. 1 3 / 35 Objekt-relationales Mapping Hibernate Vorlesung 12 Beispieltabelle: Customer Die folgende Tabelle beschreibt einen Kunden mit Adresse, wobei die ID des Kunden automatisch generiert wird (entsprechend des SQL-Dialektes für HSQLDB): CREATE TABLE CUSTOMER( ID INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 0) NOT NULL PRIMARY KEY, FIRSTNAME VARCHAR(20), LASTNAME VARCHAR(20), STREET VARCHAR(20), CITY VARCHAR(20) ) 4 / 35 Objekt-relationales Mapping Hibernate Korrespondierende Klasse Customer public class Customer { private Long id; private String firstname; private String lastname; private String street; private String city; // Getter und Setter public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } ... @Override public String toString() { return String.format("%s %s; %s; %s", getFirstname(), getLastname(),getStreet(), getCity()); } } 5 / 35 Objekt-relationales Mapping Hibernate Verknüpfung DB-Tabelle ↔ Java-Objekt Nun sollen Java-Objekte persistent gemacht werden, indem Objekte vom Typ Customer mit der gleichnamigen DB-Tabelle synchronisiert werden können. Hierzu werden der Klasse Customer zwei neue Methoden hinzugefügt: fetch() Hier werden für eine Instanz von Customer (mit gültiger ID) die Werte der Attribute per SELECT aus der Tabelle geholt. update() Hier werden die Werte der Attribute in die Tabelle geschrieben, wobei bei leerer ID (null) ein neuer Datensatz per INSERT eingefügt, andernfalls die Feldwerte des existierenden Datensatzes per UPDATE aktualisiert werden. Handout S. 2 6 / 35 Objekt-relationales Mapping Hibernate Vorlesung 12 Klasse DBConnection public class DBConnection { private static Connection con = null; public static Connection getConnection() { if (con==null) { try { Class.forName("org.hsqldb.jdbcDriver"); con = DriverManager.getConnection("jdbc:hsqldb:file:"+ "data/fibu;shutdown=true", "SA", ""); } catch (ClassNotFoundException e) {e.printStackTrace();} catch (SQLException e) {e.printStackTrace();} } return con; } public static void disconnect() { if (con!=null) { try {con.close();} catch (SQLException e) {e.printStackTrace();} finally {con=null;} } } } 7 / 35 Objekt-relationales Mapping Hibernate Methode fetch() von Customer public void fetch() { if (id==null) throw new RuntimeException("Object not persistent"); Connection con = DBConnection.getConnection(); try { PreparedStatement pst = con.prepareStatement("select * from customer where id=?"); pst.setInt(1, id.intValue()); ResultSet res = pst.executeQuery(); if (res.next()) { setLastname(res.getString("LASTNAME")); setFirstname(res.getString("FIRSTNAME")); setStreet(res.getString("STREET")); setCity(res.getString("CITY")); } else { setId(null); setLastname(null); setFirstname(null); setStreet(null); setCity(null); } } catch (SQLException e) {e.printStackTrace();} } 8 / 35 Objekt-relationales Mapping Hibernate Methode update() von Customer public void update() { Connection con = DBConnection.getConnection(); PreparedStatement pst = null; try { if (getId()==null) { pst = con.prepareStatement("insert into customer "+ "(FIRSTNAME, LASTNAME, STREET, CITY) values (?,?,?,?)"); } else { pst = con.prepareStatement("update customer set "+ "FIRSTNAME=?, LASTNAME=?, STREET=?, CITY=? where id=?"); pst.setInt(5, getId().intValue()); } pst.setString(1, getFirstname()); pst.setString(2, getLastname()); pst.setString(3, getStreet()); pst.setString(4, getCity()); pst.executeUpdate(); pst.close(); Handout S. 3 9 / 35 Objekt-relationales Mapping Hibernate Vorlesung 12 Methode update() (Forts.) if (getId()==null) { pst = con.prepareStatement("CALL IDENTITY()"); ResultSet res = pst.executeQuery(); if (res!=null && res.next()) { setId(new Integer(res.getInt(1))); res.close(); } } } catch (SQLException e) { e.printStackTrace(); } finally { if (pst!=null) { try {pst.close();} catch (SQLException e) {e.printStackTrace();} } } } 10 / 35 Objekt-relationales Mapping Hibernate Exkurs: PreparedStatement In der Realisierung von fetch() und update() wurde statt Statement jeweils PreparedStatement benutzt. Bei PreparedStatement wird beim Anlegen das Statement bereits mit einem SQL-Befehl (i. A. mit Platzhaltern) vorbereitet. Dieser wird von der Datenbank vorkompiliert. Danach können die Platzhalter mit den realen Werte belegt werden. Vorteile: • Bei mehrfacher Verwendung (z. B. in Schleifen) ergibt sich ein Laufzeitgewinn, da der Befehl schon vorkompiliert ist. • Da die Gültigkeit der Parameter überprüft wird und SQL-Sonderzeichen (z. B. ’) geschützt werden, werden so genannte SQL-Injections verhindert. 11 / 35 Objekt-relationales Mapping Hibernate Zusammenfassung In diesem Abschnitt wurde anhand einer einfachen Tabelle demonstriert, wie ein objekt-relationales Mapping selber realisiert werden kann. Der Aufwand war aber recht hoch, da individuell für die Klasse die entsprechenden Methoden zur Synchronisation inkl. der entsprechenden SQL-Befehle implementiert wurden. Wegen verschiedener SQL-Dialekte der RDBMS ist die Implementierung auch DB-spezifisch. Der Aufwand wird noch höher, wenn Tabellen über Schlüssel miteinander verknüpft sind. Daher werden in der Praxis für ORM spezielle Frameworks benutzt. Handout S. 4 12 / 35 Objekt-relationales Mapping Hibernate Vorlesung 12 Hibernate Es existieren verschiedene Frameworks, die ein objekt-relationales Mapping realisieren. Eines der bekanntesten und häufig benutzten ist Hibernate (http://www.hibernate.org). Was ist der Vorteil eines solchen Frameworks und ORM? Es verbindet die Vorteile der objektorientierten Programmierung mit den Vorteilen einer Datenbank, insbesondere der Persistenz (d. h. dass Datensätze in einem RDBMS nicht flüchtig sind). So kann man persistente Objekte realisieren, die sich weitgehend wie normale Java-Objekte verwenden lassen. 13 / 35 Objekt-relationales Mapping Hibernate Geschichte Hibernate wurde von der Firma JBoss entwickelt, die sich mit Open-Source-Software im Bereich Java-Middleware, -Applikationsservern und Message-Brokern beschäftigt. Mittlerweile wurde JBoss (nach einem Bieterkampf mit Oracle) von RedHat übernommen. 14 / 35 Objekt-relationales Mapping Hibernate Architektur Hibernate stellt in der einfachsten Benutzungsform dem Anwender eine Datenbank-Session zur Verfügung, die nach Konfiguration eines Datenbanktyps und der Datenbankparameter geöffnet wird. Objekte können sich in drei Zuständen befinden: transient Das Objekt ist noch nicht mit der Datenbank verknüpft, also flüchtig. persistent Das Objekt ist im aktuellen Kontext mit der Datenbank verknüpft. detached Das Objekt war mit der Datenbank verknüpft, der Kontext existiert aber nicht mehr. Daher ist das Objekt aktuell losgelöst. Handout S. 5 15 / 35 Objekt-relationales Mapping Hibernate Vorlesung 12 Schaubild 16 / 35 Objekt-relationales Mapping Hibernate Konfiguration einer Hibernate-Anwendung Die Konfiguration von Hibernate (Datenbank, Logging etc.) erfolgt über die Klasse org.hibernate.cfg.Configuration. Hierbei gibt es die Möglichkeit, zur Konfiguration die Datei hibernate.cfg.xml im Source-Verzeichnis zu verwenden und anschließend eine Implementierung von org.hibernate.SessionFactory zu erzeugen: Configuration cfg = new Configuration().configure(); SessionFactory sessionFactory = cfg.buildSessionFactory(); Alternativ können Konfigurationsparameter mit der Methode setProperty gesetzt werden: Configuration cfg = new Configuration(); cfg.setProperty("hibernate.connection.username","sa"); ... 17 / 35 Objekt-relationales Mapping Hibernate Beispiel einer Konfigurationsdatei <?xml version=’1.0’ encoding=’utf-8’?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <session-factory> <!-- Database connection settings --> <property name="connection.driver_class">org.hsqldb.jdbcDriver</property> <property name="connection.url">jdbc:hsqldb:file:data/fibu;shutdown=true</property> <property name="connection.username">sa</property> <property name="connection.password"></property> <!-- JDBC connection pool (use the built-in) --> <property name="connection.pool_size">1</property> <!-- SQL dialect --> <property name="dialect">org.hibernate.dialect.HSQLDialect</property> <!-- Enable automatic session context management --> <property name="current_session_context_class">thread</property> <!-- Disable the second-level cache --> <property name="cache.provider_class">org.hibernate.cache.NoCacheProvider</property> <!-- Echo all executed SQL to stdout --> <property name="show_sql">true</property> <!-- Drop and re-create the database schema on startup --> <property name="hbm2ddl.auto">create</property> <mapping resource="fibu/Customer.hbm.xml"/> </session-factory> </hibernate-configuration> Handout S. 6 18 / 35 Objekt-relationales Mapping Hibernate Vorlesung 12 Grundlagen von ORM Durch ORM werden Klassen mit Tabellen und Instanzen der Klassen mit Datensätzen innerhalb der Tabelle verknüpft. Damit Instanzen eindeutig Datensätzen zugeordnet werden können, muss die Datenbanktabelle einen eindeutigen Primärschlüssel besitzen. Dieser wird dann als WHERE-Ausdruck in den abgesetzten SQL-Anweisungen verwendet. Weiterhin dient die Existenz einer Primär-ID im Objekt der Unterscheidung, ob ein nicht persistentes Objekt transient oder detached ist. 19 / 35 Objekt-relationales Mapping Hibernate Faustregeln • Im Allgemeinen wird eine Tabelle der Datenbank einer Java-Klasse zugeordnet. • Diese Klasse muss einen Standardkonstruktor besitzen. • Die Attribute der Tabelle werden auf entsprechende Attribute der Klasse abgebildet. • In der Klasse werden dann Getter und Setter für diese Attribute definiert. • Die eigentliche Zuordnung zwischen Klasse und Tabelle erfolgt dann über eine XML-Datei, die insbesondere den Primärschlüssel angibt, der zur Identifikation von Elementen verwendet wird, sowie die Namen von Tabellenspalten den Attributen der Klasse zuordnet. 20 / 35 Objekt-relationales Mapping Hibernate Beispiel: Datenbank Fibu Hier nun ein Datenbankschema für eine Buchhaltung: Invoice ID : INTEGER CUSTOMERID : INTEGER TOTAL : DECIMAL(10,2) Item ITEM : INTEGER INVOICEID : INTEGER PRODUCTID : INTEGER QUANTITY : INTEGER COST : DECIMAL(10,2) Customer ID : INTEGER FIRSTNAME : VARCHAR(20) LASTNAME : VARCHAR(20) PRODUCT ID : INTEGER NAME : VARCHAR(20) PRICE : DECIMAL(10,2) Handout S. 7 21 / 35 Objekt-relationales Mapping Hibernate Vorlesung 12 Erster Ansatz: Klasse Invoice public class Invoice { private Long id; private Long customerID; private Double total; public Invoice() {} public Long getId() { return id; } public void setId(Long id) { this.id = id; } public Long getCustomerID() { return customerID; } public void setCustomerID(Long customerID) { this.customerID = customerID; } public Double getTotal() { return total; } public void setTotal(Double total) { this.total = total; } } 22 / 35 Objekt-relationales Mapping Hibernate Mapping Das eigentliche Mapping erfolgt dann über die Datei Invoice.hbm.xml: <?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping package="fibu"> <class name="Invoice" table="Invoice"> <id name="id" column="id"> <generator class="native"></generator> </id> <property name="customerID"></property> <property name="total"></property> </class> </hibernate-mapping> 23 / 35 Objekt-relationales Mapping Hibernate Analyse Die gezeigte XML-Datei enthält folgende Definitionen: • Das Mapping wird für Klassen innerhalb des Pakets fibu durchgeführt. • Für jede abzubildende Klasse gibt es ein Element class, in diesem Fall für die Klasse Invoice, gleichzeitig wird erklärt, welcher Tabelle die Klasse zugeordnet ist. • In diesem Fall wird für den Primärschlüssel das Attribut id der Java-Klasse mit dem gleichnamigen Attribut der Tabelle verknüpft. • Weiterhin erfolgen die Abbildungen für customerID und total. Handout S. 8 24 / 35 Objekt-relationales Mapping Hibernate Vorlesung 12 Zweiter Ansatz für Invoice In Wirklichkeit möchte man in der Klasse Invoice statt der reinen Kundennr. lieber direkt eine Referenz auf den Kunden haben: public class Invoice { private Long id; private Customer customer; private Double total; public Invoice() { } // Getter und Setter } 25 / 35 Objekt-relationales Mapping Hibernate Mapping Dies ist ein Fall einer Zuordnung many-to-one: Mehrere Rechnungen können für denselben Kunden ausgestellt sein. <?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping package="fibu"> <class name="Invoice" table="Invoice"> <id name="id" column="id"> <generator class="native"></generator> </id> <property name="total"></property> <many-to-one name="customer"> <column name="customerID"></column> </many-to-one> </class> 26 / 35 Objekt-relationales Mapping Hibernate Mapping (Forts.) <class name="Customer" table="Customer"> <id name="id" column="id"> <generator class="native"></generator> </id> <property name="firstname"></property> <property name="lastname"></property> <property name="street"></property> <property name="city"></property> </class> </hibernate-mapping> Die Zuordnung des Kunden zur Rechnung erfolgt dabei durch die Angabe der Spalte in der Tabelle Invoice, in der sich der Primärschlüssel des betreffenden Kunden befindet. Handout S. 9 27 / 35 Objekt-relationales Mapping Hibernate Vorlesung 12 Mapping der Rechnungspositionen Einer Rechnung sind in der Regel viele Rechnungsposten zugeordnet. Die Zuordnung erfolgt durch die Spalte invoiceId in der Tabelle Item. Hierfür wird in der Klasse Invoice nun eine Liste von Item neu hinzugefügt, die auf den Mapping-Typ list abgebildet wird. Hierbei wird in der Abbildung die Spalte invoiceID als key verwendet, anhand dessen die passenden Rechnungsposten ausgewählt werden. Zusätzlich wird direkt die Tabelle Product anhand der Produktnr. des Rechnunsposten in die Klasse Item abgebildet. 28 / 35 Objekt-relationales Mapping Hibernate Klasse Invoice public class Invoice { private Long id; private Customer customer; private List items = new ArrayList(); private Double total; public Invoice() { } // Getter und Setter ... public List getItems() { return items; } public void setItems(List items) { this.items = items; } } 29 / 35 Objekt-relationales Mapping Hibernate Klasse Item public class Item { private Product product; private Double cost; private Long quantity; public Item() {} // Getter und Setter public Product getProduct() { return product; } public void setProduct(Product product) { this.product = product; } ... } Handout S. 10 30 / 35 Objekt-relationales Mapping Hibernate Vorlesung 12 Klasse Customer public class Customer { private Long id; private String firstname; private String lastname; private String street; private String city; public Customer() {} // Getter und Setter ... @Override public String toString() { return String.format("%s %s; %s; %s", getFirstname(), getLastname(), getStreet(), getCity()); } } 31 / 35 Objekt-relationales Mapping Hibernate Klasse Product public class Product { private Long id; private String name; private Double price; public Product() {} // Getter und Setter public Long getId() { return id; } public void setId(Long id) { this.id = id; } ... } 32 / 35 Objekt-relationales Mapping Hibernate Mapping <hibernate-mapping package="fibu"> <class name="Invoice" table="Invoice"> <id name="id" column="id"> <generator class="native"></generator> </id> <property name="total"></property> <many-to-one name="customer"> <column name="customerID"></column> </many-to-one> <list name="items" table="item"> <key column="invoiceID"></key> <list-index column="item"></list-index> <composite-element class="Item"> <property name="quantity"></property> <property name="cost"></property> <many-to-one name="product" column="productID"/> </composite-element> </list> </class> Handout S. 11 33 / 35 Objekt-relationales Mapping Hibernate Vorlesung 12 Mapping (Forts.) <class name="Customer" table="Customer"> <id name="id" column="id"> <generator class="native"></generator> </id> <property name="firstname"></property> <property name="lastname"></property> <property name="street"></property> <property name="city"></property> </class> <class name="Product" table="Product"> <id name="id"></id> <property name="name"></property> <property name="price"></property> </class> </hibernate-mapping> 34 / 35 Objekt-relationales Mapping Hibernate Anwendung Session session = HibernateUtil.getSessionFactory() .getCurrentSession(); session.beginTransaction(); Invoice i = (Invoice) session.get(Invoice.class, 3L); System.out.println(i.getCustomer()); for (Object o : i.getItems()) { final Item it = (Item) o; System.out.println(it.getCost()); System.out.println(it.getProduct().getName()); } Customer c = (Customer) session.get(Customer.class, 6L); i.setCustomer(c); final Item it = (Item) i.getItems().get(0); it.setQuantity(5L); session.getTransaction().commit(); 35 / 35 Handout S. 12