Naar de hoofdinhoud gaan

MongoDB

Inleiding​

MongoDB is een documentgeoriënteerde NoSQL-database. In plaats van tabellen en rijen worden de gegevens opgeslagen in documenten in een JSON-achtig formaat, wat een grote flexibiliteit in het datamodel mogelijk maakt.

Netuno biedt de resource _mongo voor de integratie met MongoDB, met een low-code abstractie bovenop de officiële MongoDB-client voor Java, beschikbaar voor de verschillende programmeertalen die door Netuno worden ondersteund: JavaScript, Python, Ruby, Kotlin en Groovy.

Voordat je verdergaat, is het aanbevolen om te weten hoe je services in Netuno maakt; zie de tutorial REST - Web Services.

Een voorbeeldimplementatie van de hier gepresenteerde bewerkingen is beschikbaar in de voorbeeldservice mongo.js, die zich bevindt in:

  • apps/demo/server/services/samples/javascript/mongo.js

MongoDB installeren​

Met Docker​

Ervan uitgaande dat je Docker hebt geïnstalleerd, hoef je alleen de MongoDB-image te downloaden en een nieuwe container te starten:

docker pull mongo
docker run -d --name mongo -p 27017:27017 mongo

Op deze manier is MongoDB toegankelijk op localhost:27017 zonder authenticatie.

Om de databasegebruiker aan te maken volgens de configuratie van de applicatie, voer je uit:

docker exec -it mongo mongosh products_db --eval 'db.createUser({ user: "products_user", pwd: "12345678", roles: [{ role: "readWrite", db: "products_db" }] })'

Met Ubuntu​

MongoDB is niet opgenomen in de officiële Ubuntu-repositories, daarom is het nodig om de officiële repository toe te voegen vóór de installatie:

sudo apt-get install gnupg curl
curl -fsSL https://www.mongodb.org/static/pgp/server-7.0.asc | sudo gpg -o /usr/share/keyrings/mongodb-server-7.0.gpg --dearmor
echo "deb [ signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg ] http://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list
sudo apt-get update
sudo apt-get install -y mongodb-org

Na de installatie kun je de service starten met:

sudo systemctl start mongod

Met Windows​

Voer de gedownloade installer uit en activeer in de stap Service Configuration de optie Install MongoD as a Service zodat MongoDB automatisch opstart. MongoDB is toegankelijk op localhost:27017 zonder authenticatie.

Raadpleeg de officiële MongoDB-documentatie voor andere distributies en versies.

Configuratie van de applicatie​

De configuratie van de MongoDB-verbindingen gebeurt in het configuratiebestand van de applicatie, afhankelijk van de omgeving:

  • config/_development.json

Lees meer over de configuratiebestanden van de applicatie.

Voeg in het configuratiebestand het blok mongo toe, waarbij elke sleutel een verbinding identificeert:

{
...
"mongo": {
"store": "mongodb://store_user:12345678@localhost/store_db",
"products": {
"username": "products_user",
"password": "12345678",
"host": "127.0.0.1",
"port": "27017",
"database": "products_db"
}
},
...
}

Een verbinding kan op twee manieren worden gedefinieerd:

  • Als verbindings-URL — de waarde is simpelweg een string, bijvoorbeeld "mongodb://store_user:12345678@localhost/store_db".
  • Als configuratieobject — met de velden:
    • url — indien gedefinieerd, wordt het rechtstreeks als verbindings-URL gebruikt, waarbij de andere velden worden genegeerd.
    • protocol — het protocol van de verbinding, standaard mongodb.
    • username en password — de toegangsgegevens.
    • host — het adres van de server, standaard localhost.
    • port — de poort van de verbinding, standaard 27017.
    • database — de naam van de database, standaard de naam van de verbindingssleutel.
    • params — extra verbindingsparameters die aan de URL worden toegevoegd, bijvoorbeeld { "authSource": "admin" }. Ze worden alleen toegepast wanneer de URL wordt opgebouwd uit de velden van het object.

De verbinding hoeft niet de hoofddatabase van de applicatie te zijn. In tegenstelling tot de configuratie db is het blok mongo onafhankelijk en maakt het meerdere gelijktijdige verbindingen mogelijk.

De verbinding initialiseren​

De eerste stap is het initialiseren van de MongoDB-client met de resource _mongo.init(...), door de naam van de configuratiesleutel of rechtstreeks een verbindings-URL door te geven:

const mongo = _mongo.init("products")

Je kunt de verbinding ook rechtstreeks met een URL initialiseren:

const mongo = _mongo.init("mongodb://localhost:27017/products_db")

Bij het aanroepen van _mongo.init() zonder argumenten wordt de configuratiesleutel default gebruikt. Het wordt aanbevolen om altijd de naam van de sleutel of de URL door te geven, zodat de gebruikte verbinding expliciet is.

