Skip to content

Ontsluiting middels GraphQL

GraphQL is een open-source framework oorspronkelijk ontwerpen door Facebook en tegenwoordig onderhouden door de GraphQL foundation. GraphQL is - vanuit hun eigen woorden - een querytaal voor APIs. Het combineert de gedachte van graafmodellering van je data en laagdrempelige ontsluiting via een developer-friendly API interface.

GraphQL Logo

Voordelen en doel van GraphQL

Met GraphQL ondervangen we een aantal vaak gestelde tekortkomingen van de services die we op onze basisregistraties leveren.

  • Levering van data is vraaggestuurd in plaats van aanbodgestuurd. We geven de gebruiker de kans om precies die data te bevragen die hij/zij nodig heeft.
  • We maken bevragen op basis van objecten mogelijk.
  • Verschillende datasets met een administratieve connectie kunnen integraal bevraagd worden.

Voor ons heeft GraphQL echter twee belangrijke doelen:

  1. GraphQL is een krachtig paradigme om data integraal en laagdrempelig richting onze afnemers beschikbaar te stelen én
  2. GraphQL dient als abstractielaag op de bron, om hiermee onze Extract- Transform & Load (ETL) van Linked Data uit te voeren.

In dit document bespreken we nadrukkelijk dit eerste. Voor het tweede punt verwijzen we de geïnteresseerde lezer door naar de sectie over GraphQL in gebruik.

GraphQL op een silo

Verschillende GraphQL endpoints - naar de buitenwereld toe geidentificeerd door hun typedefs en resolvers - bevinden zich op een databron, zijnde (op dit moment) Linked Data, SQL en/of REST. Een GraphQL endpoint is dus back-end agnostisch.

Typedefs

