Fun met JPA en lambda’s

Tegenwoordig gebruiken we natuurlijk Hypermedia as the Engine of Application State (HATEOAS)[1] of GraphQL[2]  om onze REST api’s te definiëren en te implementeren. Daarvoor was er echter al wel een notie van goede of mooie REST interfaces, die de gebruiker van onze interface alle flexibiliteit biedt die we maar kunnen bedenken.

HTTP verbs (POST, GET, PUT etc), mime types, HTTP result code (200, 403, 500, etc), URI’s kunnen ingezet worden om een REST API te definiëren waarmee we bijvoorbeeld een financieel boekings systeem kunnen bouwen.

Een voorbeeld ‘https://mijn-geld/boekingen’, daarmee verwacht je alle boekingen die gedaan zijn te kunnen opvragen. Dit zijn voor gemiddelde Nederlander heel veel boekingen en levert dus een niet werkbare/perfomante API op. Gebruikers van onze API mogen verwachten dat er wat meer mogelijk is dan een alles of niets interface. Bijvoorbeeld “alle boekingen die gedaan zijn door tegenrekening  NL46INGB00129856. De url zou dan worden:

‘https://mijn-geld/boekingen/NL46INGB00129856’. De flexibiliteit in onze API is dat we het nu mogelijk maken om alleen die boekingen op te halen die gedaan zijn voor een specifiek rekeningnummer. Maar wat als we dit nu willen uitbreiden met een periode?

Wat als we een begin- en einddatum van de gewenste boekingen kunnen op geven. Dit zou resulteren in een url als ‘https://mijn-geld/boekingen/2019-01-01/2019-02-01’ of iets dergelijks. Wat betekent nu het eerste deel uit de URI, een datum, of een rekeningnummer, en wat als we nu nog meer mogelijkheden willen hebben?

 

Het mooie is dat het HTTP protocol voor het eerste probleem eigenlijk al voorzieningen voor heeft in de vorm van query string. Middels een querystring kunnen we key,value paren aan ons request mee geven.

De url voor het opvragen van alle boekingen in een bepaald tijdsvak zou dan worden:

https://mijn-geld/boekingen?vanaf_boekings_datum=2019-01-01&totenmet_boekings_datum=2019-02-01

En in het geval we alleen de boekingen willen opvragen met een bepaalde tegenrekening wordt dan:

https://mijn-geld/boekingen?tegenrekening=NL46INGB00129856

Een combinatie van tegenrekening en boekingsdatum is dan ook eenvoudig te realiseren, de url wordt dan:

https://mijn-geld/boekingen?tegenrekening=NL46INGB00129856&vanaf_boekings_datum=2019-01-01&totenmet_boekings_datum=2019-02-01

 

Implementatie

Hoe zouden we nu bovenstaande urls kunnen implementeren gebruik maken van standaard java technologieën?

We nemen als uitgangspunt dat alle boeking in een database zijn opgeslagen en middels SQL statements dus benaderbaar zijn. Het implementeren van de bovenstaande urls zou dan betekenen dat we die moeten omzetten naar de SQL statements:

 

Voor het selecteren van alle boekingen in een bepaalde periode gebruiken we de volgende SQL statement:

 

SELECT *

FROM BOEKINGEN

WHERE boekings_datum

BETWEEN ‘2019-01-01’ AND ‘2019-02-01’;

 

en voor het selecteren van alle boekingen met een bepaalde tegenrekening gebruiken we:

 

SELECT *

FROM BOEKINGEN

WHERE tegenrekening = ‘NL46INGB00129856’;

Wat nu als we deze twee willen combineren, en zelfs ordering, groeperen en of misschien zelfs willen bepalen welke column uit de SQL result we willen toestaan in onze API. Met andere woorden hoe zit het met de uitbreidbaarheid en onderhoudbaarheid van onze service?

Als we voor iedere variatie die we in de API ondersteunen een apart SQL statement maken zou dit resulteren in een plural en een niet onderhoudbaar aantal SQL statements. Zou het niet mooi zijn als we op basis van de query string in onze URL een SQL statement kunnen samenstellen? Waarbij we wel in code kunnen bepalen wat toegestane waardes zijn.

Wat we dus nodig hebben is een manier om gebruikers van onze REST API een query string te laten specificeren maar waarbij we in code nog steeds de controle hebben over hoe de query wordt opgebouwd.

JPA Criteria API to the rescue

Met de Criteria API [2] van JPA is het mogelijk om dynamische SQL statements op te bouwen. Gebruikmakend van door ons gedefinieerde JPA entiteiten en eigen java code en de Criteria API kunnen we ervoor zorgen dat alleen SQL statements ontstaan die we toestaan.