Database en collecties​

Met de geïnitialiseerde client verkrijg je de database-instantie met database("naam"). Vervolgens kun je collecties aanmaken en een bestaande collectie benaderen:

const db = mongo.database("products_db")

db.createCollection("product")

const collection = db.collection("product")

Je kunt de verbinding met de database testen met ping(), en alle bestaande collecties opvragen met collectionNames():

db.ping()

const names = db.collectionNames()

Je kunt een collectie hernoemen met renameCollection(), door de volledige naam met de database door te geven, of de naam van de database en de nieuwe naam van de collectie afzonderlijk:

collection.renameCollection("products_db.product_archive")

collection.renameCollection("archive_db", "product_archive")

Om een collectie en al haar documenten te verwijderen, gebruik je drop():

collection.drop()

Documenten invoegen​

De gegevens worden weergegeven met de resource _val, via _val.map() voor documenten en _val.list() voor lijsten met waarden.

Een enkel document invoegen​

Met insertOne() voeg je een document in en verkrijg je het gegenereerde ID:

const id = collection.insertOne(
_val.map()
.set("name", "Laptop")
.set("quantity", 22)
.set("price", 100)
.set("category", "computers")
)

De documenten kunnen geneste structuren bevatten, zoals lijsten en andere documenten:

collection.insertOne(
_val.map()
.set("name", "Tablet")
.set("tags",
_val.list()
.add("promo")
.add("new")
)
.set("details",
_val.map()
.set("color", "blue")
.set("stock", 10)
)
)

Meerdere documenten invoegen​

Met insertMany() voeg je meerdere documenten in één keer in, en ontvang je de lijst met gegenereerde ID's:

const ids = collection.insertMany(
_val.list()
.add(
_val.map()
.set("name", "Smartphone")
.set("quantity", 18)
)
.add(
_val.map()
.set("name", "Monitor")
.set("quantity", 8)
)
)

Documenten opvragen​

De methode find() retourneert een MongoFindIterable, waaruit je de documenten verkrijgt met all() (alle) of first() (de eerste, of null als er geen is).

Om alle documenten van de collectie te verkrijgen:

const docs = collection.find().all()

Om met een filter op te vragen, gebruik je de factory _mongo.filters():

const docs = collection.find(
_mongo.filters().eq("name", "Laptop")
).all()

Filters​

De factory _mongo.filters() biedt de belangrijkste query-operatoren van MongoDB:

OperatorBeschrijving
eq("veld", waarde)Gelijk aan de waarde.
ne("veld", waarde)Verschillend van de waarde.
gt("veld", waarde)Groter dan de waarde.
gte("veld", waarde)Groter dan of gelijk aan de waarde.
lt("veld", waarde)Kleiner dan de waarde.
lte("veld", waarde)Kleiner dan of gelijk aan de waarde.
in("veld", waarde1, waarde2, ...)Gelijk aan een van de waarden.
nin("veld", waarde1, waarde2, ...)Verschillend van alle waarden.
and(filter1, filter2, ...)Komt overeen met alle filters.
or(filter1, filter2, ...)Komt overeen met ten minste één van de filters.
not(filter)Komt niet overeen met het filter.
regex("veld", "patroon")Komt overeen met de reguliere expressie.
exists("veld")Het veld bestaat in het document.
text("zoekopdracht")Tekstzoekopdracht.
type("veld", "type")Het veld is van het opgegeven type.
mod("veld", deler, rest)De rest van de deling van het veld door de deler is gelijk aan de waarde.
where("expressie")Komt overeen met de documenten die voldoen aan een JavaScript-expressie, bijvoorbeeld "this.price > 100".
size("veld", n)Het matrixveld heeft precies n elementen.

Bijvoorbeeld, om de producten met een prijs hoger dan 50 en een hoeveelheid lager dan 30 te verkrijgen:

const docs = collection.find(
_mongo.filters().and(
_mongo.filters().gt("price", 50),
_mongo.filters().lt("quantity", 30)
)
).all()

Of om de producten te verkrijgen waarvan de naam overeenkomt met een reguliere expressie:

const docs = collection.find(
_mongo.filters().regex("name", "^[LS]")
).all()

Door de resultaten itereren​

Met het resultaat van de query kun je door alle gevonden documenten itereren:

for (const doc of collection.find().all()) {
_out.println(doc.getString("name"))
}

Je kunt ook door de resultaten itereren met forEach() door een functie door te geven:

collection.find().forEach((doc) =>
_out.println(doc.getString("name"))
)

Sorteren​

Met de factory _mongo.sorts() bepaal je de volgorde van de resultaten, oplopend of aflopend:

const docs = collection.find().sort(
_mongo.sorts().descending("price")
).all()

