Geonovum / KP-APIs

26 stars 40 forks source link

API principe 06 - How to go deal with relations #258

Closed LeoZandvliet closed 5 months ago

LeoZandvliet commented 5 years ago

Twee textuele opmerkingen:

Verder vind ik het té stellig om te zeggen dat bij 1 op N relaties de gerelateerde entiteit alleen als subentiteit opgevraagd kan worden. Twee scenario's:

Als ik nu paragraaf 4.2.4 volg, en ik heb een API op de webshop beschikbaar, zou ik in geen van beide scenario's binnen 'alle producten' kunnen zoeken en ben ik verplicht om per categorie een zoekopdracht te doen. De resource /producten is immers niet beschikbaar (of zelfs: mag niet aangeboden worden)?

fterpstra commented 4 years ago

De tekstverbeteringen zijn doorgevoerd. @jasperroes @dvh kunnen jullie op het inhoudelijke punt in gaan?

mevdschee commented 4 years ago

Verder vind ik het té stellig om te zeggen dat bij 1 op N relaties de gerelateerde entiteit alleen als subentiteit opgevraagd kan worden.

Eens, dit levert geen werkbare API op in de praktijk. Zoals Leo Zandvliet aangeeft: Je moet een lijst met categorieen kunnen opvragen, ook als deze categorieen leeg zijn. Die opmerking/eis moet wat mij betreft geschrapt worden.

mevdschee commented 4 years ago

Als je ("blog" example) een pad als "/posts/1/comments" (hasMany) toestaat:

Nog ingewikkelder wordt het als je denk aan "hasAndBelongToMany" met bijvoorbeeld twee koppeltabbellen tussen "posts" en "users" (bijvoorbeeld "post_authors" en "post_editors").

Mijn voorstel is om geen geneste paden toe te staan in de URI "/posts/1/comments", maar dit met een "embed" of "filter" parameter te doen, dus: "/posts/1?embed=comments" of "/comments?filter=post_id,eq,1"

mevdschee commented 4 years ago

Een andere belangrijke vraag: Als je (hasMany) subentiteiten mag opvragen in een request (post met comments), mag je ze dan ook schrijven? En als je ze mag schrijven, voegt hij dan de subentiteiten toe aan de bestaande lijst subentiteiten of overschrijft hij de lijst met subentiteiten?

Mijn voorstel is om geen subentiteiten toe te staan in de schrijfoperaties (alleen bij GET).

joostfarla commented 4 years ago

In de werkversie heb ik deze paragraaf herschreven, om de onderbouwing van deze design rule aan te scherpen en (hopelijk) te verduidelijken. Zie: https://geonovum.github.io/API-Designrules/#api-06

Voor dit issue geldt eigenlijk dezelfde onderbouwing als hier. Waar het primair om gaat is dat een API wordt ontworpen vanuit de use cases van de afnemer. In de verbeterde tekst heb ik de use case van @mevdschee beschreven.

Voor de use case van @LeoZandvliet gaat deze design rule echter niet op, aangezien er geen sprake is van een parent-child relatie. In dat geval kan namelijk worden volstaan met:

/products/123
/categories/1
/categories/2

De product resource bevat in dit geval referenties naar de bijbehorende categorieën, en deze zouden middels eager loading kunnen worden "uitgeklapt", zodat geen extra requests nodig zijn voor het ophalen van bijv de naam van de categorie. Het (ont)koppelen van categorieën aan een product kan zowel bij het aanmaken (als onderdeel van POST /products) als op een later moment (PATCH /products/123) geschieden.

Het is uiteraard ook een voor-de-hand-liggende use case om een overzicht van categorieën of producten te kunnen weergeven. Daarvoor zijn aparte (top-level) collection resources nodig:

/categories
/products

De discussie ontstaat vaak als men (om conventionele redenen?) onnodig extra resources gaat introduceren. Het heeft bijvoorbeeld totaal geen toegevoegde waarde om de volgende sub-collection resource te introduceren:

/products/123/categories

Ik kan persoonlijk geen enkele use case bedenken waarbij je alleen het lijstje met categorieën nodig hebt van een bepaald product. Dit veroorzaakt alleen maar complexiteit en onnodig veel requests.

joostfarla commented 4 years ago

mag je dan schrijven naar de "post_id" foreign key van "comments" met een PUT? Of moet dat veld worden "verborgen"? mag je dan een andere "post_id" dan 1 opgeven in een POST?

Als je een sub-collection resource /posts/1/comments hebt zou de post_id wat mij betreft geen onderdeel uitmaken van de representatie van deze resource (of sub-resource representaties). Het lijkt me vanuit functioneel perspectief geen zinvolle use case om een comment te kunnen verplaatsen naar een andere post, al zou het technisch gezien natuurlijk wel mogelijk zijn.

