Paginación en la base de datos
Parte de la sección Bases de datos con MongoDB del Journey de Node.js de Coddy. Lección 27 de 29.
Una estantería con diez mil libros no puede enviarse en una sola respuesta. El cliente solicita una página a la vez, y la base de datos solo debería enviar esa página.
skip(n) omite las primeras n coincidencias y limit(n) conserva las siguientes n. La página 1 empieza desde el principio, así que la página p omite (p - 1) * size. Envía el total junto con la página, contado con el mismo filtro, para que el cliente sepa cuántas páginas hay.
// página 3 del registro de errores, 20 entradas por página
const entries = await log
.find({ level: 'error' })
.skip((3 - 1) * 20)
.limit(20)
.toArray();
const total = await log.countDocuments({ level: 'error' });Desafío
FácilUna estantería con diez mil libros no puede enviarse en una sola respuesta. GET /books ahora envía una página cada vez. También indica cuántos libros coinciden en total, para que el cliente pueda mostrar «página 1 de 5».
- En
server.js, cambiaGET /books. Conserva los filtrosauthor,fromyto. - Lee
?page=(valor predeterminado 1) y?limit=(valor predeterminado 10). Si falta alguno o no es un número, usa su valor predeterminado. La página 1 empieza por el primer libro, así que la páginapomite(p - 1) * limitlibros y después devuelve los siguienteslimitlibros. Deja que la base de datos se encargue de omitir y limitar los resultados. - Cuenta
totalen la base de datos, usando el mismo filtro. Cuenta todos los libros que coinciden, no solo los de esta página. - Responde con el estado 200 y un objeto, no un arreglo.
GET /books?limit=2devuelve:Pruébalo: Envía{ "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 }GET /books?limit=2: obtienes Dune y Kindred, con"total": 3. EnvíaGET /books?limit=2&page=2: obtienes solo Emma, todavía con"total": 3. EnvíaGET /books?limit=2&page=3: obtienes"items": []y"total": 3. EnvíaGET /books?author=Jane%20Austen&limit=1: obtienes Emma, con"total": 1. EnvíaGET /books: obtienes los tres libros enitems.
Pruébalo tú mismo
import express from 'express';
import { ObjectId } from 'mongodb';
import { books, ratings } from './db.js';
const app = express();
app.use(express.json());
// Cada error responde con { error: message }.
function sendError(res, status, message) {
res.status(status).json({ error: message });
}
// Devuelve :id como ObjectId, o null después de responder con 400 si el id no es válido.
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');
});
Todas las lecciones de Bases de datos con MongoDB
1Documentos
Un libro como documentoColecciones y bases de datosConexión a MongoDBLectura de documentos almacenadosInserción de documentosBúsqueda de un documentoBúsqueda por ObjectIdComparación de númerosFiltros de consultaSelección de los campos devueltosOrdenación de los resultados de consultaConservar los primeros resultadosProyección, ordenación y límite4Estructura
Un módulo de base de datosPaginación en la base de datos2Modificación de documentos
Actualización de documentosOperadores de actualizaciónPractica por tu cuenta: Playground de Express