Persistence API’s - JDO vs. JPA

JDO kent een aantal voordelen ten opzichte van JPA, die op geen enkele manier zijn te bereiken met JPA. Deze features van JDO zijn echter niet nodig een webapplicatie; je kunt altijd zonder (zonder verlies van gemak/functionaliteit). Sterker nog: zonder het gebruik van deze exclusief-voor-JDO-features zijn de uiteindelijke kosten van software lager.


Overzicht



JDO (Java Data Objects) en JPA (Java Persistence Architecture) zijn twee API’s om objecten in een database op te slaan. Beide werken met een moderne JDK (1.5+), beide werken zowel met standaard-java en in een JavaEE-omgeving.


Het enige waarin ze verschillen is de manier waarop ze gegevens opslaan en de mogelijkheden die een applicatie heeft. De belangrijkste verschillen zijn het makkelijkst weer te geven als opties die JDO wel kent (en JPA niet), en vice versa.


JDO:



  •   Naast XML en annotaties kun je met JDO3 ook metadata definiëren via een API

  •   Databases: iedere, dus ook OODBMS, Excel, ... (JPA: alleen RDBMS)

  •   Velden die niet worden opgeslagen kunnen wel meedoen in transacties

  •   Klassen mogen ‘final’ zijn, in tegenstelling tot JPA.

  •   Ondersteund het opslaan van interfaces.

  •   Ondersteund meer soorten relaties, zoals bijvoorbeeld een Map waarbij de value een deel is van de key, of een Map waarin de key en value persisted, maar ongerelateerde entiteiten zijn.

  •   List-velden worden opgeslagen in de daadwerkelijke volgorde.

  •   Pessimistische transacties (JPA 2 ondersteund deze ook).

  •   JDO kent ook een (interne) object-identiteit; JPA en JDO ondersteunen beide een applicatie-identiteit, waarbij een of meer velden van een entiteit een record identificeren (i.e. een ‘primary key’ in RDBMS-termen).

  •   Meer mogelijkheden voor gegenereerde ID’s (ok, één: UUID).

  •   Meer ondersteunde datatypen, zoals java.lang.Number, java.util.Locale, boolean[], ... (maar geen java.util.Calendar, die JPA wel ondersteund).

  •   Inheritance wordt per class gedefinieerd, in plaats van per root van een inheritance tree.

  •   Fetch Groups


JPA:



  •   Datatypen: ondersteund velden van het type java.util.Calendar (JDO niet)

  •   Type-safe queries door middel van de Criteria API; JDO’s “type-safe” queries zijn dat niet, omdat een deel van de query in String-vorm is.

  •   Lazy/eager-controle via “join fetch” in queries

  •   Bulk update via JPQL (JDO en JPA kennen beide een bulk delete)

  •   List-velden worden zonder volgorde opgeslagen; je krijgt ze terug in de volgorde die je model aangeeft.


In het kort kun je daarom concluderen dat met een paar uitzonderingen (met name de Criteria API) JDO de meeste flexibiliteit biedt. Toch ga ik argumenteren dat JPA de betere keuze is.


Bronnen:




Hou het simpel!



Op het moment dat je de mogelijkheid hebt om een architectuur op te zetten, moet je er rekening mee houden dat het systeem jarenlang kan/zal bestaan. En al die tijd wordt het uitgebreid en aangepast door verschillende mensen. Het is dus belangrijk dat al deze mensen begrijpen wat het systeem kan, en vooral hoe ze hun wijziging aan moeten pakken. Dat “hoe” mag het systeem immers niet degraderen tot een legacy-systeem.


Kortom: het moet vooral simpel zijn.


Deze simpliciteit kun je op verschillende manieren bereiken:



Kiezen voor een algemeen aanwezig paradigma



Relationele databases komen zo vaak voor, dat het inmiddels impliciet het begrip van een datamodel beïnvloed. Verreweg de meeste programmeurs maken aannames die ervan uitgaan dat de data in een relationele database wordt opgeslagen. Het maakt daarbij niet uit of je een OO-database gebruikt, of Google’s Bigtable of welke andere database dan ook. Dat betekent ook, dat deze aannames lang niet altijd kloppen!


Zo gaan programmeurs vaak uit van de ACID-eigenschappen, terwijl deze voor bijvoorbeeld Google’s BigTable en andere ‘no-SQL’-databases niet opgaan. Ook staan maar weinig programmeurs er bij stil dat het filteren van de productiedatabase, om bijvoorbeeld een kleinere set testdata te maken, niet altijd even makkelijk is. Zeker als de gegevens niet voor systeembeheerders toegankelijk zijn (op een manier die zij begrijpen). De daarvoor benodigde tools zijn er echter meestal alleen voor relationele databases; OO-databases scoren op dat punt lang niet altijd goed.