CriteriaBuilder

Om te beginnen hebben we een CriteriaBuilder nodig om een CriteriaQuery te maken, dit is een query voor een specifieke entiteit. Het is dus niet mogelijk om andere entiteiten te selecteren dan wat we hier in code definiëren (Listing 1).

 

final CriteriaBuilder cb = entityManager.getCriteriaBuilder();

final CriteriaQuery<Boeking> cq = cb.createQuery(Boeking.class);

final Root<Boeking> boekingRoot = cq.from(Boeking.class);
cq.select(boekingRoot);

Listing 1.

 

Dit is de basis waarop we in het vervolg van dit artikel verder op door borduren. De SQL statement die deze code oplevert ziet er als volgd uit.

 

SELECT *

FROM BOEKINGEN;

 

Aan een “Select All” functionaliteit hebben we niks we kunnen nog steeds iet datums of een tegenrekening specificeren. We hebben dus een “where” deel nodig. Dit doen we door de  CriteriaQuery uitgebreid met een where clausule.

 

Om te beginnen definiëren we een Predicate gebruik makend van een van de factory methode van de CriteriaBuilder  om die vervolgens als where clause aan de CriteriaQuery te geven (Listing 2).

 

Predicate pr = cb.between(boekingRoot.get(Boeking_.boekingDatum),

vanafBoekeingsDatum,

totEnMetBoekingsDatum);

cq.where(pr);

Listing 2.

 

JPA model classes.

Als je mee gebouwd hebt zul je tot de conclusie komen dat er een compileerfout is ontstaan. Of je denkt dat er een typefout in de code staat.  Waar komt nu die Boeking_ class vandaan?

De Criteria API definieert de parameter in de get methode als een SingularAttribute<? super X, Y>. Dit is een onderdeel van de door JPA gegenereerde model classe.  Om in je eigen code ook gebruik te kunnen maken van deze class, moet deze gegenereerd worden. Gelukkig kunnen we hiervoor de maven-processor-plugin[4] gebruiken  samen met de processor org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor. [5] Hoe deze precies te configureren is valt buiten de scope van dit artikel.

 

Uitbreiden met meerdere parameters

Wat we nu hebben is niets anders dan een complexe manier om een query te schrijven, de kracht hiervan blijkt pas als er een tweede parameter in de API gebruikt kan worden (Listing 3).

 

Predicate pr = null;

 

switch (veldNaam) {

case “boekingsDatum”:

pr = cb.between(boekingRoot.get(Boeking_.boekingDatum),

vanafDatum(veldWaarde),

totEnMetDatum(veldWaarde));

break;

case “tegenRekening”:

pr = cb.equal(boekingRoot.get(Boeking_.tegenrekening),

veldWaarde);

break;

}

cq.where(pr);

Listing 3.

 

Het resultaat is dat we nog steeds twee verschillende mogelijkheden hebben terwijl we eigenlijk de combinatie van de twee ook willen kunnen gebruiken. Gelukkig is het mogelijk om Predicates te combineren middels een boolean method.

 

Wat ook in de huidige opzet nog moet veranderen is de mogelijkheid om meerdere veldNaam:veldWaarde en aan te kunnen. We moeten dus een loop maken over alle veldNaam:veldWaarde combinaties en vervolgens alle predicaten combineren.

 

Collection API is the new loop

Collection API en Lambda’s is het nieuwe loopje. Dus definiëren we eerst alle veld naam:waarde als een een lijst van naam:waarde tuples. Vervolgens mappen we deze combinatie naar een List<Predicate> die we uiteindelijk reduceren naar een enkele Predicate, middels een AND operatie (Listing 4).

 

apiVelden.stream()

.map(apiVeld -> {

switch (apiVeld.naam) {

case “boekingsDatum”:

return cb.between(boekingRoot.get(Boeking_.boekingDatum),

vanafDatum(apiVeld.waarde),

totEnMetDatum(apiVeld.waarde));

case “tegenRekening”:

return cb.equal(boekingRoot.get(Boeking_.tegenrekening),

apiVeld.waarde);

}

return null;

})

.reduce((a, b) -> cb.and(a, b))

.ifPresent(pr -> cq.where(pr));

Listing 4

 

De SQL statement die dit produceert zal iets zijn als:

 

SELECT *

FROM BOEKINGEN

WHERE tegenrekening = ‘NL46INGB00129856’

AND boekins_datum BETWEEN ‘2019-01-01’ AND ‘2019-02-01’;

 

Map is the new switch