Nog ingewikkelder wordt het als je denk aan "hasAndBelongToMany" met bijvoorbeeld twee koppeltabbellen tussen "posts" en "users" (bijvoorbeeld "post_authors" en "post_editors").

Hier is geen sprake van een parent-child relatie; authors en editors zouden in dit geval geen aparte sub-collecties hoeven te zijn (net als bovenstaand voorbeeld van product <-> categorie) en kunnen gewoon onderdeel uitmaken van de /posts/1 resource.

Een andere belangrijke vraag: Als je (hasMany) subentiteiten mag opvragen in een request (post met comments), mag je ze dan ook schrijven? En als je ze mag schrijven, voegt hij dan de subentiteiten toe aan de bestaande lijst subentiteiten of overschrijft hij de lijst met subentiteiten?

mevdschee commented 4 years ago

Je antwoorden zijn nogal stellig en zijn eenvoudig te weerleggen. Het is niet mijn bedoeling om een inhoudelijke discussie over de voorbeelden te houden, maar wel om aan te geven dat je antwoorden niet toereikend zijn en dat dit onderwerp nog wat verdere uitwerking behoeft.

Ik kan persoonlijk geen enkele use case bedenken waarbij je alleen het lijstje met categorieën nodig hebt van een bepaald product

Ik wel: in het invoerveld van de categorie multi-select.

Het lijkt me vanuit functioneel perspectief geen zinvolle use case om een comment te kunnen verplaatsen naar een andere post

Uiteraard moet je een comment kunnen verplaatsen, bijvoorbeeld wanneer deze op het verkeerde artikel is geplaatst.

Hier is geen sprake van een parent-child relatie; authors en editors zouden in dit geval geen aparte sub-collecties hoeven te zijn

Wanneer is er wel/niet sprake van een parent-child relatie? Ik ken alleen hasMany en eventueel een multi-value field in de database (JDBC array of JSON) om hier onderscheid tussen te maken.

Een PUT naar /posts/1/comments (in feite een replace) lijkt me niet wenselijk en zou dus ook niet ondersteund moeten worden.

Eens, maar in het geval dat dit in een JSON veld in de posts tabel wordt opgeslagen heb je mijns inziens wel update operatie nodig (voor de sub-collectie), omdat de individuele entries niet te identificeren zijn.

Ik ben geen voorstander van deze multi-value constructie in databases, dus ik zou afraden deze te gebruiken. Misschien kunnen we dat opnemen in de standaard?

joostfarla commented 4 years ago

Ik heb geprobeerd de onderbouwing boven water te krijgen (dit is een bestaande design rule, voortgekomen uit eerdere discussies en bijeenkomsten met de werkgroep) en daarmee getracht dit in de begeleidende tekst + voorbeeld uit te drukken, maar ik ben het ermee eens dat dit nog steeds multi-interpretabel kan zijn. Het blijkt knap lastig te zijn om dit eenduidig op papier te zetten, omdat er best wat nuances te maken zijn.

Neem bijvoorbeeld een bijeenkomst met deelnemers. Dan zou ik verwachten:

/bijeenkomsten/123/deelnemers

En niet:

/deelnemers?bijeenkomst=123

Een deelnemer kan simpelweg niet bestaan zonder bijeenkomst. Dit lijkt op het voorbeeld zoals beschreven in de design rule (artikelen -> comments).

Maar neem aan de andere kant de BAG. Dan zou ik verwachten:

/openbare-ruimtes/123
/woonplaatsen/456

Je zou aan de ene kant kunnen stellen dat een openbare ruimte (straat) niet zou kunnen bestaan zonder een woonplaats, maar die is toch anders: straten zouden ook bestaan hebben als we niet zoiets zouden kennen als een woonplaats. Aanvullend daarop wil je openbare ruimtes ook buiten de context van een woonplaats kunnen bevragen, bijv op basis van een geo-locatie; daar is dus een duidelijke use case voor. Ze zijn wel gerelateerd, maar er is geen sprake van een parent-child relatie. Deze lijkt weer erg op het categorie -> product voorbeeld.

In dat geval is denk ik de vraag: zou dan beiden mogelijk moeten zijn? Dus:

/woonplaatsen/456/openbare-ruimtes
/openbare-ruimtes

Waarbij beide resources een collectie van openbare ruimtes representeren met referenties naar de individuele openbare-ruimte resources? Mogelijk is de 2e in dit voorbeeld read-only?

Technisch gezien is in beide bovenstaande gevallen sprake van een 1:N relatie, maar semantisch gezien verschillen ze van elkaar.