Een andere aanname die programmeurs vaak maken is dat het aantal queries dat de ORM-laag genereert wel mee zal vallen. Maar bij bijvoorbeeld CenterParcs’ productbeheer is dit niet het geval: de JDO-implementatie DataNucleus genereert voor 1 scherm zoveel queries, dat je kunt lunchen om terug te komen naar een exceptie (transactie-timeout). Merk overigens op dat deze aanname vaak voldoende klopt om vaak geen problemen te veroorzaken (en soms daardoor juist wel).



Gebruik een eenvoudige structuur



Een systeem met de lagen Model-DAO-Service-View is eenvoudig te begrijpen, ook al wordt code vaak op de verkeerde plaats geschreven*). Het gevolg van een zo eenvoudig te begrijpen structuur is dat het makkelijker is voor programmeurs (ook de ‘mindere goden’) om aan zo’n eenvoudige structuur vast te houden.


Doordat de meest gangbare interpretaties van de lagen Model-DAO-Service-View vrij consistent zijn, blijft je architectuur intact.


De meest optimale architectuur is niet alleen voldoende effieciënt; hij is voornamelijk makkelijk te begrijpen, en blijft daardoor beter intact. Niets geeft zulke hoge onderhoudskosten als code die je niet kent, en een onregelmatige mix van verschillende architecturen is een goede manier om ervoor te zorgen dat het doorgronden van deze code zo duur mogelijk wordt.


Het is daarom verstandiger om een eenvoudige, flexibele archiectuur te kiezen, en vooral om daar aan vast te houden. In tegenstelling tot wat algemeen wordt aangenomen is dit dan ook de belangrijkste taak van een software-architect.


*) Bedrijfslogica gaat vaak naar de Service-laag, terwijl daar alleen de logica omtrend de use cases (scherm-interactie) thuis hoort. Bedrijfslogica hoort immers in het model. Een goede Service-laag bestaat dus vooral uit boilerplate-code; het is er eigenlijk alleen om transacties te definiëren.



Verminder het aantal mogelijkheden



Hoe meer mogelijkheden, hoe moeilijker het is om alles te begrijpen. En dat begint al als er voor 1 ding twee mogelijkheden zijn. Dus zodra je afwijkt van “1 concept, 1 mogelijkheid” moet het opvallen. Dit is een van de redenen dat annotaties populairder zijn dan XML voor ORM-tools: de nuances waar je bij een class/field rekening mee moet houden staat bij de definitie. Sommige uitzonderingen, zoals transient fields, moeten zichtbaar zijn. Andere uitzonderingen, zoals gebruik van een DAO-laag in je View, mogen nooit. De reden: iedereen moet van de code begrijpen wat ze waar kunnen vinden, ook al begrijpen ze niet waarom.



Waarom JPA beter is, en wanneer je toch JDO wil gebruiken



JDO kent een aantal voordelen ten opzichte van JPA, die op geen enkele manier zijn te bereiken met JPA. In aflopende mate van nut zijn dit:



  1.   Niet-relationele databases

  2.   Persisted interfaces

  3.   Metadata-definities via een API

  4.   “Transactional fields”

  5.   “Fetch Plans” (sommigen zetten deze hoger, hieronder leg ik uit waarom je ze niet wil gebruiken)


Als je een van deze features nodig hebt, wil je JDO gebruiken. Andere JDO-specifieke features, zoals het opslaan van een Map waarin de key en de value ongerelateerde entiteiten zijn, kun je prima emuleren, en zijn eigenlijk al een aanleiding om je domeinmodel onder de loep te nemen.


