Cursus
Dus je wilt je eigen contentmanagementsysteem (CMS) bouwen, ook wel een blog genoemd? Dit is een klassiek voorbeeld wanneer je leert werken met een database, of het nu een relationeel databasesysteem (RDBMS) is of een NoSQL-database, omdat je zowel een mogelijk grote hoeveelheid data als de relaties daarin verkent. Het voorbeeld van een blog-app vertaalt ook goed naar andere behoeften voor datamodellering.
In dit artikel bekijken we enkele do's en don'ts bij het ontwerpen van je NoSQL-documenten in MongoDB. We gaan echter niet daadwerkelijk een blog-app bouwen; we bekijken alles alleen vanuit een dataperspectief.
De componenten van een blog-app
Voordat we een poging doen om een documentmodel voor een blog te ontwerpen, is het een goed idee om even afstand te nemen en na te denken over elke component die data kan bevatten.
Een typische blog kan de volgende kenmerken hebben:
- Veel gebruikers of auteurs
- Veel blogposts per auteur
- Veel reacties per blogpost
Natuurlijk kan die lijst met features langer en complexer worden, afhankelijk van de zakelijke behoeften van je blog. Om het eenvoudig te houden, modelleren we onze data op basis van het bovenstaande.
Wat je kán doen, maar beter niet doet
Je eerste gedachte bij het bovenstaande kan zijn om elk opsommingselement als een eigen document te behandelen. Dit is de typische aanpak die je in een relationele database zoals Postgres zou gebruiken. In MongoDB kan het er ongeveer zo uitzien:
{
"_id": "author-1",
"name": "Nic Raboy",
"description": "I'm some dude named Nic."
}
Het bovenstaande is een sterk uitgeklede weergave van een auteur. Natuurlijk kun je veel andere velden hebben, zoals email en noem maar op, maar dat is hier niet echt belangrijk.
Vervolgens zouden we het volgende kunnen hebben voor blogposts:
{
"_id": "blog-1",
"author_id": "author-1",
"title": "MongoDB is Awesome!",
"content": "This is some blog content..."
}
Net als hiervoor kan het blogdocument andere velden hebben, zoals tags en wat je maar kunt bedenken. Belangrijk hier is dat er een id-referentie is tussen de twee documenten. Daarover zo meer.
Het laatste documentmodel in dit scenario draait om reacties:
{
"_id": "comment-1",
"blog_id": "blog-1",
"username": "Anonymous User",
"content": "Hey great article, it really helped a lot!"
}
Ook hier heeft het reactiedocument een referentierelatie op basis van een id-waarde tussen het reactiedocument en het blogdocument. Dit is heel gebruikelijk in een relationele database, en eerlijk is eerlijk: het zou in MongoDB ook prima werken. Maar dit gaat je niet de beste MongoDB-ervaring geven.
Eén reden om dit niet te doen in MongoDB is performance.
Om elk van deze drie documenten te joinen, zou je een $lookup-operatie binnen een aggregatiepijplijn gebruiken. Deze $lookup-operaties zijn niet goedkoop en kunnen je performance negatief beïnvloeden naarmate je database schaalt, zeker als je meer dan één $lookup-operatie in je pijplijn hebt.
Er zijn betere manieren om dit in MongoDB aan te pakken.
Een betere, meer MongoDB-vriendelijke aanpak voor datamodellering
Een van de eerste regels van MongoDB is dat data die samen wordt opgevraagd, ook samen moet worden opgeslagen. Elke feature in een apart document of een aparte collectie opslaan, doorbreekt die regel, omdat we niet langer met één enkele fetch-operatie de data ophalen die aan de gebruiker wordt getoond.
Laten we het probleem opnieuw benaderen en de documenten voor onze blog als volgt modelleren:
{
"_id": "author-1",
"name": "Nic Raboy",
"description": "I'm some dude named Nic.",
"posts": [
{
"title": "MongoDB is Awesome!",
"content": "This is some blog content...",
"comments": [
{
"username": "Anonymous User",
"content": "Hey great article, it really helped a lot!"
}
]
}
]
}
Prima, daarmee voldoen we aan een van de hoofdregels van MongoDB. We hebben één enkel document dat met nesting alle blogposts en alle reacties bij de auteur opslaat.
Hoewel het zou kunnen werken, is het waarschijnlijk geen goed idee om je blog-appdocumenten zo te modelleren.
MongoDB heeft limieten voor de documentgrootte, en met ongebonden arrays in die documenten loop je het risico die limieten te overschrijden. Daarnaast kun je performanceproblemen krijgen naarmate de arrays groter worden. In dit geval kunnen we een onbeperkt aantal blogposts hebben en een onbeperkt aantal reacties per blogpost. Op een gegeven moment wordt het een rommeltje.
Wat in dit voorbeeld beter kan werken, is een mix van beide strategieën.
Onthoud: auteurs, blogposts en reacties kunnen een willekeurig aantal velden hebben. In dit voorbeeld hielden we het minimaal. Met dat in gedachten weten we dat de meest geraadpleegde data de blog zelf zal zijn. Neem het nieuwe ontwerp:
{
"_id": "blog-1",
"author": {
"_id": "author-1",
"name": "Nic Raboy",
},
"title": "MongoDB is Awesome!",
"content": "This is some blog content...",
"comments": [
{
"username": "Anonymous User",
"content": "Hey great article, it really helped a lot!"
}
]
}
Het tweede document in dit voorbeeld blijft het oorspronkelijke document met auteursinformatie:
{
"_id": "author-1",
"name": "Nic Raboy",
"description": "I'm some dude named Nic."
}
We weten dat sommige auteursinformatie moet worden opgenomen wanneer een blogartikel wordt weergegeven, maar we hebben niet per se alle informatie nodig. We kunnen de benodigde auteursinformatie samen met de _id inbedden in de blogpost, en als de gebruiker meer over de auteur wil weten, kan dieper in je applicatie worden gezocht. De $lookup-operatie gebeurt dus niet elke keer, wat de performance verbetert.
Nu hebben we nog het probleem van het potentieel oneindige aantal reacties bij een blogartikel dat nu als array is gekoppeld.
Er zijn verschillende manieren om dit aan te pakken, en uiteindelijk is de keuze aan jou:
- Je kunt de naïeve aanpak kiezen en ze als array laten staan, wat waarschijnlijk geen geweldig idee is.
- Je kunt de reacties terugzetten naar het oorspronkelijke ontwerp van één reactie per document, allemaal met een referentie-id naar het blogartikel en met
$lookup-operaties om ze op te nemen. - Je kunt een archiveringsstrategie gebruiken: een vast aantal reacties in een array binnen het blogartikel opslaan, en daarnaast voor elke reactie een apart reactiedocument aanmaken, zodat je alleen extra reacties hoeft op te vragen als dat nodig is.
- Je kunt het bucket-patroon verkennen, waarbij je reacties groepeert in “buckets” van ongeveer 100 reacties per document, en je per keer het benodigde aantal buckets opvraagt. Bucketing voorkomt de documentgroottelimiet en helpt ook bij paginering. Je kunt de performance in deze aanpak verder verbeteren door een index op de
blog_idte maken om snel reacties voor een specifiek blogartikel op te halen. Meer informatie over indexering vind je in de MongoDB-documentatie. - Mixen, matchen en meer...
In het geval van het bucket-patroon kunnen je reactiedocumenten er als volgt uitzien:
{
"_id": "comment-bucket-1",
"blog_id": "blog-1",
"comments": [
{
"username": "Anonymous User",
"content": "Hey great article, it really helped a lot!"
},
// More comments here...
],
"comment_count": 57
}
In dit scenario, wanneer er nieuwe reacties worden geplaatst, zoek je een bucket die nog niet vol is. Je pusht de nieuwe reactie in de array en verhoogt de teller. Hoewel deze strategie meer afhankelijk is van logica op applicatieniveau om de buckets te beheren, levert het betere performance op. Heb je een infinite scroll-mechanisme op je blog, dan is dat nog beter, want dan kun je met de buckets beter pagineren.
Hoewel het buiten de scope van dit artikel valt: als je een idee wilt krijgen van wat er nodig is om een nieuwe reactie toe te voegen, filter je de collectie met comment buckets (zie de voorbeeldquery hieronder) op buckets die overeenkomen met de blog_id en een comment_count hebben die lager is dan je gekozen waarde.
Voor de match gebruik je de operator $push in je updatecriteria en de operator $inc om de teller van je aantal reacties te verhogen. Dit kan allemaal in één operatie, en als je de upsert-vlag gebruikt, kun je een nieuwe comment bucket aanmaken als er nog geen bestaat die aan je criteria voldoet.
Voorbeeld van updatequery:
db.commentBuckets.updateOne(
{ blog_id: "blog-1", comment_count: { $lt: 100 } },
{ $push: { comments: newComment }, $inc: { comment_count: 1 } },
{ upsert: true }
)
Conclusie
Hoe fijn het ook zou zijn om alles over je blog in één enkel genest document op te slaan, het is waarschijnlijk geen goed idee. Net zoals het waarschijnlijk geen goed idee is om een datamodel te implementeren dat goed werkt voor een relationele database, maar niet per se voor een documentdatabase zoals MongoDB.
Met MongoDB wil je je gerefereerde relaties zoveel mogelijk verminderen om de best mogelijke performance te krijgen.
Houd het volgende in gedachten terwijl je schaalt:
- Als je veel reacties verwacht, overweeg dan het bucket-patroon of een combinatie daarvan met gedeeltelijke embedding in het blogdocument.
- Wat zijn je pagineringsbehoeften?
- Indexeer je collecties op basis van hoe je in je applicatie gaat query'en.
Nogmaals: denk na over hoe je de blogdata in je frontend gaat presenteren. Heb je echt alle auteursinformatie bij het verzoek nodig of is een deel voldoende? Wil je echt één miljoen reacties met je blogartikel laden of is een klein handjevol prima? Als je er goed over nadenkt, zul je verbaasd zijn hoeveel join-operaties je niet nodig hebt.
Als je nieuw bent met MongoDB, raad ik je aan de cursus Introduction to MongoDB in Python te bekijken.
FAQs
Kan ik het datamodel van mijn blog-app overzetten van een relationele database naar MongoDB?
Je kunt platte documenten met id-referenties maken en dat zou werken, maar je benut dan niet de maximale performance die je met MongoDB kunt halen.
Waarom niet gewoon alles in één document opslaan?
Er zijn groottelimieten in MongoDB en met ongebonden arrays kun je die limieten gemakkelijk bereiken.
Het bucket-patroon werd genoemd, maar zijn er ook andere?
Er zijn andere patronen die kunnen werken, maar het bucket-patroon is een goede optie voor grote arrays.
Wat zijn de limieten voor de documentgrootte in MongoDB?
MongoDB-documenten hebben een maximale grootte van 16 MB.
Hoe ga ik om met updates van geneste data (bijv. een reactie bewerken)?
Voor ingebedde data kun je update-operators zoals $set gebruiken, en voor gebuckette reacties kun je de juiste bucket opzoeken en de specifieke reactie daarin bijwerken.

Nic Raboy is een Developer Relations Lead bij MongoDB, waar hij een team van Python-, Java-, C#- en PHP-ontwikkelaars aanstuurt dat geweldige content maakt om developers te helpen MongoDB succesvol in hun projecten op te nemen. Hij heeft ervaring met Golang en JavaScript en schrijft vaak over zijn ontwikkelavonturen.