Zoals eerder gesteld modelleren we middels GraphQL onze data als een graaf, met verschillende nodes (objecten) en edges (relaties). Deze modellering vinden we terug in de typedefs van ons GraphQL endpoint. Neem als voorbeeld het object Pand in de BAG. De corresponderende typedefs ziet er als volgt uit:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
"""Een pand is de kleinste bij de totstandkoming functioneel en bouwkundig-constructief zelfstandige eenheid die direct en duurzaam met de aarde is verbonden en betreedbaar en afsluitbaar is"""
type BAG2Pand implements JSONLD @key(fields: "lokaalid peilDatum")
{
    """De identificatiecodes voor objecten zijn uniek binnen de context van deze naamgevingsruimte.
    Correspondeerd met de [NEN3610 standaard](https://geonovum.github.io/NEN3610-Linkeddata/#nen3610id)"""
    namespace: String!
    """Een aanduiding waarmee kan worden aangegeven dat een pand in de registratie is opgenomen als gevolg van een feitelijke constatering, 
    zonder dat er op het moment van opname sprake was van een regulier brondocument voor deze opname.
    Zie ook [de BAG catalogus](https://imbag.github.io/catalogus/hoofdstukken/attributen--relaties#745-geconstateerd)
    """
    geconstateerd: Boolean
    """BAG registraties bevatten een voorkomen identificatie, waarmee de volgorde wordt aangegeven waarmee registraties voor hetzelfde object zijn aangemaakt."""
    voorkomenidentificatie: Int
    """Wordt gebruikt om de formele historie voor de bronhouder mee aan de duiden. Deze attribuut beschrijft wanneer het voorkomen is ontstaan bij de bronhouder. 
    Zie ook [deze praktijkhandleiding](https://imbag.github.io/praktijkhandleiding/artikelen/hoe-bepaal-ik-welke-gegevens-in-een-levenscyclus-van-een-object-geldig-zijn)"""
    tijdstipregistratie: DateTime
    """Wordt gebruikt om de formele historie voor de bronhouder mee aan de duiden. Deze attribuut beschrijft wanneer het voorkomen is afgesloten bij de bronhouder. 
    Zie ook [deze praktijkhandleiding](https://imbag.github.io/praktijkhandleiding/artikelen/hoe-bepaal-ik-welke-gegevens-in-een-levenscyclus-van-een-object-geldig-zijn)"""
    eindregistratie: DateTime
    """Wordt gebruikt om de materiële historie voor de bronhouder mee aan de duiden .De materiële historie beschrijft vanaf welke datum een voorkomen geldig is in de registratie, via het attribuut begingeldigheid.
    Deze datum kan in de toekomst liggen.
    Zie ook [deze praktijkhandleiding](https://imbag.github.io/praktijkhandleiding/artikelen/hoe-bepaal-ik-welke-gegevens-in-een-levenscyclus-van-een-object-geldig-zijn)"""
    begingeldigheid: Date
    """Wordt gebruikt om de materiële historie voor de bronhouder mee aan de duiden .De materiële historie beschrijft tot welke datum een voorkomen geldig is in de registratie, via het attribuut eindgeldigheid.
    Deze datum kan in de toekomst liggen.
    Zie ook [deze praktijkhandleiding](https://imbag.github.io/praktijkhandleiding/artikelen/hoe-bepaal-ik-welke-gegevens-in-een-levenscyclus-van-een-object-geldig-zijn)"""
    eindgeldigheid: Date
    """De unieke aanduiding van het brondocument op basis waarvan een opname, mutatie of een verwijdering van gegevens ten aanzien van een pand heeft plaatsgevonden binnen een gemeente."""
    documentnummer: String
    """De datum waarop het brondocument is vastgesteld op basis waarvan een opname, mutatie of een verwijdering van gegevens ten aanzien van een pand heeft plaatsgevonden."""
    documentdatum: Date
    """De minimaal tweedimensionale geometrische representatie van het bovenzicht van de omtrekken van een pand.
    Gemeten in het stelsel van de Rijksdriehoeksmeting. De [ESPG](https://epsg.io/28992) code van dit stelsel is 28992."""
    geometrie: String  @jsonrdfprefix(prefix: "<http://www.opengis.net/def/crs/EPSG/0/28992> ")
    """De aanduiding van het jaar waarin een pand oorspronkelijk als bouwkundig gereed is of zal worden opgeleverd."""
    oorspronkelijkbouwjaar: Int!
    """De fase van de levenscyclus van een pand, waarin het betreffende pand zich bevindt.
    Domeinverzameling:
    - Bouw gestart
    - Bouwvergunning verleend
    - Niet gerealiseerd pand
    - Pand buiten gebruik
    - Pand gesloopt
    - Pand in gebruik
    - Pand in gebruik (niet ingemeten)
    - Sloopvergunning verleend
    """
    pandstatus: String! @jsonrdftype
    """Een verwijzing naar de Verblijfsobjecten en diens voorkomens (historie) welke deel uit maken van dit pand. Neemt alleen het actuele voorkomen wanneer peilDatum niet leeg is."""
    bevatverblijfsobjecten: [BAG2Verblijfsobject]
}

Kortom, het object (BAG2Pand) kent attributen (bijv. oorspronkelijkBouwjaar), verscheidene vormen van metadatering (zichtbaar in de typedefs) en object-relaties (zoals bevatVerblijfsobjecten) waarmee relaties naar andere objecten worden aangegeven.

Query

Onderdeel van het implementeren van de typedefs is ook het definiëren van de Query varianten die voor deze silo geldt. Een Query definiëert de ingang waarmee de data bevraagd kan worden. Denk bijvoorbeeld aan een (set van) nummeraanduiding(en):

bag2nummeraanduiding(identificatiecode: String  @lpad(length: 16, char: "0"), peilDatum: Date, first: Int, offset: Int): [BAG2Nummeraanduiding]

Deze Query (ingang) beschrijft dus dat er een nummeraanduiding opgehaald kan worden voor een gegeven identificatiecode (vergelijkbaar met een standaard REST API) of dat er een set aan objecten opgehaald kan worden (met paginatie parameters first en offset). Middels de zogenoemde directive lpad wordt input van een gebruiker voor identificatiecode automatisch aangevuld tot 16 karakters.

Resolvers

Wanneer de typedefs gedefiniëerd zijn moet het GraphQL endpoint nog weten hoe het de onderliggende data moet ophalen. Dit is terug te vinden in de resolvers. Een voorbeeldje:

BAG2Nummeraanduiding: {
    hoofdadresVan(parent, args, ctx, info){
      return querydb({ pool: 'bag_pg', ctx: bagsql["bag2verblijfsobject"], id: "hoofdadres = :identificatie", binds: { peildatum: parent.peilDatum, identificatie: parent.identificatiecode }});
    },
}

