Menu
CoddyTech

Intégrer ou référencer

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

Les avis et les évaluations sont tous deux associés à un livre, mais le meilleur endroit où stocker chacun est différent.

L’intégration place les données associées dans le document, de sorte qu’une seule lecture récupère le livre et ses avis. Elle convient aux données lues avec leur document parent et dont la taille reste limitée. La référence les conserve dans leur propre collection avec l’identifiant du document parent, comme le fait ratings. Elle convient aux données qui augmentent sans limite ou qui sont interrogées seules, comme pour calculer une moyenne sur toutes les évaluations.

Un document peut contenir au maximum 16 MB : un tableau intégré doit donc être limité. Dans $push, $each énumère les valeurs à ajouter et $slice: -3 conserve les trois dernières.

await players.updateOne(
    { name: 'Ada' },
    { $push: { recentScores: { $each: [120], $slice: -3 } } }
);
challenge icon

Défi

Facile

Les avis sont courts, toujours affichés avec leur livre, et un livre n’affiche que ses 5 plus récents. Gardez-les donc dans le document du livre, dans un tableau reviews, et ne laissez jamais ce tableau dépasser 5 éléments.

  1. Dans server.js, ajoutez POST /books/:id/reviews. Son corps ressemble à { "user": "ada", "text": "Loved it" }. Utilisez parseId en cas d’identifiant invalide, ce qui renvoie 400.
  2. En une seule mise à jour, ajoutez l’avis, par exemple { "user": "ada", "text": "Loved it" }, à la fin du tableau reviews du livre. Dans la même mise à jour, réduisez le tableau à ses 5 derniers éléments afin que l’avis le plus ancien soit supprimé. Un livre sans avis reçoit un nouveau tableau.
  3. Si aucun livre ne possède cet _id, renvoyez 404.
  4. Sinon, renvoyez le statut 201 et uniquement le tableau reviews du livre, et non le livre entier, avec l’avis le plus ancien en premier. Après le premier avis, la réponse est :
    [ { "user": "ada", "text": "Loved it" } ]
    Essayez : Envoyez GET /books et copiez le _id de Dune. Envoyez POST /books/<Dune's _id>/reviews avec { "user": "ada", "text": "Loved it" } : vous obtenez 201 et le tableau ci-dessus. Envoyez cinq autres avis, avec les textes "Review 2" à "Review 6". La dernière réponse contient cinq avis, de Review 2 à Review 6, et Loved it a disparu. Envoyez GET /books/<Dune's _id> : Dune contient les mêmes cinq avis dans reviews, et l’onglet Data les affiche dans le livre.

Essayez vous-même

import express from 'express';
import { z } from 'zod';
import { ObjectId } from 'mongodb';
import { books, ratings } from './db.js';

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

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

// Le :id sous forme d’ObjectId, ou null après avoir renvoyé 400 pour un id 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;
}

const BookInput = z.object({
    title: z.string().min(1),
    author: z.string().min(1),
    year: z.number().int(),
    isbn: z.string().min(1),
    genre: z.string().min(1),
});

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 page = Number(req.query.page) || 1;
    const limit = Number(req.query.limit) || 10;
    const items = await books
        .find(filter)
        .skip((page - 1) * limit)
        .limit(limit)
        .toArray();
    const total = await books.countDocuments(filter);
    res.json({ items, total });
});

app.post('/books', async (req, res) => {
    const parsed = BookInput.safeParse(req.body);
    if (!parsed.success) {
        const issue = parsed.error.issues[0];
        return sendError(res, 400, `${issue.path.join('.')}: ${issue.message}`);
    }
    const book = parsed.data;
    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.put('/books/:id/ratings', async (req, res) => {
    const id = parseId(req, res);
    if (!id) return;
    const { user, stars } = req.body;
    await ratings.updateOne({ bookId: id, user }, { $set: { stars } }, { upsert: true });
    res.json({ user, stars });
});

app.get('/books/:id/ratings', async (req, res) => {
    const id = parseId(req, res);
    if (!id) return;
    res.json(await ratings.find({ bookId: id }).toArray());
});

app.get('/stats', async (req, res) => {
    const filter = req.query.genre ? { genre: req.query.genre } : {};
    res.json({
        books: await books.countDocuments(filter),
        ratings: await ratings.countDocuments(),
    });
});

app.get('/stats/genres', async (req, res) => {
    const genres = await books
        .aggregate([{ $group: { _id: '$genre', count: { $sum: 1 } } }])
        .toArray();
    res.json(genres);
});

app.get('/stats/top', async (req, res) => {
    const top = await ratings
        .aggregate([
            { $group: { _id: '$bookId', avg: { $avg: '$stars' }, count: { $sum: 1 } } },
            { $match: { count: { $gte: 2 } } },
            { $sort: { avg: -1 } },
            { $limit: 3 },
        ])
        .toArray();
    res.json(top);
});

app.get('/books/:id/full', async (req, res) => {
    const id = parseId(req, res);
    if (!id) return;
    const [book] = await books
        .aggregate([
            { $match: { _id: id } },
            {
                $lookup: {
                    from: 'authors',
                    localField: 'author',
                    foreignField: 'name',
                    as: 'authorInfo',
                },
            },
        ])
        .toArray();
    if (!book) {
        return sendError(res, 404, 'No such book');
    }
    res.json(book);
});

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

Cette leçon comprend un petit quiz. Commencez la leçon pour y répondre et suivre votre progression.

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

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