Summary for the webdev backend course at HdM Stuttgart
2023-01-28
These study materials are heavily based on professor Toenniessen’s “Web Development Backend” lecture at HdM Stuttgart and prior work of fellow students.
Found an error or have a suggestion? Please open an issue on GitHub (github.com/pojntfx/uni-webdev-backend-notes):
If you like the study materials, a GitHub star is always appreciated :)
Uni Webdev Backend Notes (c) 2023 Felicitas Pojtinger and contributors
SPDX-License-Identifier: AGPL-3.0
Vorteile:
Sehr einfache APIs, schnell zu lernen
Nachteile:
Node.js hat ein Modul-Konzept, das es ermöglicht, Funktionen und Variablen in eigene Dateien auszulagern und sie in anderen Dateien zu importieren.
Ein Beispiel dafür ist die Funktion add(x,y)
, die in
eine separate Datei namens 01d_Export.js ausgelagert
wird:
.exports = function add(x, y) {
modulereturn x + y;
; }
In einer anderen Datei, z.B. 01d_AddiererFctModule.js, wird das Modul importiert und verwendet:
const add = require("./01d_Export");
const a = 5,
= 7;
b const s = add(a, b);
console.log(`${a} + ${b} = ${s}`);
require
/module
Export-Varianten:
.exports = function add(x, y) {
modulereturn x + y;
; }
.exports = {
moduleadd: (a, b) => a + b,
subtract: (a, b) => a - b,
; }
.add = (a, b) => a + b;
exports.subtract = (a, b) => a - b; exports
Wenn Sie jedoch module.exports direkt zuweisen, werden alle vorherigen Exporte überschrieben.
Import-Varianten:
const fs = require("fs");
.readFile();
fs
const readFile = require("fs").readFile;
readFile();
const { readFile } = require("fs");
readFile();
const { readFile, ...fs } = require("fs");
readFile();
.writeFile(); fs
Export:
export function add(x, y) {
return x + y;
}
export function subtract(x, y) {
return x - y;
}
Default-Export:
export default (x, y) {
return x - y;
}
Import eines gesamten Moduls:
import * as math from "./math.js";
console.log(math.add(5, 2)); // Ausgabe: 7
console.log(math.subtract(5, 2)); // Ausgabe: 3
Import mit destrukturierenden Zuweisung:
import { add as addition, subtract } from "./math.js";
console.log(addition(5, 2)); // Ausgabe: 7
console.log(subtract(5, 2)); // Ausgabe: 3
Callbacks:
const fs = require("fs");
.readFile("file.txt", function (err, data) {
fsif (err) throw err;
console.log(data);
; })
Promises:
const fs = require("fs").promises;
.readFile("file.txt")
fs.then((data) => console.log(data))
.catch((err) => console.error(err));
async/await:
const fs = require("fs").promises;
async function readFileExample() {
try {
const data = await fs.readFile("file.txt");
console.log(data);
catch (err) {
} console.error(err);
}
}
readFileExample();
Mit Support für ein paar wenige MIME-Types:
const http = require("http");
const fs = require("fs");
const { extname } = require("path");
const app = http.createServer((request, response) => {
.readFile(__dirname + request.url, (err, data) => {
fsconst status = err ? 400 : 200;
if (extname(request.url) == ".html")
.writeHead(200, { status, "Content-Type": "text/html" });
responseif (extname(request.url) == ".js")
.writeHead(200, { status, "Content-Type": "text/javascript" });
responseif (extname(request.url) == ".css")
.writeHead(200, { status, "Content-Type": "text/css" });
response
.write(data);
response.end();
response;
});
})
.listen(3000);
app
console.log("Listening on :3000");
Mit Support für ein alle MIME-Types:
$ npm install node-static
const http = require("http");
const fileserver = new (require("node-static").Server)();
const app = http.createServer((request, response) => {
.serve(request, response);
fileserver;
})
.listen(3000);
app
console.log("Listening on :3000");
Mit Support für ein alle MIME-Types & Express:
$ npm install express
const express = require("express");
const app = express();
.use("/WDBackend", express.static(__dirname + "/public"));
app.listen(3000);
app
console.log("Listening on :3000");
require
importiert werden, ohne dass ein relativer Pfad angegeben
werden muss.npm install paket-name
oder
npm i paket-name
npm i -g paket-name
npm i -D paket-name
package-lock.json
enthält die exakten
Versionen aller installierten Abhängigkeiten.node_modules
Ordner enthält die Dateien
aller installierten Pakete.node_modules
Ordner sollte immer von
Git-Commits ausgeschlossen werdenpackage-lock.json
sollte hingegen immer
committed werden.package.json
npm init
erstellt werdenscripts
in der package.json
Datei
können Command-Line Befehle gespeichert werden, die später ausgeführt
werden können, indem man sie in der Kommandozeile aufruft, z.B.
npm run start
oder npm run test
.Best Practice: Pakete sollten immer im Projekt installiert werden, damit dort alle Abhängigkeiten definiert sind. CLI-Tools können auch global, z.B. zur Projektinitialisierung, installiert werden.
Jahr 2000: Überlastung der Web-Backends (Server)
Heute: Zustandslose Web-Backends (Server)
Sichere und idempotente Schnittstellen:
Unsichere und nicht-idempotente Schnittstelle: POST (Create auf eine Ressource). Im Gegensatz zu DELETE können hier nach einem erneuten Anruf ohne Checks weitere Objekte erstellt werden.
Pfad:
http://127.0.0.1:3000/fruits
const DATA = [
id: 1, name: "Apfel", color: "gelb,rot" },
{ id: 2, name: "Birne", color: "gelb,grün" },
{ id: 3, name: "Banane", color: "gelb" },
{ ;
]
.get("/fruits", (req, res) => {
app.send(DATA);
res; })
URL-Parameter:
http://127.0.0.1:3000/fruits/2
const DATA = [
id: 1, name: "Apfel", color: "gelb,rot" },
{ id: 2, name: "Birne", color: "gelb,grün" },
{ id: 3, name: "Banane", color: "gelb" },
{ ;
]
.get("/fruits/:id", (req, res) => {
appconst id = parseInt(req.params.id);
const item = DATA.find((o) => o.id === id);
.send(item);
res; })
Query-Parameter:
http://127.0.0.1:3000/fruits/2
const DATA = [
id: 1, name: "Apfel", color: "gelb,rot" },
{ id: 2, name: "Birne", color: "gelb,grün" },
{ id: 3, name: "Banane", color: "gelb" },
{ ;
]
.get("/fruits", (req, res) => {
appconst id = parseInt(req.query.id);
const item = DATA.find((o) => o.id === id);
.send(item);
res; })
HTTP-Body (URL-Encoded)
Nutze
express.urlencoded
.use(express.urlencoded({ extended: true }));
app
const DATA = [{ id: 1, name: "Apfel", color: "gelb,rot" }];
.post("/fruits", (req, res) => {
appconst { name, color } = req.body;
if (DATA.find((o) => o.name === name)) {
.send("Duplicate name");
reselse {
} const id = Math.max(...DATA.map((o) => o.id)) + 1;
const fruit = { id, name, color };
.push(fruit);
DATA.send(fruit);
res
}; })
HTTP-Body (JSON)
Nutze
express.json
.use(express.json());
app
const DATA = [{ id: 1, name: "Apfel", color: "gelb,rot" }];
.post("/fruits", (req, res) => {
appconst { name, color } = req.body;
if (DATA.find((o) => o.name === name)) {
.send("Duplicate name");
reselse {
} const id = Math.max(...DATA.map((o) => o.id)) + 1;
const fruit = { id, name, color };
.push(fruit);
DATA.send(fruit);
res
}; })
.get("/ab?cd", function (req, res) {
app.send("ab?cd");
res; // acdabcd
})
.get("/ab+cd", function (req, res) {
app.send("ab+cd");
res; // abcdabbbbcd
})
.get("/ab.*cd", function (req, res) {
app.send("ab.*cd");
res; // abcdabxcd
})
.get("/ab(cd)?e", function (req, res) {
app.send("ab(cd)?e");
res; // /abe und /abcde
})
.get(/a/, function (req, res) {
app.send("/a/");
res; // alles mit 'a' drin
})
.get(/.*fly$/, function (req, res) {
app.send("/.*fly$/");
res; })
Wildcard-Route:
.all(/.*/, (req, res, next) => {
appconsole.log(`wildcard-route: ${req.method} ${req.url}`);
next();
; })
Middleware (empfohlen):
.use((req, res, next) => {
appconsole.log(`middleware: ${req.method} ${req.url}`);
next();
; })
Die next()
-Methode führt immer den nächsten passenden
Routen-Handler aus.
let cb0 = function (req, res, next) {
console.log("CB0");
next();
;
}
let cb1 = function (req, res, next) {
console.log("CB1");
next();
;
}
let cb2 = function (req, res) {
.send("Hello from CB2!");
res;
}
.get("/example/c", [cb0, cb1, cb2]); app
Wichtig: next
nicht vergessen!
Mehrere HTTP-Verben für eine Route können mithilfe von Chaining Routes zusammengefasst werden.
app.route("/books")
.get(function (req, res) {
.send("Get all books");
res
}).post(function (req, res) {
.send("Add a book");
res;
})
app.route("/books/:id")
.put(function (req, res) {
.send("Update the book");
res
}).delete(function (req, res) {
.send("Delete the book");
res; })
Vorteile: weniger fehleranfällig, leichter zu pflegen (da man die Route nur einmal schreibt)
Modularisierung von Routen in Express kann mithilfe von
express.Router
erreicht werden.
Erstellung einer Router-Datei birds.js
:
const express = require("express");
const router = express.Router();
// Middleware
.use(function timeLog(req, res, next) {
routerconsole.log("Time: ", Date.now());
next();
;
})
// Routen
.get("/", function (req, res) {
router.send("Birds home page");
res;
})
.get("/about", function (req, res) {
router.send("About birds");
res;
})
.exports = router; module
Einbindung des Routers in die Anwendung app.js
:
const express = require("express");
const app = express();
const birds = require("./birds");
.use("/birds", birds);
app
.listen(3000);
app
console.log("Listening on :3000");
Result:
res.status(code)
: Setzt den HTTP-Statuscode der Antwort
(z.B. 200 für erfolgreiche Anfrage, 404 für nicht gefunden)res.redirect(url)
: Leitet den Request an eine andere
URL umres.cookie(key, value, options)
: Setzt ein Cookie im
Browser des Users, optionale Parameter können angegeben werden wie z.B.
die Dauer des Cookies und ob es sicher übertragen werden sollres.attachment(path_to_file)
: Sendet eine Datei als
Attachment (z.B. Download)res.download(path_to_file)
: Sendet eine Datei zum
Download und zeigt eine entsprechende Benachrichtigung im Browser des
UsersRequest:
req.headers()
: Gibt ein Objekt mit allen
HTTP-Request-Headern zurückreq.cookies()
: Gibt ein Objekt mit allen Cookies
zurück, die im Request enthalten sind (benötigt die Middleware
cookie-parser
)404 als JSON zurückgeben:
.use("/users", require("./routes/users"));
app.use("/products", require("./routes/products"));
app
// Middleware nach allen Routes
.use((req, res) => {
app.status(404);
res.json({ message: "Not found" });
res; })
Exceptions:
try {
throw new Error("Something went wrong");
catch (err) {
} .status(500).json({ message: "InternalServerError" });
res }
.get("/", async (req, res, next) => {
apptry {
throw new Error("Something went wrong");
catch (err) {
} next(err);
};
})
.use((err, req, res, next) => {
app.status(500);
res.json({ message: "InternalServerError" });
resconsole.error(err);
; })
const express = require("express");
const methodOverride = require("method-override");
const app = express();
.use(methodOverride("_method")); app
Jetzt kann man eine PATCH-Route definieren, die dann auch über ein Formular angesprochen werden kann:
.patch("/fruits", (req, res) => {
app// some code ...
; })
In dem Formular muss dann der URL-Parameter
_method=patch
hinzugefügt werden:
<form action="/fruits?_method=patch" method="post">...</form>
Jetzt wird die PATCH-Route aufgerufen, wenn das Formular abgeschickt wird.
Der Code auf dem Server, der die EJS-Template-Engine verwendet, sieht wie folgt aus:
const express = require("express");
const app = express();
.set("view engine", "ejs");
app
.get("/user", (req, res) => {
appconst user = {
name: "John Doe",
email: "johndoe@example.com",
phone: "555-555-5555",
;
}.render("user-template", { user });
res; })
Das Template template.ejs
im
Unterverzeichnis views
, welcher ein JavaScript-Objekt
{vorname, adresse, telefon}
übergeben wird:
<html>
<body>
<h1>User Information</h1>
<table>
<tr>
<td>Name:</td>
<td><%= user.name %></td>
</tr>
<tr>
<td>Email:</td>
<td><%= user.email %></td>
</tr>
<tr>
<td>Phone:</td>
<td><%= user.phone %></td>
</tr>
</table>
</body>
</html>
Server:
const express = require("express");
const app = express();
.set("view engine", "ejs");
app
const DATA = [
id: 1, name: "Apfel", color: "gelb,rot" },
{ id: 2, name: "Birne", color: "gelb,grün" },
{ id: 3, name: "Banane", color: "gelb" },
{ ;
]
.get("/fruits", (req, res) => {
app.render("all", { fruits: DATA }); // all.ejs Template
res;
})
.get("/fruits/:id", (req, res) => {
appconst id = parseInt(req.params.id);
const fruit = DATA.find((o) => o.id === id);
.render("fruit", fruit); // fruit.ejs Template
res;
}).listen(3000);
app
console.log("EJS server running on localhost:3000");
Template:
<html>
<body>
<table>
<tr>
<th>Name</th>
<th>Farbe</th>
</tr>
<% fruits.forEach( o => { %>
<tr>
<td><%= o.name %></td>
<td><%= o.color %></td>
</tr>
<% }) %>
</table>
</body>
</html>
cookie-parser
cookie-parser
ermöglicht zustandsbehaftete
ServerSo können Cookies gesetzt werden:
const cookieParser = require("cookie-parser");
.use(cookieParser());
app
.cookie("userID", "xyz12345"); // Einzelner Cookie
response
response.cookie("userID", "xyz12345")
.cookie("verein", "VfB Stuttgart", { maxAge: 90000 }); // Mehrere Cookies, der zweite mit 90000 milli secs Lebensdauer
So können Cookies ausgelesen werden:
const cookieParser = require("cookie-parser");
.use(cookieParser());
app
const cookies = request.cookies;
let userID = cookies.userID;
let verein = cookies.verein;
express-session
Mit dem npm-Package express-session
kann man
zustandsbehaftete Server bauen:
const express = require("express");
const session = require("express-session");
const app = express();
.use(
appsession({
secret: "mykey", // Für Encoding und Decoding des Cookies
resave: false, // Nur speichern nach Änderung
saveUninitialized: true, // Anfangs immer speichern
cookie: { maxAge: 5000 }, // Ablaufzeit in Millisekunden
});
)
.get("/", function (req, res) {
appif (req.session.count) {
// Eine Session kann beliebige Attribute bekommen
.session.count++;
req.setHeader("Content-Type", "text/html");
res.write("<p>count: " + req.session.count + "</p>");
res.end();
reselse {
} .session.count = 1;
req.end("Willkommen zu der Sitzung. Refresh!");
res
}; })
Zuerst mongodb
installieren:
npm i -s mongodb
Dann mit DB verbinden:
let db = null;
const url = `mongodb://localhost:27017`;
.connect(url, {
MongoClientuseNewUrlParser: true,
useUnifiedTopology: true,
.then((connection) => {
})= connection.db("food");
db console.log("connected to database food ...");
; })
Erstellen einer Collection:
.post("/example-create-collection-fruits", async (req, res) => {
appawait db.createCollection("fruits");
.send("Collection fruits created ...");
res; })
Löschen einer Datenbank:
.post("/example-drop-db-food", async (req, res) => {
appawait db.dropDatabase("food");
.send("Database food dropped!");
res; })
db.dropCollection
für das Löschen einer Collection
Importieren von Dokumenten:
$ mongoimport --db tools-test --collection restaurants --file restaurants.json
Exportieren von Dokumenten:
$ mongoexport --db tools-test --collection restaurants --out new-restaurants.json
Einfügen eines Dokument:
.post("/example-create/fruits", async (req, res) => {
appconst { name, color } = req.body;
const fruit = { name, color };
await db.collection("fruits").insertOne(fruit);
.send(`${name} inserted ...`);
res; })
Auslesen eines Dokuments:
.get("/example-find-one/fruits/:name", async (req, res) => {
appconst { name } = req.params;
const fruit = await db.collection("fruits").findOne({ name });
if (fruit) {
.send(fruit);
reselse {
} .status(400).send("not found ...");
res
}; })
Einfügen mehrerer Dokumente:
.post("/example-insert-many/fruits", async (req, res) => {
appconst fruits = [
name: "Apfel", color: "gelb,rot" },
{ name: "Birne", color: "gelb,grün" },
{ name: "Kiwi", color: "grün" },
{ name: "Banane", color: "gelb" },
{ name: "Pfirsich", color: "gelb,rot" },
{ ;
]
await db.collection("fruits").insertMany(fruits);
.send("Fruits inserted");
res; })
Auslesen aller Dokumente:
.get("/example-list/fruits", async (req, res) => {
appconst fruits = await db.collection("fruits").find().toArray();
.send(fruits);
res; })
Löschen eines Dokuments:
.delete("/example-delete/fruits/:name", async (req, res) => {
appconst { name } = req.params;
const result = await db.collection("fruits").deleteOne({ name });
if (result.deletedCount > 0) {
.send(`${name} deleted ...`);
reselse {
} .status(400).send("fruit not found, nothing to delete ...");
res
}; })
Löschen mehrerer Dokumente:
.collection("fruits").deleteMany({ name: { $regex: name } }); db
Aktualisierung von Dokumenten:
.patch("/example-update-cuisine/:name", async (req, res) => {
appconst { name } = req.params;
const { cuisine } = req.body;
const result = await db.collection("restaurants").updateOne(
,
{ name }
{$set: { cuisine },
$currentDate: { lastModified: true }, // Änderungsdatum
};
)
.send(result);
res; })
Hinzufügen von Arrayelementen in Dokumenten:
.post("/example-push-grade-score/restaurants/:name", async (req, res) => {
appconst { name } = req.params;
const { grade, score } = req.body;
const newGrade = { date: new Date(), grade, score };
const result = await db.collection("restaurants").updateOne(
,
{ name }= {
(update $push: { grades: newGrade },
$currentDate: { lastModified: true },
});
)
.send(result);
res; })
Löschen von Arrayelementen in Dokumenten:
.post("/example-pop-grade-score/restaurants/:name", async (req, res) => {
appconst { name } = req.params;
const query = { name };
const result = await db.collection("restaurants").updateOne(query, {
$pop: { grades: 1 },
$currentDate: { lastModified: true },
;
})
.send(result);
res; })
$pop {<array>: 1}
entfernt das letzte
Element,$pop {<array>: -1}
entfernt das erste Element des
Arrays.Tiefe Queries:
.get("/example-zip/restaurants", async (req, res) => {
appconst { cuisine, zip } = req.query;
const restaurants = await db
.collection("restaurants")
.find({ "address.zipcode": zip, cuisine })
.toArray();
.send(restaurants.map((o) => ({ name: o.name, zip: o.address.zipcode })));
res; })
BSON für Vergleichsoperatoren:
.get("/example-zip-range/restaurants", async (req, res) => {
appconst { zipMin, zipMax, cuisine } = req.query;
const restaurants = await db
.collection("restaurants")
.find({
,
cuisine"address.zipcode": { $gte: zipMin, $lt: zipMax }, // es gibt auch $eq, $in, $neq, $nin ...
}).toArray();
.send(restaurants.map((o) => ({ name: o.name, zip: o.address.zipcode })));
res; })
BSON für Oder-Verknüpfung:
.get("/example-zip-or-cuisine/restaurants", async (req, res) => {
appconst { zip, cuisine } = req.query;
const restaurants = await db
.collection("restaurants")
.find(
$or: [{ "address.zipcode": zip }, { cuisine }] } // es gibt auch noch $and, $not und $nor
{
).toArray();
.send(
res.map((o) => ({
restaurantsname: o.name,
cuisine: o.cuisine,
zip: o.address.zipcode,
}));
); })
result.map
auf
JavaScript-ArraysEin/Auschluss von Attributen:
.get("/example-fields/restaurants", async (req, res) => {
appconst { borough, cuisine } = req.query;
const restaurants = await db
.collection("restaurants")
.find(
, cuisine },
{ boroughprojection: { name: 1, address: 1, _id: 0 } } // 1 bedeuted Einschluss eines Attributs, 0 den Ausschluss
{
).toArray();
.send(restaurants);
res; })
Sortieren:
.get("/example-sort/restaurants", async (req, res) => {
appconst { borough, cuisine } = req.query;
const restaurants = await db
.collection("restaurants")
.find({ borough, cuisine }, { projection: { name: 1, address: 1, _id: 0 } })
.sort({ name: 1 }) // Mit 1 wird aufsteigend sortiert, mit 0 absteigend.
.toArray();
.send(restaurants);
res; })
Aggregation:
.get("/example-avg-score/restaurants", async (req, res) => {
appconst { borough, cuisine } = req.query;
const restaurants = await db
.collection("restaurants")
.aggregate([
{// Wie `WHERE` in SQL
$match: { borough, cuisine },
,
}
{// Wie `SELECT` in SQL
$project: {
name: "$name", // auch name: 1 möglich
avg_score: { $avg: "$grades.score" },
, // auch $min, $max, $sum
},
}$sort: { name: 1 } },
{
]).toArray();
.send(restaurants);
res; })
Gruppierung:
.get("/example-group/restaurants", async (req, res) => {
appconst { borough, cuisine } = req.query;
const restaurants = await db
.collection("restaurants")
.aggregate([
{$match: { borough, cuisine },
,
}
{// Wie `GROUP BY` in SQL
$group: {
_id: "$address.zipcode",
count: { $sum: 1 }, // auch $min, $max, $avg
,
},
}$sort: { _id: 1 } },
{
]).toArray();
.send(restaurants);
res; })
Datenbank-Indizes beschleunigen die Zugriffe für Queries und
Updates, wenn nicht konkret mit der _id
gesucht
wird.
Anlegen eines Index:
await db.collection("restaurants").createIndex({ cuisine: 1 }); // 1: Aufsteigend, -1: Absteigend
Form:
{name: 'cuisine_1'}
Anlegen eines kombinierten Index:
await db
.collection("restaurants")
.createIndex({ cuisine: 1, "address.zipcode": -1 });
Form:
{name: 'cuisine_1_address.zipcode_-1'}
Abfragen aller Indizes:
const indexes = await db.collection("restaurants").getIndexes();
Löschen eines Index:
const result = await db.collection("restaurants").dropIndex("cuisine_1"); // 0: not ok, 1: success
Löschen aller Indizes:
const result = await db.collection("restaurants").dropIndexes();
Vorteile:
Nachteile:
mongodb
in Node.jsIst sehr ähnlich zu MongoDB:
const url = "mongodb://localhost:27017/food_mongoose";
mongoose.connect(url, { useNewUrlParser: true, useUnifiedTopology: true })
.then(() => {
console.log("connected to database food_mongoose ...");
; })
Definition von Schemata:
const mongoose = require("mongoose");
const fruitSchema = new mongoose.Schema({
name: { type: String, required: true },
color: { type: String, required: true },
img: { data: Buffer, contentType: String },
;
})
const Fruit = mongoose.model("Fruit", fruitSchema); // `Fruit` maps to a collection `fruits`
Schema Types aus ES6:
const schemaExample = new mongoose.Schema({
bool: Boolean,
updated: Date,
age: Number,
array: [],
arrayofString: [String],
arrayofArrays: [[]],
arrayofArrayOfNumbers: [[Number]],
map: Map,
mapOfString: { type: Map, of: String },
; })
Weitere Schema Types aus MongoDB:
const schemaExample = new mongoose.Schema({
mixed: mongoose.Mixed, // Kann alles sein (`any`)
_someId: mongoose.ObjectId, // Explizite MongoDB-Id
decimal: mongoose.Decimal128, // 128-bit Floating-Point; `mongoose.Types.Decimal128.fromString('3.1415')`
; })
Indizes in Schemas:
const schemaExample = new mongoose.Schema({
name: {
type: String,
required: true, // Feld zwingend notwendig
index: true, // Index wird automatisch angelegt
,
}; })
Options-Objekt:
const schemaExample = new mongoose.Schema(
{name: String,
age: Number,
,
}
{// Das Options-Objekt
timestamps: true, // Automatisch `createdAt` und `modifiedAt` verwalten
}; )
Verschachtelte Schemata mit Kopien:
const ratingSchema = new mongoose.Schema({
grade: {
type: Number,
min: 1,
max: 6,
,
}comment: String,
date: Date,
;
})
const productSchema = new mongoose.Schema({
name: { type: String, required: true },
price: { type: String, required: true },
ratings: [ratingSchema],
;
})
const Rating = mongoose.model("Rating", ratingSchema);
const Product = mongoose.model("Product", productSchema);
Verschachtelte Schemata mit Referenzen:
const productSchema = new mongoose.Schema({
name: String,
category: {
// 1:1-Beziehung
type: mongoose.ObjectId,
ref: "Category",
,
};
})
const Product = mongoose.model("Product", productSchema);
const categorySchema = new mongoose.Schema({
name: String,
products: [
// 1:n-Beziehung
{type: mongoose.ObjectId,
ref: "Product",
,
},
];
})
const Category = mongoose.model("Category", categorySchema);
Auslesen von Referenzen mit
populate
(wie SQLJOIN
):await Product.find().populate("category")
Auslesen aller Dokumente:
Fruit.find().lean()
liefert nur einfache JS-Objekte für
bessere Performance.get("/example-list/fruits", async (req, res) => {
appconst fruits = await Fruit.find();
.send(fruits);
res; })
Hinzufügen eines Dokuments:
.post("/example-create/fruits", async (req, res) => {
appconst { name, color } = req.body;
const doc = await Fruit.findOne({ name });
if (doc) {
.status(400).send("fruit found, delete first ...");
res
return;
}
const fruit = new Fruit({ name, color });
const imgPath = path.join(IMAGE_PATH, `${name}.png`);
try {
.img.data = await fs.readFile(imgPath);
fruit.img.contentType = "image/png";
fruitcatch (err) {
} console.log(`No image for ${name} found.`);
}
await fruit.save();
.send(`${name} inserted ...`);
res; })
Umwandlung von MongoDB-Queries in Mongoose-Queries:
.get("/example-group-by-color/fruits/:color", async (req, res) => {
appconst { color } = req.params;
const result = await Fruit.aggregate([
{$match: { color: { $regex: color } },
,
}
{$group: {
_id: "$color",
count: { $sum: 1 },
,
},
}$sort: { _id: 1 } },
{ ;
]).send(result);
res; })
Ersetze
db.collection("fruits")
durchFruit
Eingebaute Validatoren:
const breakfastSchema = new Schema({
eggs: {
type: Number,
min: [6, "Too few eggs"],
max: 12,
,
}bacon: {
type: Number,
required: [true, "Why no bacon?"],
,
}drink: {
type: String,
enum: ["Coffee", "Tea"],
required: function () {
return this.bacon > 3;
,
},
}; })
Eigene Validatoren:
function myValidator(val) {
return val === "something";
}
new mongoose.Schema({ name: { type: String, validate: myValidator } });
// Hinzufügen einer Error-Message, {PATH} ist der fehlerhafte Pfad im Schema:
const customValidator = [
,
myValidator'Ups, {PATH} does not equal "something".',
;
]
new mongoose.Schema({ name: { type: String, validate: customValidator } });