Gastbeitrag: SQL als "interne DSL" in Java mit jOOQ

Vor einiger Zeit hat Christian auf seinem Blog das fluent API seiner jRTF-Library vorgestellt, welche dem Benutzer ermöglicht, RTF-Dokumente mit einfachen Sprach-Konstrukten direkt mit Java zu erstellen:

DSL (Domain Specific Languages) sind ein spannendes Thema in vielen Bereichen. Für die meisten Zwecke werden sogenannte "externe" DSL’s definiert, also für einen technischen oder fachlichen Bereich spezialisierte Sprachen, welche unabhängig von anderen Sprachen interpretiert oder kompiliert werden. Manchmal gelingt es aber auch, solche spezialisierten Sprachen in einer anderen Sprache zu "internalisieren", wie dies eben mit jRTF gelungen ist. Heute möchte ich eine ähnliche DSL präsentieren, welche ich in jOOQ (Java Object Oriented Querying) eingebaut habe. jOOQ bildet SQL als "interne DSL" in Java ab, so dass Datenbank-Abfragen direkt in Java formuliert werden können. Ein Beispiel:

// Suche alle Bücher, welche in 2011 publiziert wurden
create.select()
      .from(BOOK)
      .where(PUBLISHED_IN.equal(2011))
      .orderBy(TITLE)
      .fetch();

Dabei steht BOOK für eine Tabelle mit den Feldern PUBLISHED_IN und TITLE. Alle diese Objekte werden von jOOQ generiert.

SQL – Grundlagen und Motivation für jOOQ

SQL ist eine deklarative "externe DSL", welche von einem Datenbanksystem in beliebiger Form interpretiert werden kann, um das gewünschte Resultat zu produzieren. So kann eine vereinfachte SELECT-Syntax beispielsweise wie folgt definiert werden:

SELECT [ ALL | DISTINCT [ ON ( ausdruck [, ...] ) ] ] * | 
        ausdruck [ AS ausgabename ] [, ...]
    [ FROM from_element [, ...] ]
    [ WHERE bedingung ]
    [ GROUP BY ausdruck [, ...] ]
    [ HAVING bedingung [, ...] ]
    [ { UNION | INTERSECT | EXCEPT } [ ALL ] select ]
    [ ORDER BY ausdruck [ ASC | DESC | USING operator ] [, ...] ]
    [ LIMIT { anzahl | ALL } ]
    [ OFFSET start ]
    [ FOR UPDATE [ OF tabellenname [, ...] ] ]

Aus dem Postgres-Handbuch:

http://www.postgresql.org/files/documentation/books/pghandbuch/html/sql-select.html

Andere Datenbanken, wie z.B. Oracle, haben noch eine viel komplexere Syntax. Es werden zum Beispiel auch Elemente wie [WITH ..], [CONNECT BY ..], [PIVOT ..], [UNPIVOT ..] und so weiter unterstützt. Auf Details wird hier verzichtet.

Für viele Java-Entwickler ist SQL etwas Externes und wird oft missverstanden, und gemieden. SQL funktioniert grundlegend anders als die meisten prozeduralen oder objekt-orientierten Programmiersprachen. Die Vielfalt von SQL-Dialekten und deren enorm nützlicher Funktionalität wird unter anderem genau deswegen von den führenden Datenbank-Abstraktions-Frameworks hinter massiven Vereinfachungen versteckt. Zum Beispiel hat Hibernate HQL und criteria queries entwickelt, welche nur sehr rudimentäre Abfragen ermöglichen.

jOOQ geht einen anderen Weg und möchte dem SQL-orientierten Entwickler erlauben, sämtliche Syntax-Konstrukte direkt anzuwenden. Dabei macht sich jOOQ die wichtigsten Eigenschaften einer internen DSL zu nutze.

  • Die SQL-Abfragen können vom Java Compiler formell auf Syntax und Typsicherheit überprüft werden.
  • IDE’s wie Eclipse können mit Auto-Vervollständigung helfen, korrektes SQL zu schreiben
  • Unterstützung der gesamten Standard-SQL Syntax, sowie einiger proprietärer Konstrukte. Es gibt keine künstliche Grenze, wie z.B. mit HQL.

Internalisierung einer externen DSL

