Menu
CoddyTech

Upserts

Teil des Abschnitts Datenbanken mit MongoDB der Node.js-Journey von Coddy. Lektion 18 von 29.

Jeder Leser bewertet ein Buch einmal. Bei einer erneuten Bewertung sollten sich die Sterne ändern, statt eine zweite Bewertung hinzuzufügen.

Mit { upsert: true } aktualisiert updateOne das Dokument, auf das der Filter zutrifft, und fügt, wenn kein Dokument zutrifft, eines ein, das aus den Feldern des Filters und der Aktualisierung erstellt wird. Ein Aufruf deckt den ersten und alle weiteren Fälle ab.

// ein Einstellungsdokument pro Benutzer
await settings.updateOne(
    { user: 'ada' },
    { $set: { theme: 'dark' } },
    { upsert: true }
);

Der Filter legt fest, was ein Dokument eindeutig macht; hier ist es ein Dokument pro Benutzer. upsertedId des Ergebnisses wird gesetzt, wenn der Aufruf ein Dokument eingefügt hat. Ein einfaches insertOne kann das nicht: Es fügt jedes Mal ein Dokument hinzu.

challenge icon

Aufgabe

Einfach

Leser bewerten Bücher mit 1 bis 5 Sternen. Jeder Leser bewertet ein Buch nur einmal: Bei einer erneuten Bewertung werden die Sterne geändert, statt eine zweite Bewertung hinzuzufügen.

  1. Öffne in server.js neben const books = db.collection('books'); eine zweite Collection namens ratings. Jede Bewertung ist ein Dokument:
    { "bookId": "6720a1f4c2b9e81d3a5f0c11", "user": "ada", "stars": 4 }
    Speichere bookId als die ObjectId, die parseId zurückgibt, nicht als den Text aus der URL. In JSON sehen beide gleich aus.
  2. Füge PUT /books/:id/ratings hinzu. Der Body sieht so aus: { "user": "ada", "stars": 4 }. Wenn dieser Nutzer dieses Buch bereits bewertet hat, ändere seine Sterne. Falls nicht, füge eine neue Bewertung hinzu. Erledige beides mit einem einzigen Aufruf. Antworte mit Status 200 und der Bewertung: { "user": "ada", "stars": 4 }.
  3. Füge GET /books/:id/ratings hinzu. Die Route antwortet mit Status 200 und einem Array der Bewertungen dieses Buchs, so wie sie gespeichert sind. Bewertungen anderer Bücher dürfen nicht enthalten sein:
    [ { "_id": "6720a1f4c2b9e81d3a5f0c21", "bookId": "6720a1f4c2b9e81d3a5f0c11", "user": "ada", "stars": 2 } ]
  4. Beide Routen verwenden bei einer ungültigen ID parseId, das den Status 400 zurückgibt. Probiere es aus: Sende GET /books und kopiere Dunes _id. Sende PUT /books/<Dune's _id>/ratings mit { "user": "ada", "stars": 4 }: Du erhältst 200 und { "user": "ada", "stars": 4 }. Sende es erneut mit { "user": "ada", "stars": 2 }: Du erhältst 200 und { "user": "ada", "stars": 2 }. Sende GET /books/<Dune's _id>/ratings: Du erhältst eine Bewertung, die von Ada, mit 2 Sternen. Bewerte Dune mit { "user": "bo", "stars": 5 } und rufe die Liste erneut ab: Du erhältst zwei Bewertungen. Im Tab „Data“ wird jetzt eine Collection ratings angezeigt. GET /books/<Kindred's _id>/ratings gibt [] zurück.

Probier es selbst

import express from 'express';
import { MongoClient, ObjectId } from 'mongodb';

const client = new MongoClient('mongodb://localhost:27017');
const db = client.db('shelf');
const books = db.collection('books');

await books.createIndex({ isbn: 1 }, { unique: true });
await books.insertMany([
    { title: 'Dune', author: 'Frank Herbert', year: 1965, isbn: '9780441013593', genre: 'scifi' },
    { title: 'Kindred', author: 'Octavia E. Butler', year: 1979, isbn: '9780807083697', genre: 'scifi' },
    { title: 'Emma', author: 'Jane Austen', year: 1815, isbn: '9780141439587', genre: 'classic' },
]);

const app = express();
app.use(express.json());

// Bei jedem Fehler wird { error: message } zurückgegeben.
function sendError(res, status, message) {
    res.status(status).json({ error: message });
}

// Die :id als ObjectId oder null, nachdem bei einer ungültigen id mit 400 geantwortet wurde.
function parseId(req, res) {
    if (ObjectId.isValid(req.params.id)) {
        return new ObjectId(req.params.id);
    }
    sendError(res, 400, 'Not a valid id');
    return null;
}

app.get('/', (req, res) => {
    res.send('Bookshelf API');
});

app.get('/books', async (req, res) => {
    const filter = {};
    if (req.query.author) {
        filter.author = req.query.author;
    }
    if (req.query.from || req.query.to) {
        filter.year = {};
        if (req.query.from) {
            filter.year.$gte = Number(req.query.from);
        }
        if (req.query.to) {
            filter.year.$lte = Number(req.query.to);
        }
    }
    const list = await books.find(filter).toArray();
    res.json(list);
});

app.post('/books', async (req, res) => {
    const book = req.body;
    try {
        await books.insertOne(book);
    } catch (err) {
        if (err.code === 11000) {
            return sendError(res, 409, 'That ISBN is already on the shelf');
        }
        throw err;
    }
    res.status(201).json(book);
});

app.get('/books/newest', async (req, res) => {
    const n = Number(req.query.n) || 3;
    const list = await books
        .find({}, { projection: { title: 1, year: 1, _id: 0 } })
        .sort({ year: -1 })
        .limit(n)
        .toArray();
    res.json(list);
});

app.get('/books/:id', async (req, res) => {
    const id = parseId(req, res);
    if (!id) return;
    const book = await books.findOne({ _id: id });
    if (!book) {
        return sendError(res, 404, 'No such book');
    }
    res.json(book);
});

app.patch('/books/:id', async (req, res) => {
    const id = parseId(req, res);
    if (!id) return;
    const result = await books.updateOne({ _id: id }, { $set: req.body });
    if (result.matchedCount === 0) {
        return sendError(res, 404, 'No such book');
    }
    res.json(await books.findOne({ _id: id }));
});

app.post('/books/:id/copies', async (req, res) => {
    const id = parseId(req, res);
    if (!id) return;
    const result = await books.updateOne({ _id: id }, { $inc: { copies: 1 } });
    if (result.matchedCount === 0) {
        return sendError(res, 404, 'No such book');
    }
    res.json(await books.findOne({ _id: id }));
});

app.post('/books/:id/tags', async (req, res) => {
    const id = parseId(req, res);
    if (!id) return;
    const result = await books.updateOne({ _id: id }, { $push: { tags: req.body.tag } });
    if (result.matchedCount === 0) {
        return sendError(res, 404, 'No such book');
    }
    res.json(await books.findOne({ _id: id }));
});

app.delete('/books/:id', async (req, res) => {
    const id = parseId(req, res);
    if (!id) return;
    const result = await books.deleteOne({ _id: id });
    if (result.deletedCount === 0) {
        return sendError(res, 404, 'No such book');
    }
    res.status(204).end();
});

app.listen(3000, () => {
    console.log('Bookshelf is running on port 3000');
});

Alle Lektionen in Datenbanken mit MongoDB

Übe selbstständig: Express-Playground