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 } } }
);Défi
FacileLes 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.
- Dans
server.js, ajoutezPOST /books/:id/reviews. Son corps ressemble à{ "user": "ada", "text": "Loved it" }. UtilisezparseIden cas d’identifiant invalide, ce qui renvoie 400. - En une seule mise à jour, ajoutez l’avis, par exemple
{ "user": "ada", "text": "Loved it" }, à la fin du tableaureviewsdu 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. - Si aucun livre ne possède cet
_id, renvoyez 404. - Sinon, renvoyez le statut 201 et uniquement le tableau
reviewsdu livre, et non le livre entier, avec l’avis le plus ancien en premier. Après le premier avis, la réponse est :Essayez : Envoyez[ { "user": "ada", "text": "Loved it" } ]GET /bookset copiez le_idde Dune. EnvoyezPOST /books/<Dune's _id>/reviewsavec{ "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, deReview 2àReview 6, etLoved ita disparu. EnvoyezGET /books/<Dune's _id>: Dune contient les mêmes cinq avis dansreviews, 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');
});
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
1Documents
Un livre comme documentCollections et bases de donnéesSe connecter à MongoDBLire des documents stockésInsérer des documentsTrouver un documentRechercher par ObjectIdComparer des nombresFiltres de requêteChoisir les champs renvoyésTrier les résultats de requêteConserver les premiers résultatsProjection, tri et limite4Structure
Un module de base de donnéesLa pagination dans la base de donnéesEntraînez-vous par vous-même : Playground Express