In diesem Artikel möchte ich aufzeigen, wie eine formell definierte, externe DSL in Java mittels einfacher Regeln "internalisiert" werden kann, und wie dies für SQL in jOOQ gelöst worden ist. Aber zuerst zu den Grundsätzen:

1. Versteckte Implementierung

Der wichtigste Schritt ist der allererste. Eine "interne" DSL ist schwer vorstellbar, ohne das Abstraktionsvermögen von Java’s Schnittstellen zu verwenden. Um eine Implementierung der verschiedenen DSL-Schnittstellen zu konstruieren benötigen wir eine Factory. Je nach Funktionalitätsbedarf kann die Factory statische oder nicht-statische Methoden anbieten. Der Vorteil von statischen Methoden ist die Möglichkeit der Verwendung von static imports, welche den Code noch lesbarer machen. Der Vorteil von nicht-statischen Methoden ist die Möglichkeit, dass die konstruierten Objekte eine Referenz auf die Factory halten können.

2. Schlüsselwörter

Idealerweise wird jedes Schlüsselwort der externen DSL durch eine Java-Methode "internalisiert". Zusammengesetzte Schlüsselwörter (wie z.B. GROUP BY) werden dabei wie in Java üblich durch "Camel Case" zusammengesetzt (also z.B. groupBy). Somit werden die SQL-Schlüsselwörter

SELECT .. FROM .. JOIN .. ON .. WHERE .. GROUP BY

durch die entsprechenden Methoden modelliert:

.select(...).from(...).join(...).on(...).where(...).groupBy(...)

3. Sonderzeichen

Die externe DSL spezifiziert oft Sonderzeichen, wie ( ) || + - und so weiter. Diese Sonderzeichen können in Java nicht abgebildet werden. Andere Sprachen, wie z.B. C++, C# haben mächtigere Mittel um auch Operatoren zu überladen. In Java werden sie am besten einfach ignoriert, oder in extremeren Sonderfällen ausformuliert – also .parentheses() .closingParentheses() .concat() .plus() .minus()

4. Klauseln

Eine Klausel wird meist in einer Form ähnlich dieser Formen hier formuliert:

a) EIN EINZELNES WORT
b) EIN PARAMETRISIERTES WORT parameter
c) EIN WORT [ EIN OPTIONALES WORT ]
d) EIN WORT { EIN WAHLWORT | EIN ANDERES WAHLWORT }
e) EIN WORT [ , EIN WORT ... ]

Die Internalisierung dieser Definitionen erfolgt so:

a) Die Grundklausel
class Factory {
    // Nach EIN EINZELNES WORT geht's nicht mehr weiter.
    // Es können keine weiteren Klauseln angehängt werden
    Schluss einEinzelnesWort();
}

interface Schluss {
    // Hier können grundsätzliche Methoden der DSL deklariert werden
    // z.B. execute(), run(), etc
    void execute();
}

// Anwendung
new Factory().einEinzelnesWort().execute();
b) Eine Klausel mit Parameter, z.B. einem numerischen Parameter
class Factory {
    // Bei "kompatibler" Syntax kann die "Schluss-Schnittstelle"
    // wiederverwendet werden
    Schluss einParametrisiertesWort(int parameter);
}

// Anwendung
new Factory().einParametrisiertesWort(42).execute();

c) Optionale Klauseln
class Factory {
    // Nach EIN WORT können noch weitere Klauseln hinzugefügt werden. 
    // Deswegen wird OptionalerSchritt zurückgegeben
    OptionalerSchritt einWort();
}

// Dadurch, dass OptionalerSchritt von Schluss erbt, können die hier
// deklarierten Methoden (=Schlüsselwörter) übersprungen werden
interface OptionalerSchritt extends Schluss {
    // Danach ist aber trotzdem Schluss
    Schluss einOptionalesWort();
}

// Anwendungen
new Factory().einWort().execute();
new Factory().einWort().einOptionalesWort().execute();
d) Wahl-Klauseln
class Factory {
    // Wie zuvor können nach EIN WORT weitere Klauseln
    // hinzugefügt werden
    WahlSchritt einWort();
}

// Eine Wahl ist nicht optional, es muss eine weitere Klausel
// folgen. Entsprechend erbt die Schnittstelle NICHT
interface WahlSchritt {
    // Es kann gewählt werden zwischen...
    Schluss einWahlwort();
    
    // ... und ...
    Schluss einAnderesWahlwort();
    