LeoZandvliet commented 4 years ago

Of een entiteit een eigen endpoint mag hebben vind ik puur afhangen van de usecase. Ja, het lijkt mij een 'best practice' om afhankelijkheden via het url pad op te lossen:

/bijeenkomst/123/deelnemers
/post/8/comments
/author/x/comments

Maar afhankelijk van de use cases kan besloten worden om ze wel via een eigen endpoint beschikbaar te stellen. De deelnemer JohnDoe kan niet bestaan zonder bijeenkomst, maar de mens wel. Een relatie met een bijeenkomst maakt het plots een deelnemer. Een voorbeeld van de soort usecases die naar mijn mening een eigen endpoint rechtvaardigen:

Veel entiteiten kunnen prima op zichzelf staan, maar het is vanuit de bron vaak 'onwenselijk' om ze als zodanig beschikbaar te stellen (privacy/verdienmodel/voorkomen verkeerde interpretatie/etc).

Wat betreft het opvragen/maken/bijwerken van relaties lijkt het mij het beste om te kijken naar andere 'standaarden' zoals bijvoorbeeld bij JSON API en de discussies die daar lopen in plaats van het wiel uit te vinden:

mevdschee commented 4 years ago

In het voorbeeld "/bijeenkomst/123/deelnemers" zijn er de entiteiten "bijeenkomst", "deelnemer" en "persoon". We hebben het hier dus over een hasAndBelongsToMany relatie. In het voorbeeld "/post/8/comments" betreft het een hasMany relatie "post" en "comment". De tweede is iets eenvoudiger te modelleren dan de eerste. Of iets zelfstandig kan bestaan lijkt me niet relevant voor het al dan niet krijgen van een endpoint (hangt immers af van de use-case die we niet kennen). Het al dan niet beschikbaar stellen om redenen van privacy/verdienmodel heeft niets met de vorm van de endpoints te maken, maar met het authorisatemodel (en de standaardisatie van authorisatieregels).

mevdschee commented 4 years ago

Wanneer we kijken naar bestaande implementaties dan zien we dat iets als "GraphQL" duidelijk maakt dat alle entiteiten moeten kunnen worden opgevraagd en kunnen worden gefilterd op relaties. De resultaten moeten al dan niet met hun belongsTo hasMany en hasAndBelongsToMany gerelateerde entiteiten worden terugggeven. Dit is uitgewerkt in het het voorstel met de "embed" of "join" parameter.

joostfarla commented 4 years ago

@LeoZandvliet mag ik daaruit concluderen dat je je kan vinden in de verbeterde formulering van API-06? Zie https://geonovum.github.io/API-Designrules/#api-06 Of zie jij daar nog mogelijke verbeteringen/verduidelijkingen?

Deze design rule gaat puur over de opbouw van URIs, dus nog even los van de manier waarop je ze kan aanmaken of bijwerken (heeft uiteraard wel raakvlakken).

De deelnemer JohnDoe kan niet bestaan zonder bijeenkomst, maar de mens wel. Een relatie met een bijeenkomst maakt het plots een deelnemer.

Dat ligt eraan: een deelnemer kan ook een resource op zichzelf zijn, met een datumAangemeld, dieetwensen etc., eventueel in relatie met een globale "persoon" entiteit.

LeoZandvliet commented 4 years ago

@joostfarla t.o.v. augustus 2019 is de tekst in ieder geval een stuk beter. Verdere op/aanmerkingen op de huidige tekst heb ik op het moment niet.

fsamwel commented 4 years ago