Om op meerdere velden te sorteren, gebruik je orderBy():

const docs = collection.find().sort(
_mongo.sorts().orderBy(
_mongo.sorts().descending("price"),
_mongo.sorts().ascending("quantity")
)
).all()

Projectie van velden​

Met de factory _mongo.projections() beperk je de velden die in de documenten worden geretourneerd:

const docs = collection.find().projection(
_mongo.projections().include("name", "quantity")
).all()

Om meerdere projecties te combineren:

const docs = collection.find().projection(
_mongo.projections().fields(
_mongo.projections().include("name", "quantity"),
_mongo.projections().excludeId()
)
).all()

De beschikbare projectietypen zijn de volgende:

ProjectieBeschrijving
include("veld", ...)Neemt alleen de opgegeven velden op.
exclude("veld", ...)Sluit de opgegeven velden uit.
excludeId()Sluit het veld _id uit.
fields(projectie1, ...)Combineert projecties.
slice("veld", n)Neemt alleen de eerste n elementen van het matrixveld op.
slice("veld", sprong, n)Neemt n elementen van het matrixveld op, vanaf na de eerste sprong elementen.
elemMatch("veld")Neemt alleen het eerste element van het matrixveld op.
elemMatch("veld", filter)Neemt alleen het eerste element van de matrix op dat overeenkomt met het filter.
computed("veld", expressie)Voegt een berekend veld toe op basis van een aggregatie-expressie.

Limieten en sprongen​

Met limit() en skip() beheer je het aantal geretourneerde resultaten:

const docs = collection.find()
.skip(1)
.limit(2)
.all()

Documenten bijwerken​

Met de factory _mongo.updates() definieer je de toe te passen wijzigingen, en met updateOne() of updateMany() pas je de wijzigingen toe op de documenten die overeenkomen met het filter:

collection.updateOne(
_mongo.filters().eq("name", "Laptop"),
_mongo.updates().set("quantity", 42)
)

collection.updateMany(
_mongo.filters().eq("category", "computers"),
_mongo.updates().set("category", "featured")
)

De factory _mongo.updates() biedt de update-operatoren:

OperatorBeschrijving
set("veld", waarde)Stelt de waarde van een veld in.
unset("veld")Verwijdert een veld uit het document.
rename("veld", "nieuweNaam")Wijzigt de naam van een veld.
push("veld", waarde)Voegt een waarde toe aan een array.
combine(update1, update2, ...)Combineert meerdere updates in één.

Om meerdere wijzigingen in één keer toe te passen, gebruik je combine():

const combined = _mongo.updates().combine(
_mongo.updates().set("quantity", 42),
_mongo.updates().rename("other", "more")
)

collection.updateOne(
_mongo.filters().eq("name", "Laptop"),
combined
)

Zoeken en bijwerken​

Met findOneAndUpdate() zoek en werk je een document atomair bij, waarbij het oorspronkelijke document wordt geretourneerd (of null als er geen is):

const old = collection.findOneAndUpdate(
_mongo.filters().eq("name", "Laptop"),
_mongo.updates().set("quantity", 42)
)

Documenten vervangen​

Met replaceOne() vervang je het volledige document (in tegenstelling tot updates mag het geen update-operatoren bevatten):

collection.replaceOne(
_mongo.filters().eq("name", "Laptop"),
_val.map()
.set("name", "Laptop")
.set("quantity", 50)
.set("price", 150)
.set("category", "featured")
)

Om atomair te zoeken en te vervangen, waarbij het oude document wordt geretourneerd, gebruik je findOneAndReplace():

const old = collection.findOneAndReplace(
_mongo.filters().eq("name", "Laptop"),
_val.map()
.set("name", "Laptop")
.set("quantity", 50)
)

Documenten verwijderen​

Met deleteOne() en deleteMany() verwijder je de documenten die overeenkomen met het filter:

collection.deleteOne(
_mongo.filters().eq("name", "Laptop")
)

collection.deleteMany(
_mongo.filters().eq("category", "inactive")
)

Om atomair een document te zoeken en te verwijderen, waarbij het verwijderde document wordt geretourneerd, gebruik je findOneAndDelete():

const old = collection.findOneAndDelete(
_mongo.filters().eq("name", "Tablet")
)

Om alle documenten van de collectie te verwijderen, geef je een leeg filter door met behulp van _mongo.valToDoc():

collection.deleteMany(_mongo.valToDoc(_val.map()))

Documenten tellen​

Met countDocuments() verkrijg je het aantal documenten van de collectie, optioneel met een filter. estimatedDocumentCount() is sneller omdat het de metadata van de collectie gebruikt, maar zonder filter:

const total = collection.countDocuments()

const totalMain = collection.countDocuments(
_mongo.filters().eq("category", "computers")
)

const estimated = collection.estimatedDocumentCount()

