23. November 2015
Denny Israel
0

JavaFX Persistence – Teil 3 (JPA Lazy Loading)

Im ersten Teil und zweiten Teil haben wir uns angesehen, wie man Instanzen von – mit JavaFX Properties versehen – Klassen mittels JAXB und JPA persistieren kann. Durch Umstellung der Zugriffsart auf die Nutzung von Gettern/Setter, können sowohl JAXB als auch JPA mit JavaFX Properties umgehen.

Im dritten Teil möchte ich darauf eingehen, wie man JPA dazu bringt ObservableLists korrekt zu verarbeiten und was man bei der Nutzung von Lazy Loading beachten sollte. Das Beispiel der ersten beiden Teile wird dazu leicht abgeändert und besteht nun aus einer Master-Detail-Ansicht, welche Manufacturer auf der einen und Cars auf der anderen Seite zeigt. Jeder Manufacturer hat eine Liste von Cars. Diese Liste möchten wir mit JPA speichern und lazy laden.

Der Grundgedanke, die Zugriffe auf Getter/Setter umzustellen ist auch hier notwendig. Der JPA Provider beschwert sich nun jedoch, dass er den Typ ObservableList nicht instanziieren kann.

Exception Description: The class [interface javafx.collections.ObservableList] cannot be used as the container for the results of a query because it cannot be instantiated.

Wir müssen JPA also dazu überreden, diese ObservableList als normale Liste anzusehen, ohne jedoch die Observable-Fähigkeiten zu verlieren.

Hierzu wenden wir einen neuen Trick an und übersetzen die von JPA gelieferte Liste in eine ObservableList mit der wir weiterarbeiten. Da sowohl Getter als auch Setter in den Augen von JPA mit dem gleichen Typ arbeiten müssen (andernfalls meckert JPA, dass es zu dem gegebenen Getter keinen Setter gibt), ist es nötig, Getter und Setter auf List umzustellen, wodurch unsere View sich jedoch nicht mehr daran binden kann. Für Abhilfe sorgt eine weitere Getter-Methode, die die Liste als ObservableList zurückliefert.

public ObservableList<Car> getCarsObservable() {
 return observableCars;
}

@OneToMany(orphanRemoval = true, cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)
public List<Car> getCars() {
 return observableCars;
}

public void setCars(final List<Car> cars) {
 this.observableCars = FXCollections.observableList(cars);
}

Damit kann die JPA arbeiten und wir haben gleichzeitig die Observable-Fähigkeiten für die View erhalten.

Doch wie sieht es mit dem Lazy Loading aus? Wir haben den FetchType auf „LAZY“ gestellt und erwarten, dass die Liste erst geladen wird, wenn wir damit etwas machen, z.B. an eine ListView binden. Wenn wir uns jedoch das JPA-Log der gegebenen Implementierung ansehen (das Beispiel verwendet Eclipselink, dessen Logginglevel mit folgender Umgebungsvariable eingestellt werden kann: -Declipselink.logging.level=FINE ), erkennen wir, dass alle Daten beim initialen Laden mitgeladen werden. Als Beispiel habe ich zwei Manufacturer angelegt mit jeweils ein paar Cars. Beim Start der Anwendung werden die Manufacturer geladen und einer wird selektiert, wodurch die Cars in der Detailansicht dargestellt werden. Durch Lazy Loading würde man erwarten, dass es nur einen Aufruf zum Laden der Cars des ersten Manufacturers gibt. Tatsächlich, gibt es zwei Aufrufe:

[EL Fine]: sql: 2015-11-15 11:26:34.788--ServerSession(1987787569)--Connection(1298739264)--Thread(Thread[JavaFX Application Thread,5,main])--SELECT t1.ID, t1.NAME FROM MANUFACTURER_CAR t0, CAR t1 WHERE ((t0.Manufacturer_ID = ?) AND (t1.ID = t0.cars_ID))
  bind => [1]
[EL Fine]: sql: 2015-11-15 11:26:34.817--ServerSession(1987787569)--Connection(1298739264)--Thread(Thread[JavaFX Application Thread,5,main])--SELECT t1.ID, t1.NAME FROM MANUFACTURER_CAR t0, CAR t1 WHERE ((t0.Manufacturer_ID = ?) AND (t1.ID = t0.cars_ID))
  bind => [2]

