Dit is deel twee van de serie over het bouwen van een low-code platform. In dit deel wordt uitgelegd hoe data opgehaald en geschreven wordt.

Dit is wellicht het meest cruciale aspect van het platform, omdat datastructuren en instanties daarvan allemaal gedefinieerd en gemaakt worden met deze code.

Requirements voor Data opslag

De belangrijkste functionaliteiten in de data-laag zijn:

  • CR(U*)D van elk type (de datastructuur)
  • CRUD instanties van elk type (data met een specifieke structuur)

De requirement ‘eenmalig en in run-time definiëren van de datastructuur’ dicteert dat de datastructuur alleen opgeslagen wordt in de database, omdat dit de plaats is waar we onze data opslaan. Dus ook de datastructuur.

Een andere requirement is dat we de taal en de structuur van de database zoveel mogelijk willen benutten. De reden hiervoor is eenvoudig: Databases zijn geoptimaliseerd voor dataopslag en het ophalen daarvan. Ze bieden daarnaast ook aanvullende functionaliteit om met data te werken en deze te manipuleren.

Deze requirement lijkt misschien voor de hand liggend. Ik heb echter met bestaande platformen gewerkt die hun eigen datastructuur aanwendden, bovenop de database structuur. Dit resulteerde in een suboptimale situatie: De functionaliteiten (indexen bijvoorbeeld) van de database konden niet optimaal gebruikt worden, waardoor de lees/schrijf snelheid van de database niet gehaald kon worden. Dit leidde tot performance issues.

*de U (Update) staat hier tussen haakjes. Omdat het veranderen van een type grote gevolgen kan hebben voor bestaande instanties van dat type, heb ik dit niet geïmplementeerd in de PoC.

Meta-meta model

Het platform gebruikt Neo4j. De datastructuren van het platform moeten dus opgeslagen worden in het native format van Neo4j om optimaal van die structuur gebruik te kunnen maken. Daaruit is het volgende datamodel ontstaan:

De meta-meta datastructuur

De lezers die Neo4j kennen, roepen nu dat dit het datamodel van Neo4j is… Het antwoord hierop: Ja, in aangepaste vorm. En met voorbedachte rade. Door dit zo te doen, kunnen we het model in Neo4j expressies uitdrukken én de instanties opslaan in een native Neo4j structuur zodat we de kracht van Neo4j ten volle kunnen benutten.

Belangrijk om te weten: De structuur van de opgeslagen data is wel op te vragen in Neo4j. Er is echter nergens een plek om de structuur op te slaan van structuren die geschreven gaan worden. Dat is niet goed of slecht, maar het is voor dit platform wel nodig om de datastructuur af te dwingen, dus we gaan het wel opslaan en we gebruiken daar ook de database zelf voor.

Domain en MetaLookupAttr zijn nieuw ten opzichte van het standaard Neo4j model:

Domain — De labels worden gebruik om het domain van een metaType te duiden. Zo ontstaat een collective van verbonden metaTypen in één domein. Dit zorgt er ook voor dat twee types met dezelfde naam kunnen bestaan in twee verschillende domeinen.

MetaLookupAttr — Een gewoon attribuut wordt gevuld met de waarde die een gebruiker invult, maar er zijn ook waarden die je in wilt laten vullen met referentiedata. Bijvoorbeeld een MetaType genaamd Vorm met de instanties Vierkant, Driehoek en Cirkel. Dit dwingt de gebruiker om één van deze waarden te kiezen én om de waarde in het attribuut-veld op te slaan (in plaats van het maken van een MetaRelation)

Er zijn natuurlijk andere typen die toegevoegd kunnen worden. Zoals een QueryableAttr, die gebruikt kan worden om de som van een aantal OrderItems onder een Order op te tellen. Voor deze PoC heb ik er echter voor gekozen om hier één voorbeeld (MetaLookupAttr) van te implementeren.

Het lezen en schrijven van data(structuren)

Het is noodzakelijk om de data en  hun onderliggende structuren te kunnen lezen/schrijven voor het gebruik met Functionele modules (zie deel 3) en voor in-/uitvoer op schermen.

Omdat we de native Neo4j taal gebruiken is het lezen van deze data(structuren) zeer eenvoudig. En doordat het een veelgebruikte functionaliteit is, komt deze in een service die alle generieke lees/schrijf-functies bevat:

Neo4j Service (Data Service module) — Deze service bevat alle functies om datastructuren en instanties daarvan de lezen en schrijven. Het werkt met bovenstaand meta-meta model.

Dit is de functie die alle attributen voor een metaType ophaalt:

public function get_typeattr($neocl,$metaType,$domain)
{
$query = ‘MATCH (n:’.$domain.’)-[:HASATTR]->(a:MetaAttr) WHERE n.name = {metaType} RETURN a.name AS name,a.attrtype AS type,a.defval as defval, a.description as desc ORDER BY a.name’;
return $neocl->run($q, [‘metaType’ => $metaType]);
}

En dit is een functie om een instantie te verwijderen:

public function delete_instance($neocl, $metaType, $instanceID)
{
$query = ‘Match (n:’.$metaType.’) WHERE n.in_id = {instanceID} DETACH DELETE n’;
$result = $neocl->run($query, [‘instanceID’ => $instanceID]);
}

Noot 1: $neocl is de neo client connectie die ik gebruik om queries uit te voeren en het resultaat daarvan op te halen. Deze kan in Functionele modules aangeroepen worden.

Noot 2: Enthousiaste Neo4j Cypher gebruikers zien dat ik string concatenatie gebruik voor het maken van de query. Helaas kunnen labels niet als parameter doorgegeven worden. Dit heb ik hard nodig, dus vandaar deze omweg.

Er zijn een aantal van dit soort functies die de data lezen/schrijven. De grotere zijn bedoeld om data van instanties aan te maken en te wijzigen. Ze vertalen feitelijk de data in de database naar voor het platform verwerkbare data en vice versa. Deze functies kunnen het beste in de code van de PoC zelf bekeken worden, omdat er mappings en transformaties gebruikt worden die voor dit verhaal niet interessant zijn.

Basic CRUD voor datastructuren/instanties

Zodra bovenstaande interface is gemaakt, kan deze gebruikt worden in Functionele modules. De eersten zijn toegewijd aan het ontsluiten van deze functionaliteit aan de eindgebruiker:

Edit Controller (Functional module) — Bevat schermen om datastructuren en instanties te lezen/schrijven. Deze controller bevat een generieke twig-form waarin de velden van een datastructuur gerenderd worden. Dit laatste wordt gedaan door de volgende stappen te volgen:

  1. Haal een datastructuur op (met de Neo4j Service)
  2. Itereer door de structuur → foreach($attributes as $attribute){…
  3. Genereer de elementen in het formulier → $form->add(…)
  4. Presenteer het formulier → $form->handleRequest($request)
  5. Verwerk de resultaten (met de Neo4j Service)

Dit maakt het mogelijk om de datastructuren alleen in de database te laten bestaan. De code is generiek, zodat het elke datastructuur aan kan die voldoet aan het meta-meta model.

Het volgende formulier wordt gebruikt om een metaType te maken:

Scherm om een nieuw type toe te voegen

Het stelt de gebruiker in staat om een 0..n attributen, lookup attributen en relaties te maken. In de PoC zijn de name en description attributen verplicht omdat ik dat zelf fijn vond tijdens het fast-prototypen en voor het maken van generieke schermen.

Als het metaType is gemaakt, kan een formulier gegenereerd worden die de instanties van zo’n metaType. In het volgende voorbeeld wordt een instantie van het type Frame geëdit:

Het wijzigen van een instantie van metaType Frame

Voor het maken van dit formulier zijn zowel de type-data als de instantie-data nodig: Het formulier wordt gemaakt met de type-data en gevuld met de instantie-data.

Volgende stap: Data bekijken

In deel 3 wordt het meta model voor Screens uitgelegd.

Neem een kijkje in de keuken: De PoC staat op Github, zodat je de werking van het platform kan bestuderen. Hij ìs hier te vinden: GitHub — InfoNotion(Als je bij wilt dragen of samen iets wilt bouwen, stuur een berichtje!)

Terug naar overzicht

Meer lezen van Stefan? 

Vond je dit een interessant artikel? Lees dan meer van Stefan via onderstaande knop:

Bekijk alle artikelen van Stefan

Stefan Dreverman, Trending Technologies Zuid