23. Oktober 2015
Denny Israel
1

JavaFX Persistence – Teil 2 (JPA)

Im ersten Teil habe ich gezeigt, wie man Instanzen von – mit JavaFX Properties versehen – Klassen mittels JAXB persistieren kann. Durch Umstellung der JAXB Zugriffsart auf die Nutzung von Getter/Setter, kann auch JAXB mit JavaFX Properties umgehen und man kann alle Vorteile auch im Model nutzen. Im zweiten Teil möchte ich zeigen, wie man Instanzen solcher Klassen mittels JPA in eine Datenbank speichern und aus ihr lesen kann.

Zunächst besteht wieder das Problem, dass der übliche Zugriff auf die Felder der Klassen direkt über Reflection erfolgt. Der verwendete OR-Mapper wird also in den zu speichernden Objekten nach Feldern suchen und diese direkt auslesen. Da es sich jedoch nicht um primitive Typen handelt, sondern um komplexe Properties (z.B. StringProperty statt String), scheitert dieser Versuch. Wir können den gleichen Kniff wie in der Arbeit mit JAXB verwenden. JPA ist ebenfalls in der Lage statt reflektiv die Eigenschaften zu lesen und zu schreiben, die Getter und Setter der Eigenschaft zu verwenden und diese zu lesen bzw. zu schreiben, da diese trotz der Verwendung von JavaFX Properties den eigentlichen Wert, also ein String im Falle einer StringProperty, zurückgeben. Mit folgender Annotation kann die Zugriffsart entsprechend umgestellt werden:

@Access(AccessType.PROPERTY)

Die Besonderheit an dieser Annotation besteht darin, dass sie entweder an der Klasse oder am Getter der Eigenschaft definiert werden kann. Definiert man sie an der Klasse, müssen alle Felder per Getter/Setter erreichbar sein und man bekommt Probleme beim Einsatz von Annotationen wie @Id. Daher habe ich mich im Beispiel der Einfachheit halber dafür entschieden, den Zugriffstyp auf Klassenebene bei @Access(AccessType.FIELD) zu belassen und die Getter der Property-Felder mit @Access(AccessType.PROPERTY) zu annotieren. Bei dieser Kombination von Field- und Property-Zugriff beschwert sich der OR-Mapper mit einer Warnung im Log. Um den OR-Mapper zufrieden zu stellen, sollte man die Felder, für die man Property-Zugriff konfiguriert hat mit @Transient markieren.

Die Modell-Klasse Car sieht nun wie folgt aus:

import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Transient;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

@Entity
@Access(AccessType.FIELD)
public class Car {

 @Id
 @GeneratedValue(strategy = GenerationType.IDENTITY)
 private Long id;
 @Transient
 private final StringProperty manufacturer = new SimpleStringProperty();
 @Transient
 private final StringProperty model = new SimpleStringProperty();

 public Long getId() {
  return id;
 }

 public void setId(final Long id) {
  this.id = id;
 }

 public final StringProperty manufacturerProperty() {
  return this.manufacturer;
 }

 @Access(AccessType.PROPERTY)
 public final String getManufacturer() {
  return this.manufacturerProperty().get();
 }

 public final void setManufacturer(final String manufacturer) {
  this.manufacturerProperty().set(manufacturer);
 }

 public final StringProperty modelProperty() {
  return this.model;
 }

 @Access(AccessType.PROPERTY)
 public final String getModel() {
  return this.modelProperty().get();
 }

 public final void setModel(final String model) {
  this.modelProperty().set(model);
 }
}

Die Klasse CarList kann hingegen von ihren Annotationen befreit werden, da sie im Beispiel nicht durch JPA gespeichert wird. Eine Anwendung dieses Prinzips auf Listen oder andere durch JPA verwaltete Objekte ist jedoch genauso möglich. In diesem Fall gibt man einen Getter auf entsprechende ListProperties oder ObjectProperties an.

Bevor wir uns die Handhabung von Car-Objekten in der GUI-Logik ansehen, werfen wir einen Blick auf den Datenbankzugriff (ohne Garantie auf Eleganz und Effizienz ;-)):

import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.persistence.Query;

public class CarDb {
 private static final String PERSISTENCE_UNIT_NAME = "cars";
 private static final EntityManagerFactory factory = Persistence.createEntityManagerFactory(PERSISTENCE_UNIT_NAME);

