Menu
CoddyTech

La pagination dans la base de données

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

Une étagère de dix mille livres ne peut pas être envoyée en une seule réponse. Le client demande une page à la fois, et la base de données ne doit envoyer que cette page.

skip(n) passe les n premiers résultats correspondants, et limit(n) conserve les n suivants. La page 1 commence au début, donc la page p ignore (p - 1) * size. Envoyez le total avec la page, en le comptant avec le même filtre, afin que le client sache combien il y a de pages.

// page 3 du journal des erreurs, 20 entrées par page
const entries = await log
    .find({ level: 'error' })
    .skip((3 - 1) * 20)
    .limit(20)
    .toArray();
const total = await log.countDocuments({ level: 'error' });
challenge icon

Défi

Facile

Une étagère de dix mille livres ne peut pas être renvoyée en une seule réponse. GET /books envoie maintenant une page à la fois. Elle indique également combien de livres correspondent au total, afin qu’un client puisse afficher « page 1 sur 5 ».

  1. Dans server.js, modifie GET /books. Conserve les filtres author, from et to.
  2. Lis ?page= (par défaut 1) et ?limit= (par défaut 10). Si l’un des deux est absent ou n’est pas un nombre, utilise sa valeur par défaut. La page 1 commence au premier livre : la page p ignore donc (p - 1) * limit livres, puis renvoie les limit livres suivants. Laisse la base de données se charger de l’ignorance et de la limitation.
  3. Compte total dans la base de données en utilisant le même filtre. Ce nombre comprend tous les livres correspondants, pas uniquement ceux de cette page.
  4. Renvoie le statut 200 et un objet, pas un tableau. GET /books?limit=2 renvoie :
  5. {
      "items": [
        { "_id": "6720a1f4c2b9e81d3a5f0c11", "title": "Dune", "author": "Frank Herbert", "year": 1965, "isbn": "9780441013593", "genre": "scifi" },
        { "_id": "6720a1f4c2b9e81d3a5f0c12", "title": "Kindred", "author": "Octavia E. Butler", "year": 1979, "isbn": "9780807083697", "genre": "scifi" }
      ],
      "total": 3
    }
    Essaie : Envoie GET /books?limit=2 : tu obtiens Dune et Kindred, avec "total": 3. Envoie GET /books?limit=2&page=2 : tu obtiens uniquement Emma, toujours avec "total": 3. Envoie GET /books?limit=2&page=3 : tu obtiens "items": [] et "total": 3. Envoie GET /books?author=Jane%20Austen&limit=1 : tu obtiens Emma, avec "total": 1. Envoie GET /books : tu obtiens les trois livres dans items.

Essayez vous-même

import express from 'express';
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 si l’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.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');
});

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

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