    // In jedem Fall gelangt man zum Schluss
}

// Anwendungen
new Factory().einWort().einWahlwort().execute();
new Factory().einWort().einAnderesWahlwort().execute();
e) Wiederholungen
class Factory {
    // Gewisse Klauseln können beliebig oft aneinandergereiht werden.
    WiederholungsSchritt einWort();
}

// Weitere Wiederholungen sind optional. Dies wird auch hier mit Vererbung
// gelöst
interface WiederholungsSchritt extends Schluss {
    // Bei Wiederholung gelangt man zurück zum Ausgangstyp
    WiederholungsSchritt einWort();
}

// Anwendungen
new Factory().einWort().execute();
new Factory().einWort().einWort().execute();
new Factory().einWort().einWort().einWort().execute();

Die genannten Regeln lassen sich beliebig kombinieren und verschachteln, so dass fast alle möglichen Syntaxbäume abbildbar sind. Dadurch kann eine relativ grosse Anzahl Schnittstellen entstehen, was für heutige Compiler und ClassLoader aber kein Problem mehr darstellen sollte.

SQL als interne DSL in jOOQ

Das für jOOQ verwendete fluent API baut auf einer Hierarchie von Schnittstellen auf, welche etwas vereinfacht so definiert ist (die tatsächliche Umsetzung weicht etwas von diesem Beispiel ab):

// Die Factory-Klasse ist der Einstiegspunkt für die DSL.
// Alle Objekte werden von hier konstruiert
class Factory {

    // Es können 1..n Felder selektiert werden
    SelectFromStep select(Field... fields);
}

// Dieser "Schritt" oder diese Klausel der SELECT Syntax
// ist obligatorisch. Man muss also .from() aufrufen
interface SelectFromStep {

    // Es kann von 1..n Tabellen selektiert werden
    SelectJoinStep from(Table... tables);
}

// Dieser "Schritt" der SELECT Syntax ist optional. Er erbt also vom nächsten
// Schritt, falls JOIN's ausgelassen würden
interface SelectJoinStep extends SelectWhereStep {

    // Eine Tabelle kann hinzugefügt werden
    SelectOnStep join(Table table);
}

// Nach einer JOIN-Klausel muss zwingend eine ON- oder USING-Klausel folgen.
// Danach ist eine weitere JOIN-Klausel möglich.
interface SelectOnStep {
    SelectJoinStep on(Condition condition);
    SelectJoinStep using(Field... fields);
}

// Auch die WHERE-Klausel ist optional.
interface SelectWhereStep extends SelectOrderByStep {

    // Der Einfachheit halber werden GROUP BY, HAVING Klauseln übersprungen.
    SelectOrderByStep where(Condition condition);
}

// Auch die ORDER BY-Klausel ist optional.
interface SelectOrderByStep extends SelectFinalStep {
    
    // Der Einfachheit halber wird hier abgebrochen
    SelectFinalStep orderBy(SortField... fields);
}

Und so weiter. Die Implementierung all dieser Schnittstellen übernimmt eine einzige, nur der Factory bekannte Klasse, welche sämtliche Methoden-Aufrufe registriert und am Schluss eine passende SQL-Abfrage als String ausführt. Mehr Details hierfür gibt’s hier:

https://sourceforge.net/apps/trac/jooq/wiki/Manual/DSL/SELECT

Interessant ist auch die Frage, wie zusammengesetzte Elemente wie Condition und SortField zustande kommen. Diese werden durch von der SELECT-Syntax unabhängige DSL’s erstellt. So bietet zum Beispiel der Typ Field folgende DSL-Methoden an:

interface Field {
    
    // Erstellen von Conditions
    Condition equal(Field field);
    Condition equal(Object value);
    Condition notEqual(Field field);
    Condition notEqual(Object value);
    // [...] und noch viele mehr
    
    // Erstellen von spezialisierten Feldern für die ORDER BY Klausel
    SortField asc();
    SortField desc();
}

Diese Typen wiederum erlauben weitere DSL-Schritte:

interface Condition {

    // Zusammensetzen von Conditions
    Condition and(Condition condition);
    Condition andNot(Condition condition);
    Condition or(Condition condition);
    Condition orNot(Condition condition);
    
    // Negieren von Conditions
    Condition not();
}

interface SortField {

    // Hinzufügen von weiteren Schlüsselwörtern
    SortField nullsFirst();
    SortField nullsLast();
}