Indexen​

Met de factory _mongo.indexes() maak je indexen om de prestaties van queries te verbeteren. Om een index te maken, hoef je alleen de specificaties aan createIndex() door te geven:

collection.createIndex(
_mongo.indexes().ascending("quantity")
)

collection.createIndex(
_mongo.indexes().compoundIndex(
_mongo.indexes().descending("price"),
_mongo.indexes().ascending("quantity")
)
)

Je kunt MongoDB aangeven welke index in een query moet worden gebruikt, met hint():

const docs = collection.find().hint(
_mongo.valToDoc(
_val.map().set("quantity", 1)
)
).all()

Met min() en max() beperk je het bereik van indexwaarden dat door de query wordt doorlopen:

const docs = collection.find()
.min(
_mongo.valToDoc(
_val.map().set("quantity", 10)
)
)
.max(
_mongo.valToDoc(
_val.map().set("quantity", 50)
)
)
.all()

Aggregaties​

Voor geavanceerdere analyses maakt aggregate() het mogelijk een aggregatie-pipeline uit te voeren, waarbij elke fase wordt gedefinieerd met de factory _mongo.aggregates() en de accumulatoren met _mongo.accumulators().

Bijvoorbeeld, om de producten per categorie te groeperen en de prijs van elke groep op te tellen:

const docs = collection.aggregate(
_mongo.aggregates().match(
_mongo.filters().eq("category", "computers")
),
_mongo.aggregates().group(
"$category",
_mongo.accumulators().sum("total", "$price")
),
_mongo.aggregates().sort(
_mongo.sorts().descending("total")
)
).all()

De beschikbare aggregatiefasen zijn de volgende:

FaseBeschrijving
match(filter)Selecteert alleen de documenten die overeenkomen met het filter.
group("$veld", accumulatoren)Groepeert de documenten per veld en past de accumulatoren toe.
project(projectie)Voegt velden toe, verwijdert of wijzigt ze in de documenten.
sort(sortering)Sorteert de documenten.
limit(n)Beperkt tot de eerste n documenten.
skip(n)Slaat de eerste n documenten over.
count()Telt het aantal documenten.
lookup("collectie", "lokaalVeld", "vreemdVeld", "als")Voert een join uit met een andere collectie.

En de meest gebruikte accumulatoren:

AccumulatorBeschrijving
sum("veld", "$expressie")Som van de waarden.
avg("veld", "$expressie")Gemiddelde van de waarden.
min("veld", "$expressie")Minimale waarde.
max("veld", "$expressie")Maximale waarde.
first("veld", "$expressie")Waarde van het eerste document.
last("veld", "$expressie")Waarde van het laatste document.
top("veld", sortering, "$expressie")Bovenste waarde volgens de sortering.
bottom("veld", sortering, "$expressie")Onderste waarde volgens de sortering.

In de accumulatoren verwijst de expressie met prefix $ naar het veld van een invoerdocument, bijvoorbeeld "$price".

Conversie tussen Values en BSON​

Soms is het nodig om te converteren tussen de _val-objecten van Netuno en BSON-documenten van MongoDB:

const doc = _mongo.valToDoc(
_val.map().set("name", "Laptop")
)

const values = _mongo.docToVal(doc)

Bewerkingen met opties​

De factories *Options() van de resource _mongo retourneren de standaard optie-objecten van de MongoDB-driver, en je kunt de methoden ervan rechtstreeks koppelen. Bijvoorbeeld, upsert(true) voegt het document in wanneer het filter niet overeenkomt met een bestaand document:

const old = collection.findOneAndUpdate(
_mongo.filters().eq("name", "Monitor"),
_mongo.updates().set("quantity", 8),
_mongo.findOneAndUpdateOptions().upsert(true)
)

Wanneer het document wordt ingevoegd, retourneert findOneAndUpdate() null.

De beschikbare optie-factories zijn: insertOneOptions(), insertManyOptions(), updateOptions(), findOneAndUpdateOptions(), replaceOptions(), findOneAndReplaceOptions(), deleteOptions(), findOneAndDeleteOptions(), countOptions(), estimatedDocumentCountOptions(), dropCollectionOptions() en textSearchOptions().

De verbinding sluiten​

Na afloop is het een goede gewoonte om de verbinding met MongoDB te sluiten:

_mongo.close()

Conclusie​

Met de resource _mongo biedt Netuno een low-code en polyglot abstractie bovenop de officiële MongoDB-client voor Java, waarmee je kunt werken met documenten, filters, sorteringen, projecties, updates, indexen en aggregaties in JavaScript, Python, Ruby, Kotlin en Groovy.

Voor meer details over alle beschikbare methoden, raadpleeg de documentatie van de resource _mongo en van de klasse MongoCollection.

Veel ontwikkelplezier!