Ok, dit is misschien niet helemaal waar maar de switch statement is niets meer dan een lookup tabel met een functie als waarde. En aangezien we met lambda’s in java ook de mogelijkheid hebben om een functie als een object te beschouwen kunnen we deze switch dan niet herschrijven? Om te beginnen maken we een map van veldnaam naar een functie die een Predicate oplevert voor het betreffend veld. De code wordt dan (Listing 5):

 

public interface PredicateSupplier {

Predicate supply(CriteriaBuilder cb, Root<Boeking> root, String value);

}

 

Map<String, PredicateSupplier> map = new TreeMap<>();

 

map.put(“boekingsDatum”, new PredicateSupplier() {

public Predicate supply(CriteriaBuilder cb, Root<Boeking> r, String v) {

return cb.between(r.get(Boeking_.boekingDatum),

vanafDatum(v),

totEnMetDatum(v));

}

});

Listing 5.

 

Maken we nu PredicateSupplier ook nog eens een @FunctionalInterface kunnen we het als een lambda schrijven waardoor de code vereenvoudigt kan worden tot:

 

map.put(“tegenRekening”,

(cb, r, v) -> cb.equal(r.get(Boeking_.tegenrekening), v));

 

Gebruiken we nu deze map in onze code om een Predicate te verkrijgen wordt de code als volgd (Listing 6:

 

apiVelden.stream()

.map(apiVeld -> map.get(apiVeld.naam)

.supply(cb, boekingRoot,

apiVeld.waarde))

.reduce((a, b) -> cb.and(a, b))

.ifPresent(pr -> cq.where(pr));

Listing 6.

 

Enum is the new Map

We hebben nu wel een map te onderhouden waarbij de key een string waarde is, Kunnen we dit niet nog verder dichttimmeren? Wat hebben we nog meer tot onze beschikking dat een vaste lijst van waardes is, waarbij we ook nog specifike parameter kunnen meegeven.

 

Een enum, gecombineerd met onze @FunctionalInterface ziet er dan als volgd uit (Listing 7):

 

public enum PredicateField {

TEGENREKENING((cb, r, v)

-> cb.equal(r.get(Boeking_.tegenrekening), v)),

BOEKINGSDATUM((cb, r, v) -> cb.between(r.get(Boeking_.boekingDatum),

vanafDatum(v),

totEnMetDatum(v)));

 

private final PredicateSupplier ps;

 

private PredicateField(PredicateSupplier ps) {

this.ps = ps;

}

 

public Predicate predicate(CriteriaBuilder cb,

Root<Boeking> root,

String value) {

return ps.supply(cb, root, value);

}

}

Listing 7.

 

Gebruikmakend van de enum kan de code herschreven worden in (Listing 8):

 

apiVelden.stream()

.map(apiVeld ->

PredicateField.valueOf(apiVeld.naam.toUpperCase())

.predicate(cb, boekingRoot, apiVeld.waarde))

.reduce((a, b) -> cb.and(a, b))

.ifPresent(pr -> cq.where(pr));

Listing 8.

 

Het opbouwen van een where clausule is nu een ‘oneliner’ geworden, de uitbreidbaarheid zit hem nu in het verder definiëren van de PredicateField enum.

 

TL;DR;

 

Gebruikmakend van de Criteria API om dynamisch SQL statements te creëren waarbij we enum gebruiken om vast te leggen welke opties we toestaan, en vervolgens een lambda om ook daadwerkelijk de betreffende optie te genereren. Dat dan weer gecombineerd met de Collection streams maakt het mogelijk om een flexibele REST API te definiëren waarbij we wel vast leggen wat er geselecteerd kan worden maar niet hoe. Alle code uit dit artikel is te vinden op: https://gitlab.com/pnmtjonahen/restjpalambda.git

References

[1] https://en.wikipedia.org/wiki/HATEOAS

[2] https://graphql.org/

[3] https://docs.oracle.com/javaee/6/tutorial/doc/gjrij.html

[4] https://bsorrentino.github.io/maven-annotation-plugin/usage.html

[5] https://docs.jboss.org/hibernate/jpamodelgen/1.0/reference/en-US/html_single/

 

BIO

Ordina J-Tech Codesmit, met een focus op interne en externe kennisoverdracht waarbij geput kan worden uit 20+ jaar programmeer ervaring in verschillende talen en omgevingen C, C++, Java, JavaEE, Spring, Maven, Git, Docker, javascript, typescript, Rust hebben een plaats hebben gevonden in het arsenaal van gegeven gastcolleges en workshops.

Dit artikel is eerder geplaatst in Java Magazine editie 1 jaargang 2020