 public List<Car> readCars() {
  final EntityManager em = factory.createEntityManager();
  try {
   final Query q = em.createQuery("select c from Car c");
   final List<Car> cars = q.getResultList();
   return cars;
  } finally {
   em.close();
  }
 }

 public void saveCar(final Car newCar) {
  final EntityManager em = factory.createEntityManager();
  try {
   em.getTransaction().begin();
   em.persist(newCar);
   em.getTransaction().commit();
  } finally {
   em.close();
  }
 }

 public void deleteCar(final Car carToDelete) {
  final EntityManager em = factory.createEntityManager();
  try {
   em.getTransaction().begin();
   em.remove(em.merge(carToDelete));
   em.getTransaction().commit();
  } finally {
   em.close();
  }
 }
}

Und die dazugehörige persistence.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<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="cars" transaction-type="RESOURCE_LOCAL">
  <class>de.saxsys.javafx.persistence.Car</class>
  <properties>
   <property name="javax.persistence.jdbc.driver" value="org.apache.derby.jdbc.EmbeddedDriver" />
   <property name="javax.persistence.jdbc.url" value="jdbc:derby:carsDb;create=true" />
   <property name="javax.persistence.jdbc.user" value="test" />
   <property name="javax.persistence.jdbc.password" value="test" />
   <property name="eclipselink.ddl-generation" value="create-tables" />
   <property name="eclipselink.ddl-generation.output-mode"
    value="database" />
  </properties>
 </persistence-unit>
</persistence>

Die Klasse CarDb kapselt den DB Zugriff und bietet Methoden zum Speichern und Laden von Car-Objekten an. Diese Klasse wird im nächsten Schritt in der GUI-Logik benutzt. Für weitere Informationen gibt es hier ein interessantes Tutorial: http://www.vogella.com/tutorials/JavaPersistenceAPI/article.html.

Nun aber zur GUI-Logik die sich gegenüber dem JAXB Beispiel ebenfalls leicht geändert hat. Die Methoden für Speichern und Laden der Liste gibt es nicht mehr, da die Car-Objekte nun sofort beim Anlegen in die Datenbank gespeichert werden und beim Löschen daraus entfernt werden. Darüber hinaus liegt die Instanz von CarList nicht mehr in einer ListProperty, da sie nicht mehr ausgetauscht wird, sondern sich lediglich der Inhalt verändert.

import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Random;

import javafx.collections.ListChangeListener;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.ListView;

public class CarView {
 
 …

 private final CarList cars = new CarList();
 private final CarDb carDb = new CarDb();

 @FXML
 private ListView<Car> lvCars;

 @FXML
 public void initialize() {
  lvCars.setCellFactory(param -> new CarListCell());
  lvCars.itemsProperty().bind(cars.carsProperty());
  cars.getCars().setAll(carDb.readCars());
  cars.getCars().addListener((ListChangeListener<Car>) c -> {
   while (c.next()) {
    if (c.wasAdded()) {
     c.getAddedSubList().stream().forEach(carDb::saveCar);
    }
    if (c.wasRemoved()) {
     c.getRemoved().stream().forEach(carDb::deleteCar);
    }
   }
  });
 }

 @FXML
 public void addCar(final ActionEvent event) {
  …
 }

 @FXML
 public void removeCar(final ActionEvent event) {
  …
 }
}

Die Liste der Car-Objekte wird initial mit cars.getCars().setAll(carDb.readCars()); aus der DB geladen. Jede Änderung an der Liste wird durch einen ListChangeListener abgefangen und entsprechend umgesetzt. Dabei werden hinzugefügte Car-Objekte mit c.getAddedSubList() an die Methode saveCar weitergeleitet und entfernte Car-Objekte werden mit c.getRemoved() an deleteCar weitergeleitet.

Das vollständige Beispiel ist wieder auf Github zu finden: https://github.com/sideisra/javafx-persistence/tree/jpa.

Denny ist Senior Consultant für Softwareentwicklung im Bereich Java und JavaFX. Er beschäftigt sich vornehmlich mit allen Aspekten der Entwicklung von JavaFX Anwendungen, von der Oberfläche über die Architektur bis hin zum Backend und der Anbindung an andere Systeme. Darüber hinaus interessiert er sich für moderne Web-Technologien wie React und Angular 2.

Twitter Xing 

TeilenTweet about this on TwitterShare on Facebook0Share on Google+0Share on LinkedIn0

1 Kommentar

Kommentare sind geschlossen.