Grenzen von internen DSL’s

Mit jOOQ ist es bereits möglich, sehr komplexe SQL-Abfragen in Java zu formulieren. Dies beinhaltet unter anderem

  • Verschachtelte Abfragen
  • Stored Procedures und Functions
  • Komplexe JOINs
  • Rekursives SQL (CONNECT BY, bald auch Common Table Expressions)
  • Window/Ranking Functions
  • Bald auch Oracle’s PIVOT/UNPIVOT Syntax

Trotzdem kann eine interne DSL nie die Mächtigkeit ihrer abgebildeten externen DSL erreichen. Die grössten Limitierungen in jOOQ sind zum Beispiel

1. Aliasing

In SQL können Tabellen und Felder einfach mit Namen versehen werden, zum Beispiel um Kinder-Elemente mit ihren Eltern-Elementen zu verknüpfen:

SELECT *
FROM my_table a, my_table b
WHERE a.id = b.parent

Dies ist in jOOQ etwas umständlicher:

Table<?> a = MY_TABLE.as("a");
Table<?> b = MY_TABLE.as("b");

create.select()
      .from(a, b)
      .where(a.getField(ID).equal(b.getField(PARENT)));

2. Grenzen der Syntax

Wie bereits erwähnt können SQL-spezifische Sonderzeichen nicht berücksichtigt werden, auch wenn diese sinnvoll für die Lesbarkeit wären. Umgekehrt fügt jeder Methodenaufruf() ein weiteres Klammern-Paar hinzu. Dies kann schon Lisp-ähnliche Zustände erreichen 🙂

Man beachte die vier Klammern am Schluss:

create.select()
      .from(BOOK)
      .where(Book.AUTHOR_ID.in(create.select(Author.ID)
                                     .from(AUTHOR)
                                     .where(Author.BORN.equal(1920))));

3. Nur die Syntax kann validiert werden, nicht die Semantik

In SQL ist es zum Beispiel nicht möglich, bei einer gruppierten Abfrage ein Feld zu selektieren, welches nicht auch in der GROUP BY Klausel Auftritt. Dies ist zum Beispiel nicht korrekt:

SELECT id
FROM book
GROUP BY title

jOOQ kann dies nicht zur Kompilierzeit überprüfen, und somit ist folgende Abfrage (in Java) möglich:

create.select(ID)
      .from(BOOK)
      .groupBy(TITLE)

Fazit

jOOQ ist ein sehr aktives Projekt und wie jRTF ein gutes Beispiel für den Nutzen eines fluent API in der modernen Java-Welt. Mit jOOQ können SQL-bewandte Programmierer wie auch SQL-Anfänger von der Typsicherheit einer internen DSL, sowie vom Funktionalitätsumfang ihrer Datenbank vollumfänglich profitieren, und dies ohne relevanten Performance-Overhead.

Weitere Details auf http://www.jooq.org

Und auf der User-Group: http://groups.google.com/group/jooq-user

Vielen Dank auch an Christian, dafür dass er mir die Möglichkeit eines Gast-Artikels gegeben hat.

Lukas Eder (lukas.eder@gmail.com)

Ähnliche Beiträge