Mijn betoog is, dat bovenstaande features van JDO niet nodig zijn. Hieronder behandel ik ze afzonderlijk:



  Niet-relationele databases

  Als je objecten opslaat in een database, verwachten de meeste programmeurs de eigenschappen van een relationele database. Daarvan afwijken werkt alleen maar verwarrend. Als je dan toch een andere data-opslag nodig hebt, of dat nu Excel is of een content repository middels de JCR-standaard, kan dat beter duidelijk anders zijn dan de opslag van gegevens in een database. Zo niet, anders open je een grote doos gotcha’s (i.e. dingen werken wel zoals gedocumenteerd, maar niet zoals verwacht). Dezelfde persistence API gebruiken is dan juist geen duidelijk verschil, en de onverwachte verschillen in het gebruik zijn een bron van bugs, en dus van extra kosten.

  Persisted interfaces

  Interfaces definieren het gedrag van een klasse. Ze bevatten geen gegevens, en hebben dus geen identiteit. Als je een interface opslaat, betekent dat in feite dat de implementerende klassen iets gemeenschappelijk hebben, dat ook betekenis heeft in je database. Dat kunnen alleen maar gegevens zijn, en dus is het gebruik van een gewone persistente klasse op z’n plaats.

  Metadata-definities via een API

  Hier kan ik kort over zijn: je datamodel veranderd niet na het starten van het systeem. De meta-data in je code wijzigen is dus nooit nodig. De enige nuttige toepassing is om de Fetch Groups dynamisch te definiëren, omdat het beheer erg foutgevoelig is. Maar zoals bij “fetch plans” beargumenteerd wordt, is dit een oplossing voor een probleem dat je niet zou moeten hebben.

  “Transactional fields”

  Hoewel het handig is als de waarde van niet-opgeslagen velden wordt teruggezet wanneer de transactie wordt teruggedraait, is het lastig te volgen. Een veld is immers niet meer “transient” of opgeslagen (dit verschil is al lastig genoeg), maar “genegeerd”, “doet half mee in de transactie” of “opgeslagen”. Bovendien zijn er nauwelijks situaties waarin een object nog gebruikt wordt nadat de transactie heeft gefaald en is teruggedraaid. Doorgaans kun je uitstekend volstaan met extra een extra controle vooraf, danwel het object opnieuw te laden. De enige mogelijke uitzondering is een systeem met complexe, lang-lopende transacties, maar dan heb je al een probleem (complexiteit). In zowel de gangbare als exotische OLTP-systemen zijn de transacties te kort en te eenvoudig om dit nuttig te maken.

  “Fetch Plans”

  Zo mogelijk de enige feature van JDO die JPA niet heeft en die daadwerkelijk nuttig is. Een Fetch Plan is namelijk veel flexibeler dan de gebruikelijke, statische strategie om een veld eager/lazy op te halen.


  Maar wanneer heb je dat echt nodig?


  Meestal zijn aanpassingen aan de statische strategie alleen nodig voor queries, want een goede UI toont van een enkel object altijd dezelfde gegevens (soms met een paar kleine uitzonderingen). Voor alle lijsten (incl. Collectie-relaties) gebruik je een query, en dan is het duidelijker als je het ophalen van de gegevens (eager/lazy) in de query zelf definieert. Zo is wat er wordt opgehaald expliciet (i.e. zichtbaar), terwijl het bij een Fetch Plan impliciet (en dus onzichtbaar) is. Dat is ook de reden dat het instellen van het fetch plan doorgaans vlak voor het uitvoeren van de query gedaan wordt: zo maak je het weer zichtbaar, en dus onderhoudbaar.


  En zelfs als je toch Fetch Plans wilt gebruiken, zal dat doorgaans zijn op basis van wat je op je scherm nodig hebt. Dus heeft je model/DAO-laag óf een zoekmethode per use case (dan definieert de DAO-methode het fetch plan, en zijn ze dus overbodig), óf je hebt generieke queries en dus een mogelijkheid om extern te definiëren wat er wordt opgehaald. Maar daarmee heb je een circulaire dependency: je View- en Service-lagen zijn afhankelijk van de data (logisch), maar ook andersom: je Model is ervan afhankelijk dat de gebruikende laag kennis heeft van hoe de database wordt aangesproken (namelijk dat niet alle data opgehaald hoeft te worden), en dat is interne kennis!


  Maar als ook dat je niet afschrikt, zijn de problemen nog niet voorbij. Om de Fetch Plans beheersbaar te houden, krijg je vroeg of laat de wens om alle op het scherm gebruikte relaties letterlijk weer te geven en te vertalen naar een Fetch Plan. Bij Centerparcs is dit elegant ge-implementeerd door de gevraagde relaties (OGNL-expressies) te vertalen naar losse relaties op entiteiten (en dat zijn dan weer de Fetch Groups). Maar het gevolg is wel dat elke relatie een aparte Fetch Group moet worden (in String-vorm!), en dus is je model veel minder onderhoudbaar. Met alle (toekomstige) extra kosten van dien.




Andersom kent JPA ook een aantal voordelen ten opzichte van JDO:



  •   Type-safe query-parameters en -filters (JDO doet dit in String-vorm; zie ook het volgende punt).

  •   Een parameter-declaratie in queries die niet te verwarren is met een type-declaratie in Java (JDO doet dit zelfs in String-vorm, dus naast de bron van verwarring ben je ook nog eens je compiler-fouten kwijt).

  •   Queries die expliciet aangeven welke gegevens bij uitzondering eager gefetched worden.


Met name bij het ophalen van gegevens biedt JPA meer veilige mogelijkheden dan JDO; veilig in de zin dat de compiler helpt fouten te herkennen. Tevens biedt JPA een duidelijkere structuur om afwijkingen van het default fetch-gedrag zichtbaar te maken. Daar komt nog bij dat het tonen van (subsets van) gegevens zo belangrijk is, dat een persistence API die is geoptimaliseerd voor verzamelingen een duidelijk voordeel biedt. Door de gelijkvormigheid met SQL van JPQL is duidelijk dat JPA hiervoor beter geschikt is.


Tot slot: in de praktijk is een relationele database meestal voldoende. Daar waar dat niet zo is (denk aan Google, Bol.com, Amazon, etc.) zijn de eisen zo hoog dat een gespecialiseerde aanpak vereist is. En dat is dan inclusief de betere, en duurdere, programmeurs.


In alle gewone gevallen is JPA een voldoende efficiënte, en meest eenvoudige keuze. En daarmee dus de meest onderhoudbare en goedkope keuze.