MongoDB
Läs och titta
Bekanta dig översiktligt med organisationen kring databasen MongoDB. Övningen (längre ned) kommer vidare utgå från informationen på denna webbplatsen.
Bekanta dig översiktligt med dokumentationen för “MongoDB Node.js driver” vilken är den driver vi kommer använda för att koppla JavaScript i Node.js till MongoDB. Det handlar både om referens-dokumentationen och API-dokumentationen. Användarexemplen är ett bra ställe att börja.
Sen låter vi Chief Technical Officer Eliot Horowitz hos MongoDB berätta om Dokumentorienterade databaser.
Exempelkod
Om ni vill titta på ett fullständigt exempelprogram som använder alla dessa tekniker är auth_mongo ett bra ställe att börja. auth_mongo repot är en klon av det auth repo som användes i projektet i kursen webapp. Jag har bytt databasen från SQLite till mongodb.
Installation MongoDB
I denna artikel installerar vi MongoDB lokalt på din utvecklingsdator, om du vill och har möjlighet kan du använda MongoDB i Docker. Artikeln MongoDB i Docker visar hur det kan gå till.
Vi kommer sedan använda oss av MongoDB Atlas för att driftsätta vår databas, men mer om det senare.
I kursrepot finns exempelkod under db/mongodb.
Om du vill lära dig mer om mongodb utanför kursen är MongoDB University en bra resurs.
Windows
Gå till MongoDB Community Server och välj ditt operativsystem i listan. Följ sedan installationsinstruktionerna.
MacOS
Installera med hjälp av pakethanteraren brew med kommandona:
$brew tap mongodb/brew
$brew install mongodb-community@5.0
Starta sedan mongodb som en service med kommandot: brew services start mongodb-community@5.0
.
Linux (Debian/Ubuntu)
Vi börjar med att installera dirmngr
, för att kunna ta hand gpg nycklar, med kommandot sudo apt-get install dirmngr
. Vi följer sedan de rekommenderade installationsinstruktionerna hos MongoDB. Se till att välja rätt operativsystem i menyn.
Starta klienten
Det ska nu gå att starta mongodb klienten med kommandot mongosh
i din terminal. Kommandot help
inne i mongodb klienten ger en översikt över tillgängliga kommandon.
$mongosh
> help
db.help() help on db methods
db.mycoll.help() help on collection methods
sh.help() sharding helpers
rs.help() replica set helpers
help admin administrative help
help connect connecting to a db help
help keys key shortcuts
help misc misc things to know
help mr mapreduce
show dbs show database names
show collections show collections in current database
show users show users in current database
show profile show most recent system.profile entries with time >= 1ms
show logs show the accessible logger names
show log [name] prints out the last segment of log in memory, 'global' is default
use <db_name> set current database
db.foo.find() list objects in collection foo
db.foo.find( { a : 1 } ) list objects in foo where a == 1
it result of the last line evaluated; use to further iterate
DBQuery.shellBatchSize = x set default number of items to display on shell
exit quit the mongo shell
Du som är van vid liknande klienter till andra databaser kan känna igen dig bland de kommandon som erbjuds.
Det finns en manual som hjälper dig igång med grunderna och baskommandona.
Skapa en databas
Vi prövar att använda klienten för att skapa en databas och lägga in ett dokument i en collection.
Först skapar vi databasen.
> use mumin
> show collections
Den är tom och innehåller inga collections ännu. Vi skapar en collection genom att lägga ett dokument i den.
> db.crowd.insertOne( { name: "Mumintrollet" } )
{
"acknowledged" : true,
"insertedId" : ObjectId("5a13069000b2ff0b912aeeb6")
}
> show collections
crowd
Om jag fyller på ytterligare några dokument så kan det se ut så här när vi frågar efter innehållet i en collection.
> db.crowd.find()
{ "_id" : ObjectId("5a13069000b2ff0b912aeeb6"), "name" : "Mumintrollet" }
{ "_id" : ObjectId("5a13079100b2ff0b912aeeb7"), "name" : "Sniff" }
{ "_id" : ObjectId("5a13079b00b2ff0b912aeeb8"), "name" : "Snusmumriken" }
{ "_id" : ObjectId("5a1307a900b2ff0b912aeeb9"), "name" : "Snorkfröken" }
Vi kan uppdatera samtliga dokument/objekt i vår collection.
> db.crowd.updateMany({}, {$set: { bor: "Mumindalen" }})
{ "acknowledged" : true, "matchedCount" : 4, "modifiedCount" : 4 }
> db.crowd.find().pretty()
{
"_id" : ObjectId("5a13069000b2ff0b912aeeb6"),
"name" : "Mumintrollet",
"bor" : "Mumindalen"
}
{
"_id" : ObjectId("5a13079100b2ff0b912aeeb7"),
"name" : "Sniff",
"bor" : "Mumindalen"
}
{
"_id" : ObjectId("5a13079b00b2ff0b912aeeb8"),
"name" : "Snusmumriken",
"bor" : "Mumindalen"
}
{
"_id" : ObjectId("5a1307a900b2ff0b912aeeb9"),
"name" : "Snorkfröken",
"bor" : "Mumindalen"
}
>
Det finns alltså ett antal vanliga CRUD-operationer vi kan göra för att jobba med datat i databasen. Du kan läsa mer om dessa CRUD-operationer i manualen.
Låt oss gå vidare och skapa ett program som använder vår nyskapade databas.
Node till MongoDB
Först installerar vi npm-paketet mongodb
som är en Node driver till databasen MongoDB. Det finns redan i package.json
så följande kommandon fungerar.
npm install
npm install mongodb --save
Vi kan läsa om MongoBD Node.JS Driver i dokumentationen. Där finner vi också dokumentationen för API:et och dess metoder.
Setup med grunddata
I filen src/setup.js
finns kod som kopplar upp sig mot MongoDB och skapar databasen mumin, rensar den från innehåll och lägger in en del av befolkningen från mumindalen i en collection crowd
genom att hämta data från filen src/setup.json
.
Du kan pröva köra programmet och därefter koppla dig med klienten mongo för att se att datan ligger på plats.
$node src/setup.js
$mongo --eval "db.crowd.find().pretty()"
MongoDB shell version v3.4.10
connecting to: mongodb://mongodb/mumin
MongoDB server version: 3.4.10
{
"_id" : ObjectId("5a134ec3c28e762f068f48f1"),
"name" : "Mumintrollet",
"bor" : "Mumindalen"
}
{
"_id" : ObjectId("5a134ec3c28e762f068f48f2"),
"name" : "Sniff",
"bor" : "Mumindalen"
}
{
"_id" : ObjectId("5a134ec3c28e762f068f48f3"),
"name" : "Snusmumriken",
"bor" : "Mumindalen"
}
{
"_id" : ObjectId("5a134ec3c28e762f068f48f4"),
"name" : "Snorkfröken",
"bor" : "Mumindalen"
}
Söka information från databasen
I filen src/search.js
finns kod som kopplar upp sig mot MongoDB och söker i databasen. Kodexemplet visar på ett par alternativa sätt att jobba med MongoDB avseende den asynkrona biten. Det API som erbjuds bygger på att man kan välja callbacks eller Promise för att hantera det asynkrona flödet.
Låt oss titta på koden.
Först har vi en funktion som kopplar sig mot databasen och en colletion samt utför själva find-operationen.
/**
* Find documents in an collection by matching search criteria.
*
* @async
*
* @param {string} dsn DSN to connect to database.
* @param {string} colName Name of collection.
* @param {object} criteria Search criteria.
* @param {object} projection What to project in results.
* @param {number} limit Limit the number of documents to retrieve.
*
* @throws Error when database operation fails.
*
* @return {Promise<array>} The resultset as an array.
*/
async function findInCollection(dsn, colName, criteria, projection, limit) {
const client = await mongo.connect(dsn);
const db = await client.db();
const col = await db.collection(colName);
const res = await col.find(criteria, projection).limit(limit).toArray();
await client.close();
return res;
}
Funktionen använder konstruktionen async/await för att serialisera flödet mot databasen. Varje metod som jobbar mot databasen, i exemplet ovan, är asynkron och har alternativet att använda callbacks, eller Promise. I koden ovan bygger vi på att ett Promise returneras när respektive metod är avklarad.
En vanlig frågeställning i en async funktion är om await behövs eller inte. För att besvara det behöver man delvis veta om metoden/funktionen returnerar ett Promise eller ej. Här vänder vi oss till API-manualen för respektive metod. Man kommer inte framåt utan att bli bekant med det API man jobbar med. En vanlig fråga är till exempel vad som returneras inom ett Promise, vilka argument man har tillgång till. API manualen ger svaret.
Låt oss titta på hur vi kan använda funktionen ovan. Jag kan visa två alternativ och vi börjar med async/await.
Jag lägger premisserna för sökningen i variabler, för tydlighetens skull.
// Find documents where namn starts with string
const criteria2 = {
namn: /^Sn/
};
const projection2 = {
_id: 1,
namn: 1
};
const limit2 = 3;
Sedan wrappar jag koden i en Immediately Invoked Async Arrow Function för att kunna använda await inom funktionen.
// Do it within an Immediately Invoked Async Arrow Function.
// This is to enable usage of await within the function scope.
(async () => {
// Find using await
try {
let res = await findInCollection(
dsn, "crowd", criteria2, projection2, limit2
);
console.log(res);
} catch(err) {
console.log(err);
}
})();
Jag lägger koden inom en traditionell try/catch för att hantera eventuella fel som uppkommer. Jag använder await på findInCollection()
och lägger svaret i en variabel. På det sättet löser jag serialiseringen.
Vi tittar på en annan variant.
(() => {
// Find using .then()
findInCollection(dsn, "crowd", criteria1, projection1, limit1)
.then(res => console.log(res))
.catch(err => console.log(err));
})();
Här finns inget krav på att använda async, ej heller att wrappa koden inom ett funktionsscope. Serialiseringen sköts av .then()
och felhanteringen i .catch()
.
Vi hade också kunnat tänka oss en variant av findInCollection()
som jobbar med callbacks. Funkionen hade isåfall tagit ytterligare ett argument callback
som hade anropats när funktionen var klar.
Lägga till och uppdatera data
Vi har i ovanstående sett hur vi läser data från databasen och i exempelkoden db/mongodb/src/setup.js
finns ett exempel där funktionen insertMany används.
Förutom insertMany
finns insertOne
funktionen där man lägger till ett dokument i databasen. Om vi vill lägga till ett dokument med attributen name
och html
gör vi på följande sätt.
const doc = {
name: body.name,
html: body.html,
};
const result = await db.collection.insertOne(doc);
MongoDB lägger automatiskt till ett _id
fält i dokumentet/objektet och vi kan kolla om allt gått bra och titta på det objekt vi har lagt till med följande kod. result.ops
innehåller det objekt som har lagts till i databasen bland annat det automatgenererade _id
.
if (result.result.ok) {
return res.status(201).json({ data: result.ops });
}
Detta _id
behövs sedan när vi vill uppdatera dokumentet i databasen. Vi gör det med funktionen updateOne. Först importerar vi ObjectId funktionen för att kunna hitta rätt _id
i databasen. Vi skapar sedan ett filter
och ett updateDocument
och använder oss av updateOne
. Bara de fält som skickas in uppdateras, vill vi ersätta dokumentet istället kan vi använda replaceOne
.
const ObjectId = require('mongodb').ObjectId;
const filter = { _id: ObjectId(body["_id"]) };
const updateDocument = {
name: body.name,
html: body.html,
};
const result = await db.collection.updateOne(
filter,
updateDocument,
);
Express till MongoDB
Hur kan det se ut om vi kopplar in databasen MongoDB mot en instans av Express? Låt oss titta på ett exempel i src/server.js
som exponerar en route /list
som visar allt innehåll i en collection i databasen.
Vi kan starta upp server och testa att accessa routen.
$npm start
Server is listening on 1337
Via en webbläsare eller curl kan vi nu komma åt routen och med kommadnot jq får vi en renare utskift.
$curl -s http://localhost:1337/list | jq
[
{
"_id": "5a13efb54dbe18550bce601b",
"namn": "Mumintrollet",
"bor": "Mumindalen"
},
{
"_id": "5a13efb54dbe18550bce601c",
"namn": "Sniff",
"bor": "Mumindalen"
},
{
"_id": "5a13efb54dbe18550bce601d",
"namn": "Snusmumriken",
"bor": "Mumindalen"
},
{
"_id": "5a13efb54dbe18550bce601e",
"namn": "Snorkfröken",
"bor": "Mumindalen"
}
]
Som vi kunde ana var det inget större bekymmer att flytta in vår kod i en route i express som stödjer async funktioner som callbacks till en route.
Bryta ut databas koden
För att få mer DRY kod och för att underlätta för testning lite längre fram i kursen är detta ett bra tillfälle att bryta ut hanteringen av databas uppkopplingen till en egen modul. Jag har i mitt projekt skapat en katalog db
där jag har lagt filen database.js
. Här skapar jag först en dsn
sträng. Om vi håller på att testa koden ändrar jag den till en test databas istället, mer om detta när vi senare i kursen ska testa vår applikation.
Nästa steg är som vi tidigare har gjort i exempelprogrammen att koppla oss mot databasen och som det sista steget att returnera client
och collection
.
const mongo = require("mongodb").MongoClient;
const config = require("./config.json");
const collectionName = "docs";
const database = {
getDb: async function getDb () {
let dsn = `mongodb://localhost:27017/folinodocs`;
if (process.env.NODE_ENV === 'test') {
dsn = "mongodb://localhost:27017/test";
}
const client = await mongo.connect(dsn, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
const db = await client.db();
const collection = await db.collection(collectionName);
return {
collection: collection,
client: client,
};
}
};
module.exports = database;
Vi kan sedan i koden hämta databasen, ställa frågor och sedan stänga ner databasen.
const db = await database.getDb();
const resultSet = await db.collection.find({}).toArray();
await db.client.close();
Felhantering av frågor till databasen
Vi har ovan sett en kort introduktion till felhantering. Och här kommer ett lite längre exempel där vi även tittar på hur vi kan stänga ner databasen. Vi använder oss av konstruktionen try-catch-finally
(Dokumentation).
let db;
try {
db = await database.getDb();
const filter = { email: email };
const keyObject = await db.collection.findOne(filter);
if (keyObject) {
return res.json({ data: keyObject });
}
} catch (e) {
return res.status(500).json({
errors: {
status: 500,
source: "/",
title: "Database error",
detail: e.message
}
});
} finally {
await db.client.close();
}
Finally delen av konstruktionen utförs alltid både när det har gått bra och vid fel.