Wir hatten in den ersten drei Teilen (1, 2, 3) der Serie eine Vaadin-Anwendung ausführlich unter Last gesetzt und vor allem ein Bottleneck festgestellt: das fehlende Paging (man könnte auch sagen: Lazy Loading) im Backend der Anwendung. Dieser Artikel diskutiert nun die Umsetzung von Lazy Loading in Vaadin und stellt die Test-Ergebnisse mit und ohne Lazy Loading gegenüber.
Es lassen sich drei Aspekte von Lazy Loading im Kontext von Server-seitigen Frameworks wie Vaadin unterscheiden:
- Nur wirklich nötige Zeilen zwischen Server und Client synchronisieren
- Nur wirklich nötige Zeilen auf Server-Seite ermitteln und laden
- Fähigkeit der Datenbank, einen Ausschnitt der Relation zu liefern
Die folgende Darstellung verdeutlicht die Problemstellung:
Der erste Punkt (nur nötige Zeilen über das Netzwerk zum Client übertragen) ist von Vaadin intrinsisch gelöst – sprich: es wird immer und ohne weiteres Zutun nur das benötigte Minimum übertragen. Für den Entwickler ist damit grundsätzlich keine weitere Aktion nötig. Ebenso sind so gut wie alle relationalen Datenbanken in der Lage, einen Ausschnitt der Relation zu liefern. Dass sich das SQL zwischen den Datenbanken (leider) unterscheidet wird durch Frameworks wie JPA mitigiert (setFirstResult /setMaxResults).
Damit bleibt der Punkt Nummer zwei von oben zu lösen (in der Grafik gestrichelt umrandet): die Frage also, wie auf Server-Seite die benötigten Zellen ermittelt und selektiv aus den darunter liegenden Schichten ermittelt werden können. Man könnte auch sagen: auf Server-Seite zu ermitteln, wie weit der Benutzer herunter gescrolled hat und den aktuell sichtbaren Ausschnitt aus der Datenbank zu laden.
Die Vorteile eines durchgängig selektiven Ladens werden sehr schnell evident, wenn man sich eine größere Tabelle vor Augen hält: das Laden von 10.000 Records ist bereits fast prohibitiv Ressourcen-intensiv während der indizierte Zugriff selbst in Millionen Records (durch logarithmisches Wachstum) sehr wenig Ressourcen verbraucht (idealtypisch ca. 20 Vergleiche für die Positionierung innerhalb einer Million Records – abgeschätzt per Logarithmus dualis).
In Vaadin bieten sich dafür mehrere Lösungen an, die in der folgenden Tabelle kurz gegenübergestellt werden:
Ansatz | Vorteile | Nachteile |
Einhängen in den Lebenszyklus des Server State | Keine weitere Bibliothek nötig, schnelle + kompakte Lösung des Problems unter Wahrung von sauberer Schichten-Architektur | Zusätzliches Einhängen eines Listeners nötig, der das Scrollen des Containers abfängt. |
Nutzung eines Standard-Containers | Offiziell (im Book of Vaadin) dokumentierter Weg, sehr hohe Kapselung | Umgehen einer (sauberen) Schichten-Architektur – da direkter Zugriff auf die Datenbank (SQL-Container) bzw. die Persistenz (JPA-Container) |
Lazy Query Container (unter Apache License verfügbar und auch im Code kompakt) | Sauberes Einhalten einer Schichtenarchitektur, durchgängige Verwendung der Vaadin-Architektur (Datenbindung), Lösung an einer Stelle, Callbacks | Zusätzliche Komponente (aber: Open Source + kompakt) |
Ein Umgehen der Schichten-Architektur scheidet aus unserer Sicht sehr schnell aus. Das Einhängen in den Lebenszyklus des Server ist hier klar überlegen – und auch wenn diese Lösung (erprobterweise) funktioniert, so mangelt es ihr doch an Eleganz.
Der LazyQueryContainer ist ein Container, d.h. eine Datenquelle, an die z.B. Tabellen gebunden werden können. In einer MVP-Architektur (wie auch Officewerker eine ist) ist dieser Container (wie möglichst alle anderen Framework-abhängigen Klassen auch) Teil des View.
Um nun den LazyQueryContainer einzusetzen ist dieser als „ContainerDataSource“ in die Tabelle zu setzen:
[sourcecode language=”java”]
Table tableItems = new Table();
/* this implementiert QueryFactory für Callbacks, die in ID_PROP_NAME genannte Property ist für jedes Item eine eindeutige ID, nach 50 nachladen, false = keine verschachtelten Items */
LazyQueryContainer items = new LazyQueryContainer(this, ID_PROP_NAME, 50, false);
// allPropertyNames – z.B. über getItemPropertyIds() des Item (ggf. Prototyp-Item anlegen)
for(String propertyName : allPropertyNames) {
items.addContainerProperty(propertyName, Long.class, null);
}
tableItems.setContainerDataSource(this.items);
// allVisiblePropertyNames ist ein Subset von allPropertyNames
this.tableItems.setVisibleColumns(allVisiblePropertyNames);
[/sourcecode]
Mit diesen Schritten ist die Vaadin-Table mit dem LazyQueryContainer verbunden. Im obigen Beispiel wurde über this die Implementierung des Callback übergeben. Statt this wäre auch jedes andere Objekt denkbar – in jedem Fall aber muss die Schnittstelle org.vaadin.addons.lazyquerycontainer.QueryFactory implementiert werden. Wir haben das wie folgt gelöst (die Liste ist damit Read-Only – neue Items sind entweder nicht möglich oder benötigen im entsprechenden Code einen Neu-Aufbau der Tabelle. Letzteres ist fast immer ausreichend):
[sourcecode language=”java”]
@Override
public Query constructQuery(QueryDefinition arg0) {
return new QueryImpl();
}
private class QueryImpl implements Query {
@Override
public Item constructItem() {
throw new UnsupportedOperationException("Kein Anlegen erlaubt");
}
@Override
public boolean deleteAllItems() {
throw new UnsupportedOperationException("Kein Löschen erlaubt");
}
@Override
public List<Item> loadItems(int startIndex, int count) {
// der Presenter registriert sich als Observer
List<Invoice> invoiceList = observer.loadItems(startIndex, count);
List<Item> resList = new ArrayList<Item>(invoiceList.size());
for (Invoice invoice : invoiceList) {
// wir brauchen wieder eine Item-Instanz
resList.add(obtainItemFromBean(invoice));
}
return resList;
}
@Override
public void saveItems(List<Item> arg0, List<Item> arg1, List<Item> arg2) {
throw new UnsupportedOperationException("Kein Speichern erlaubt");
}
@Override
public int size() {
// der Presenter registriert sich als Observer
return observer.size();
}
}
[/sourcecode]
Damit bekommen wir eine saubere Schichten- und MVP-Architektur hin: der Presenter registriert sich als Observer an der hier dargestellten View – und stellt die sehr geradlinige Methoden loadItems(startIndex, count) und size() zur Verfügung. Der Presenter wiederum kann über die Backend-Schichten an die Daten gelangen.
Auf den ersten Blick ist loadItems() nicht das, was man von einem Observer erwarten würde. Genauso erscheint das proaktive Abholen von Daten durch die View im Kontext von MVP auf den ersten Blick etwas seltsam. Auf den zweiten Blick allerdings wird das MVP-Muster sehr gut eingehalten: alle über die Anzeige hinausgehenden Funktionen werden im Presenter gekapselt. Auch bleibt der Presenter Unit-testbar (so dies nötig ist). Dem Presenter bleibt die Kontrolle über fachliche und inhaltliche Zusammenhänge – genau wie bei einem „buttonWasClicked“ oder einer anderen in MVP üblichen Methode. Dass wir die Daten als Return-Wert statt via Setter hineinreichen wird damit zum Detail.
Über die Verwendung von Callbacks haben wir allerdings die Möglichkeit geschaffen, Daten erst dann aus der Datenbank zu holen, wenn diese im Browser des Benutzers wirklich gebraucht werden.
Dies sollte sich massiv auf den Speicherverbrauch auswirken: Wiederholt man die Messung des Speicherverbrauchs mit 100 Threads lokal und misst im laufenden Test die Anzahl der Instanzen, so erhält man (vgl. Abbildung) noch 1900 Instanzen:
Das bedeutet auch: wir liegen mehr als zwei Zehnerpotenzen unter dem Ausgangswert (von 900.000) – und haben absolut betrachtet eine komfortabel handhabbare Instanz-Zahl.
Mit anderen Worten: auch große Datenmengen lassen sich über Lazy Loading gut beherrschen. Da immer nur der sichtbare Teil + ein kleiner Puffer geladen wird hätten wir auch bei noch mehr Testdatensätzen kein anderes Ergebnis mehr – und skalieren damit sehr gut.
Du muss angemeldet sein, um einen Kommentar zu veröffentlichen.