7 Gedanken zu “Gastbeitrag: SQL als "interne DSL" in Java mit jOOQ

  1. An fast dem gleichen baue ich schon seit ein paar Jahren. Leider noch immer ohne Website, Beispiele, Doku dazu, weil ich es hauptsächlich firmenintern einsetze.

    Allerdings geht mein „SqlEngine“ genannter Ansatz weit über einfaches abbilden von SQL DSL mit Java OOP hinaus.
    So werden z.B. auch orientiert am SQL Standard DBMS-Eigenheiten wegabstrahiert (einfachstes Beispiel: FETCH FIRST aus dem Standard heißt meistens TOP oder LIMIT, usw. btw ich würde die javaseitigen Methoden nicht „limit()“ usw nennen, sondern am Standard orientieren), Tabellen können javaseitig als „Tabellenklassen“ definiert werden (mit allen indizes usw.), so dass man seine Tabellenstruktur nur noch an einer Stelle warten und dann synchronisieren lassen kann anstatt Änderungen immer an zwei Stellen (Java Code und DB) vornehmen zu müssen.

    Irgendwie ärgere ich mich grad ziemlich, immer noch keine representable Webseite zu haben, weil ich ziemlich sicher bin, dass dieser Ansatz dem hier als jOOQ beschriebenen überlegen ist.

    Ich hätte da auch einen Kritikpunkt:
    Bei allen Queryframeworks, die ich bisher gesehen habe, wird immer gleich versucht, noch ein „vierteltes OR-mapping“ mit einzubauen. So interpretiere ich zumindest das „Result books = …“ Beispiel. Das ist zwar eine naheliegende Idee, jedoch letztendlich falsch, da ein ORM auch auf Dinge wie referenzielle Integrität etc. achten muss. Und die geht schnell verloren, wenn eine „simple“ Logik die Werte aus einem ResultSet einfach nur in ein paar Objekte steckt. Wenn man die dann auf „sind ja nur domain Klassen mit simplen Daten“ reduziert verkompliziert man die OOP Verwendbarkeit nur wieder bis ins unermessliche.

    Dafür ist eine ORM Schicht über der SQL Schicht nötig. Ein SQL Framework kann nur bis zum ResultSet (oder evtl einer ResutlTable Wrapperklasse für die bessere Verwendbarkeit) gehen, nicht weiter.
    An einer ORM Schicht (und zwar der ersten „richtigen“, bei der man nicht wie bei bisherigen ORMs seinen Javacode respektive XML Files mit tonnenweise relationen Zusatzinformationen durchtränken und seine Anwendung „um“ die relational fixierten Möglichkeiten des ORMs „herumbauen“ muss. Wir setzen z.B. Hibernate in der Firma ein und es ist einfach nur ein Graus, das mit anzusehen) passend zu der SqlEngine arbeite ich auch gerade, aber da wird es sich wohl noch um Jahre handeln :(.

    Na jedenfalls guter Artikel, gute Idee 😛 und anerkennender Neid für die schon aufgesetzte Webseite 😉

  2. An Jadoth,

    > Allerdings geht mein “SqlEngine” genannter Ansatz weit über
    > einfaches abbilden von SQL DSL mit Java OOP hinaus.

    Da bin ich ja mal gespannt. Schade können wir nun nicht vergleichen… 🙂 Natürlich geht es nicht um Details wie limit oder fetch first (leider ist im Übrigen weder das eine noch das andere im Standard SQL:2008 vorhanden. Als hauptsächlicher Oracle-Nutzer warte ich schon lange darauf)

    Vielleicht ist dir die Tragweite von jOOQ nicht ganz bewusst? jOOQ beinhaltet auch:

    – Einen code generator, welcher die Dictionary Biews aller 11 bisher unterstützten Datenbanken abstrahiert und abfragt. Daraus werden Schema-Metadaten generiert.

    – SQL Dialekt Abstraktion. Was in diesem Artikel nicht beschrieben und in der Dokumentation nicht ganz offensichtlich ist: die .limit(1).offset(5) Syntax wird je nach Datenbank übersetzt in: LIMIT 5, 1 (MySQL) / LIMIT 1 OFFSET 5 (H2, HSQLDB, Postgres, SQLite) / OFFSET 5 ROWS FETCH NEXT 1 ONLY (Derby) / TOP (SQL Server, Sybase, ohne offset) / Oder aber verschachtelte SELECTS mit Filter auf ROW_NUMBER() Window Funktionen (Oracle, SQL Server, Sybase, DB2), ähnlich wie ich sie hier dokumentiert habe:

    http://stackoverflow.com/questions/6033080/speed-of-paged-queries-in-oracle

    – Im Übrigen abstrahiert jOOQ auch komplexere Konstrukte wie MySQL’s „ON DUPLICATE KEY UPDATE“ und simuliert diese mit einem SQL:2003-MERGE statement.

    – ORM: Da hast du jOOQ vielleicht missverstanden, bzw. die Dokumentation ist unklar. jOOQ hat keine ORM-Elemente, sondern simple „Active Records“. Das wissen über relationen wird vorausgesetzt und in Java nicht weiter modelliert. Hier wäre ich besonders interessiert an deiner Lösung!

    Natürlich bin ich sehr gespannt auf eine Veröffentlichung von SqlEngine und einer Webseite, die der angekündigten Überlegenheit Rechnung trägt! 🙂 Gibt es denn schon irgendwo Zugriff auf Quellcode, oder wird es sich nicht um Open Source handeln?

  3. Hi

    Danke für die Antwort. Denke aber ab einem bestimmten Punkt muss das wohl eher in Mails verlagert werden :).

    FETCH FIRST n ROW[S] ONLY ist tatsächlich inzwischen schon im SQL Standard, oder einem Randteil davon, aber sei’s drum. Das ist natürlich nur ein Detail.

    Code Generator:
    Klar. Von DB Tabellen nach Java Klassen, hab ich gelesen.
    Ich will jetzt nicht das leidige „hab ich auch alles“ Spielchen anfangen. Viel wichtiger ist eh die Richtung:
    Java Klassen -> DB Tabellen.
    Also ich meine keine ORM Projektion, sondern aus klassenmäßigen SQL Framework Representationen dann in der DB Tabellen erzeugen.
    Der große Unterschied ist nämlich:
    Damit hat man alles aus einem Guss, kann von der Logikseite aus Dinge wie Initialdaten usw reinbringen. Sogar OOP Vererbung für die Definition der Tabellen verwenden (wenn auch nicht für die Tabellen selbst in der DB).
    Beispielsweise besteht das Deployment meines Projekts in der Arbeit einfach nur aus dem Starten der Anwendung. Der Rest (Tabellen, Indizes, etc. anlegen, Initialdaten holen, usw.) geschieht automatisch auf generischem Weg.
    Auch wichtig um wie gesagt nicht ständig an zwei Seiten rumwarten zu müssen (Javaklassen aus Tabellen neu generieren lassen erzeugt halt massig Compiler Fehler und damit Nacharbeitungsaufwand. Tabellenklassen live refatoren und dann automatisiert die DB Struktur anpassen lassen hat dieses problem nicht).
    Das entzerrt dann natürlich wieder ganz enorm Programmarchitekturen, weil die DB „nur“ noch ein „dummer“ von Java aus gesteuerter Container ist und mit der Business Logik nichts mehr zu tun hat.

    Dialekt Abstraktion war das was ich knapp mit „Wegabstrahieren“ bezeichnet hatte. Geht bei DDL dann natürlich auch um Datentypen, etc.
    Das war eine der Hauptmotivationen: komplette DB-unabhängigkeit (die JDBC allein ja längst nicht liefert).
    Einfach alle SQLs abstrahiert entwickeln und wenns sein muss (hatten wir tatsächlich mal im Projekt) kann der Datenbankhersteller mit minimalem Aufwand einfach ausgetauscht werden.

    Das Substutieren von Funktionalität ist interessant. Das mach ich bisher noch nicht. Teils absichtlich, teil weil ich es in den projekten nicht brauche.

    ORM: Okay dann hab ich das missverstanden. Gut dass mal einer saubere Grenzen zwischen SQL Abfragedefinition und ORM zieht 🙂

    Jedenfalls muss ich sagen, dass es natürlich eine zweifelhafte Legitimität von mir hat, ohne Webseite, Doku, etc. trotzdem rumzukritisieren. Bzw wenn ich das schon hätte würde ich schon längst mit dem Framework hausieren gehen :D.
    Von diesem Punkt her ist jOOQ natürlich klar „überlegen“. Einfach weil es dokumentiert verwendbar ist und mein Code eigentlich nur ein Open Source firmeninternes Werkzeug ist.

    Darum ja der Ärger über mich selbst :D.

    Quellcode gibt’s hier: http://jadoth.sourceforge.net/
    Allerdings hab ich 2010 ein verbessertes Collections framework „dazwischengeschoben“, an dem ich immer noch hänge (siehe blog http://www.jadoth.net), darum längere Zwangspause.
    War wohl auch die Pause, die es ermöglicht hat, dass vergleichbare Frameworks aufkommen bevor ich meins soweit releasereif habe. Denn seit Beginn meiner Arbeiten hab ich immer wieder nach vergleichbaren Frameworks gesucht, aber nie was ernstnehmbares gefunden (bis heute ^^).

    Naja Rest per Mail, denk ich.

    Ach doch, noch was:
    Egal ob SqlEngine oder jOOQ, ich finde solche eine API Lösungen noch eleganter als so „built-in“ Artefakte wie C#’s LINQ, weil besser in den Programmablauf integrierbarer usw. Insofern freu ich mich fast, dass Java kein LINQ Pendant hat *gg*.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.