begrijp ik de ontwerp regel (inclusief toelichting) nu goed dat er staat:

  1. het heeft de voorkeur ("should be") de child resource te nesten (/articles/123/comments/789
  2. alleen collecties kan je ook dieper nesten (/articles/123/photos/456/comments), niet de resource (/articles/123/photos/456/comments/789)
  3. je mag ook enkele resources als zelfstandige resource definiëren (/comments/789), maar niet de collectie (/comments)

Is dit niet gewoon iets wat bij het ontwerpen van de API moet worden gekozen op basis van de te ondersteunen user stories?

Door de zin "only exist in the context of a parent resource" is het per definitie al zo dat de child resource alleen in de context van de parent resource bestaat (dus nested is in de url). Maar wat voegt deze ontwerpregel dan toe?

Anders zou het zijn wanneer er stond dat het bedoelde ding (dat in de werkelijkheid dat door de child resource wordt beschreven) ook alleen kan bestaan in de context van het andere ding (dat in de werkelijkheid dat door de parent resource wordt beschreven). Of wanneer de child resource is afgeleid van een relatie-entiteit in een semantisch- of datamodel.

Een resource kan alles zijn en kan je dus op elke gewenste manier modelleren, ongeacht hoe hetzelfde ding in de werkelijkheid ergens gemodelleerd, opgeslagen of begrepen wordt.

Bijvoorbeeld een huwelijk. Administratief is dat twee dingen (drie losse registraties): een huwelijksactie, en een bijschrijving van de partner op de persoonslijsten van beide partners. Dus in LO GBA is het huwelijk een categorie op een persoonslijst. Wanneer je het huwelijk semantisch gaat modelleren (zoals vroeger in RSGB is gedaan) is het huwelijk ineens een relatie-entiteit. Maar hoe het huwelijk in de API komt, mag volgens mij niet direct gekoppeld worden aan de registratie en ook niet aan het semantische model. Dit hangt af van de user story die ermee ondersteund kan worden. Bijvoorbeeld als ik alle huwelijken van het afgelopen jaar wil zoeken, of iedereen die deze maand zijn gouden bruiloft viert, is een resource /huwelijken handig. Wanneer gebruikers het huwelijk van een persoon zien als fundamentele eigenschap van de persoon, neem je die op in de resource /personen. Wanneer men het huwelijk ziet als gerelateerd iets, kan je het als nested resource opnemen (/personen/123/huwelijken). Enzovoort.

HenriKorver commented 4 years ago

Eens met @fsamwel dat deze ontwerpregel nog veel onduidelijkheid oproept. Het is meer een zachte best practice dan een echte design rule. Bovendien lijkt issue Geonovum/KP-APIs#173 (dat in mijn ogen onterecht gesloten is) met de huidige uitwerking niet verholpen. Mijn voorstel zou zijn om API-06 voor deze versie te schrappen en uit te stellen voor een volgende versie.

joostfarla commented 4 years ago

@fsamwel Dank voor je feedback. Ik begrijp de verwarring en dat is ook de worsteling om dit scherp geformuleerd te krijgen. Wat er staat (of in ieder geval zoals het bedoeld is):

Is dit niet gewoon iets wat bij het ontwerpen van de API moet worden gekozen op basis van de te ondersteunen user stories?

Er wordt juist in de tekst extra benadrukt dat het wel of niet aanbieden van een resource afhankelijk is van de relevante use cases. Het concrete voorbeeld: er hoeft dus bijv niet persee ook een /comments resource te bestaan.

Een resource kan alles zijn en kan je dus op elke gewenste manier modelleren, ongeacht hoe hetzelfde ding in de werkelijkheid ergens gemodelleerd, opgeslagen of begrepen wordt.

Dat klopt, maar met het design rules document proberen we best practices te stimuleren en consistentie over alle overheids-APIs heen te bewerkstelligen. Het design rules document zegt overigens bewust niks over de granulariteit van resources. Dus wanneer je 3 administratieve entiteiten in 1 resource wil stoppen is dat prima.

Denk je met deze toelichting nog steeds dat jouw huwelijk-voorbeeld niet past?

HenriKorver commented 4 years ago

Ter aanvulling op mijn vorige post. Issue Geonovum/KP-APIs#173 is dus nog niet verholpen in de uitwerking van API-06. De openingspost van dat issue luidt als volgt:

In de APIs voor zaakgericht werken waren met dit principe begonnen, maar zodra we client-applicaties gingen bouwen voor demo-doeleinden kwamen we er snel achter dat dit flink in de weg zit om bulk-data op te vragen.

Statussen horen bijvoorbeeld logisch gezien bij zaken en zou je volgens dit principe dus als een geneste resource ontsluiten. Echter, in een lijst-overzicht van een set aan zaken, betekent dit x extra calls om de statussen ook meteen op te halen, met x het aantal zaken in de lijst.

Dit kan veel efficiënter met een vlakke structuur, waarbij je 1 call krijgt om de lijst van zaken op te halen, en uit die lijst bouw je een filter parameter op om met 1 call een lijst van relevante statussen op te vragen. De hoeveelheid calls reduceer je dan van O(n+1) naar O(2).

Onze API's voor zaakgericht-werken waar veel gemeenten gebruik van maken voldoen hierdoor niet aan de API Strategie. Onterecht mijns inziens.

joostfarla commented 4 years ago

Besluit n.a.v. werkgroepsessie: @joostfarla en @HenriKorver gaan in overleg afstemmen waar extra ruimte in deze design rule vereist is. Tekst wordt waar nodig aangepast.

fterpstra commented 4 years ago

@joostfarla kan deze afgesloten worden?

mrtn78 commented 5 months ago

Gesloten tijdens werkgroep / TO ADR 25-04-2024 De voorgestelde verbetering is meegenomen in ADR 2