Aber warum ist das so? Lazy Loading lädt die Elemente der Liste nach, sobald auf diese zugegriffen wird. Wenn man sich mal im Debugger ansieht, welche Methoden wann aufgerufen werden wird man feststellen, dass die JPA beim Laden sowohl Setter als auch Getter aufruft und auf der vom Getter gelieferten Liste noch Aktionen ausführt. Die JPA erwartet verständlicherweise, dass es sich um dieselbe Liste handelt und nicht um eine neue ObservableList, wie wir sie zurückliefern. Die Mechanismen die dazu führen, dass die Liste lazy nachgeladen wird, greifen dadurch nicht mehr und die Kapselung der Liste in eine ObservableList führt zum sofotigen Nachladen der Inhalte.

Um dies zu umgehen, müssen wir die von der JPA gesetzte Liste im Getter wieder zur Verfügung stellen. Wir wollen aber gleichzeitig verhindern, dass die restliche Programmlogik und insbesondere die View diese Liste verwendet, da Manipulationen sonst nicht überwacht werden und die ObservableList ihren Dienst nicht tun kann.

Als Trick speichern wir die JPA-Liste als Instanzvariable und geben diese im Getter zurück. Gleichzeitig initialisieren wir die ObservableList beim Setzen der Liste und geben diese im Observable Getter zurück:

@Transient
private List<Car> cars;
@Transient
private final ListProperty<Car> observableCars = new SimpleListProperty<>(FXCollections.observableArrayList());

@Transient
public ListProperty<Car> carsProperty() {
 return observableCars;
}

@OneToMany(orphanRemoval = true, cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)
private List<Car> getCars() {
 return cars;
}

private void setCars(final List<Car> cars) {
 this.cars = cars;
 this.observableCars.set(FXCollections.observableList(cars));
}

Wenn wir uns nun die Ausgaben des Logs ansehen, erscheint die zweite Ausschrift über das Nachladen der Carliste erst, wenn der Manufacturer in der GUI ausgewählt wird.

Als Detail fällt auf, dass die ObservableList nicht mehr ersetzt wird, sondern in eine ListProperty eingesetzt wird. Der Grund dafür ist das Speicherverhalten von JPS. Wenn Änderungen an einem Manufacturer-Objekt gespeichert werden, indem auf dem EntityManager merge aufgerufen wird, setzt die JPA die Carliste erneut. Wenn wir jedesmal die ObservableList ersetzen, verlieren die Bindings in der GUI ihre Gültigkeit, da sie mit der alten Instanz der ObservableList vorgenommen wurden. Eine ListProperty sorgt dafür, dass die Werte von daran gebundenen Listen immer synchron gehalten werden, auch wenn die zugrunde liegende Liste ersetzt wird.

Tipps und Tricks:

Um sicherzustellen, dass im Rest der Anwendung die ObservableList verwendet wird, kann man die Getter und Setter für die normale Liste als private deklarieren, da sich die JPA daran nicht stört.

Da das Laden unter Umständen einige Zeit beanspruchen kann, sollte sowohl das initale Laden, als auch das Nachladen asynchron zur GUI Logik ausgeführt werden, da diese sonst einfriert. Dies kann bei JavaFX mit Tasks und Services erreicht werden: https://docs.oracle.com/javafx/2/threads/jfxpub-threads.htm. Um das Lazy Loading asynchron auszuführen, sollte der erste Zugriff auf die lazy geladene Liste in einem Extra-Thread erfolgen. Hierbei ist natürlich auf Race Conditions innerhalb des Models zu achten, da die ObservableLists nicht Threadsafe sind. Es gibt auch synchronisierte Versionen, siehe FXCollections#synchronizedObservableList.

Beim Verpacken der Liste in eine ObservableList muss darauf geachtet werden, wie sich die Methoden von FXCollections verhalten. Verwendet man beispielsweise statt observableList observableArrayList, zerstört man die Verbindung zwischen der JPA-Liste und der ObservableList, da die Elemente der Liste bei der Konstruktion in eine neue ArrayList kopiert werden und nicht wie bei observableList die gegebene Liste als Backing-List fungiert.

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

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