Kortom, de resolvers beschrijven hier dat er een voorgedefiniëerde SQL query (bag2verlijfsobject) moet worden afgevuurd met daarin een aantal bind variabelen (parameters) om van een object met type BAG2Nummeraanduiding het object op te halen dat het hoofdadresVan deze nummeraanduiding is. Middels de pool wordt aangegeven uit welke database de data moet komen.

GraphQL over de silo's heen

Al onze GraphQL endpoints voldoen aan de Apollo GraphQL standaard. Apollo is een open-source framework bovenop GraphQL waarmee voornamelijk federatie gemakkelijker is gemaakt. Wanneer verschillende GraphQL endpoints naar elkaar verwijzen (bijvoorbeeld doordat de onderliggende data relaties naar een andere databron bevat) gebruiken we Apollo om de brug tussen deze silo's te slaan.

Apollo GraphQL

Dit betekent dat - governance technisch - data eigenaren verantwoordelijk zijn voor de data in hun eigen silo (en desbetreffende endpoint). Maar ook dat er middels een zogenoemde Apollo Gateway een bevraging kan worden uitgevoerd over deze verschillende silo's heen. Hierbij is het irrelevant waar deze endpoints staan. Voor deze endpoints geldt dus een volledige federated approach. De kracht hiervan is dat Kadaster niet per se een endpoint hoeft te hosten op alle data, maar dat ook externe partijen (zoals bijvoorbeeld CBS of NDW) een eigen endpoint zouden kunnen hosten, welke we dan wel weer integraal kunnen bevragen. Uiteraard wel zolang zij maar aan dezelfde standaarden voldoen. Om de silo's met elkaar te verbinden moeten er in de silo's zogenoemde extends worden aangebracht. Een voorbeeldje:

extend type BGTPand @key (fields: "identificatiebagpnd peilDatum"){
    identificatiebagpnd: String @external
    peilDatum: Date @external
    """In de BGT is voor het Pand object een relatie tot het afgeleide BAG pand opgenomen. Een BGT pand kan niet bestaan zonder onderliggend BAG pand. 
    Dit is een verwijzing naar de bijbehorende voorkomens (historie) van het BAG pand. Alleen het actuele voorkomen wordt meegenomen wanneer peilDatum niet leeg is."""
    bagpand: [BAG2Pand] 
}

Hier wordt dus gesteld dat een BGTPand een relatie heeft met een BAGPand. En dat klopt. Een BGT Pand kan zelfs niet bestaan zonder BAG Pand. De identifier naar het BAG pand is dan ook een attribuut van het BGT Pand Object. Door in een extend aan te geven dat deze administratieve relatie bestaat, en een bijbehorende resolver te implementeren, kunnen beide objecten nu in samenhang worden bevraagd.

GraphQL over Linked Data

Voor onze toepassing geldt dat wij vaak GraphQL toepassen om traditionele data naar Linked Data te transformeren. Er zijn echter ook een scala aan toepassingen te bedenken waarom GraphQL ook een hele waardevolle abstractielaag kan bieden bovenop Linked Data. GraphQL is een breder geadopteerd query mechanisme dan SPARQL (de querytaal van Linked Data, red.) en biedt ook meer mogelijkheden om gemakkelijk door het schema heen te lopen.

Het is dan ook mogelijk om een Linked Data endpoint met een (Apollo) GraphQL endpoint te abstraheren. Voor Linked Data endpoints geldt dat hier een communica implementatie met bijbehorende JSON-LD context (Zie bijvoorbeeld de JSON-LD context van BAG 2.0) bij hoort.

Conclusie

  • Een GraphQL endpoint is back-end agnostisch.
  • Een GraphQL endpoint wordt gedefiniëerd door zijn typedefs en resolvers.
  • GraphQL endpoints kunnen middels het Apollo framework geheel federatief worden opgezet en beheerd.
  • Middels een Apollo GraphQL Gateway zijn de federatieve endpoints echter wel op één plek toegankelijk.
  • GraphQL kan ook middels Communica dienen als abstractielaag voor een gegeven Linked Data bron.

Een technische architectuur van dit geheel is te vinden in de volgende plaat:

Technische Architectuur GraphQL

Terwijl we functioneel de volgende overzichtsplaat hanteren:

Functionele Architectuur GraphQL