Menu
CoddyTech

Upserts

Fait partie de la section Bases de données avec MongoDB du Journey Node.js de Coddy. Leçon 18 sur 29.

Chaque lecteur note un livre une seule fois. Le noter à nouveau doit modifier les étoiles, et non ajouter une deuxième note.

Avec { upsert: true }, updateOne met à jour le document correspondant au filtre et, lorsqu’aucun document ne correspond, en insère un à partir des champs du filtre et de la mise à jour. Un seul appel couvre la première fois et toutes les fois suivantes.

// un document de paramètres par utilisateur
await settings.updateOne(
    { user: 'ada' },
    { $set: { theme: 'dark' } },
    { upsert: true }
);

Le filtre indique ce qui rend un document unique ; ici, un document par utilisateur. Le champ upsertedId du résultat est défini lorsque l’appel a inséré un document. Un simple insertOne ne peut pas faire cela : il ajoute un document à chaque fois.

challenge icon

Défi

Facile

Les lecteurs attribuent aux livres de 1 à 5 étoiles. Chaque lecteur évalue un livre une seule fois : s’il l’évalue à nouveau, cela modifie le nombre d’étoiles au lieu d’ajouter une deuxième évaluation.

  1. Dans server.js, à côté de const books = db.collection('books');, ouvrez une deuxième collection nommée ratings. Chaque évaluation est un document :
    { "bookId": "6720a1f4c2b9e81d3a5f0c11", "user": "ada", "stars": 4 }
    Enregistrez bookId comme l’ObjectId renvoyé par parseId, et non comme le texte de l’URL. En JSON, les deux se ressemblent.
  2. Ajoutez PUT /books/:id/ratings. Son corps ressemble à { "user": "ada", "stars": 4 }. Si cet utilisateur a déjà évalué ce livre, modifiez son nombre d’étoiles. Sinon, ajoutez une nouvelle évaluation. Faites les deux avec un seul appel. Répondez avec le statut 200 et l’évaluation : { "user": "ada", "stars": 4 }.
  3. Ajoutez GET /books/:id/ratings. La route répond avec le statut 200 et un tableau contenant les évaluations de ce livre telles qu’elles sont enregistrées. Les évaluations des autres livres ne doivent pas apparaître :
    [ { "_id": "6720a1f4c2b9e81d3a5f0c21", "bookId": "6720a1f4c2b9e81d3a5f0c11", "user": "ada", "stars": 2 } ]
  4. Les deux routes utilisent parseId en cas d’identifiant invalide, ce qui renvoie le statut 400. Essayez : envoyez GET /books et copiez le _id de Dune. Envoyez PUT /books/<Dune's _id>/ratings avec { "user": "ada", "stars": 4 } : vous obtenez le statut 200 et { "user": "ada", "stars": 4 }. Envoyez-le à nouveau avec { "user": "ada", "stars": 2 } : vous obtenez le statut 200 et { "user": "ada", "stars": 2 }. Envoyez GET /books/<Dune's _id>/ratings : vous obtenez une seule évaluation, celle d’Ada, avec 2 étoiles. Évaluez Dune avec { "user": "bo", "stars": 5 } et affichez la liste à nouveau : vous obtenez deux évaluations. L’onglet Data affiche maintenant une collection ratings. GET /books/<Kindred's _id>/ratings renvoie [].

Essayez vous-même

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());

// Chaque erreur renvoie { error: message }.
function sendError(res, status, message) {
    res.status(status).json({ error: message });
}

// Renvoie :id sous forme d’ObjectId, ou null après avoir renvoyé 400 si id est invalide.
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');
});

Toutes les leçons de Bases de données avec MongoDB

Entraînez-vous par vous-même : Playground Express