Phân trang trong cơ sở dữ liệu
Thuộc phần Cơ sở dữ liệu với MongoDB trong hành trình Node.js của Coddy. Bài học 27 trên 29.
Không thể gửi cả kệ sách có mười nghìn cuốn chỉ trong một phản hồi. Máy khách yêu cầu từng trang một, và cơ sở dữ liệu chỉ nên gửi trang đó.
skip(n) bỏ qua n kết quả khớp đầu tiên, còn limit(n) lấy n kết quả tiếp theo. Trang 1 bắt đầu từ đầu, nên trang p bỏ qua (p - 1) * size. Gửi tổng số cùng với trang, được đếm bằng cùng bộ lọc, để máy khách biết có bao nhiêu trang.
// trang 3 của nhật ký lỗi, 20 mục mỗi trang
const entries = await log
.find({ level: 'error' })
.skip((3 - 1) * 20)
.limit(20)
.toArray();
const total = await log.countDocuments({ level: 'error' });Thử thách
DễMột kệ sách có mười nghìn cuốn không thể trả về trong một phản hồi. GET /books giờ trả về từng trang một. API cũng cho biết tổng số sách khớp, để phía client có thể hiển thị "page 1 of 5".
- Trong
server.js, sửaGET /books. Giữ lại các bộ lọcauthor,fromvàto. - Đọc
?page=(mặc định là 1) và?limit=(mặc định là 10). Nếu thiếu một trong hai giá trị hoặc giá trị đó không phải là số, hãy dùng giá trị mặc định tương ứng. Trang 1 bắt đầu từ cuốn sách đầu tiên, vì vậy trangpbỏ qua(p - 1) * limitcuốn sách rồi trả vềlimitcuốn tiếp theo. Để cơ sở dữ liệu xử lý việc bỏ qua và giới hạn số sách trả về. - Đếm
totaltrong cơ sở dữ liệu bằng cùng bộ lọc. Giá trị này đếm tất cả sách khớp, không chỉ những cuốn trên trang hiện tại. - Trả về trạng thái 200 và một đối tượng, không phải một mảng.
GET /books?limit=2trả về:Thử nhé: Gửi{ "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: bạn nhận được Dune và Kindred, cùng"total": 3. GửiGET /books?limit=2&page=2: bạn chỉ nhận được Emma, vẫn có"total": 3. GửiGET /books?limit=2&page=3: bạn nhận được"items": []và"total": 3. GửiGET /books?author=Jane%20Austen&limit=1: bạn nhận được Emma, cùng"total": 1. GửiGET /books: bạn nhận được cả ba cuốn sách trongitems.
Tự làm thử
import express from 'express';
import { ObjectId } from 'mongodb';
import { books, ratings } from './db.js';
const app = express();
app.use(express.json());
// Mọi lỗi đều trả về { error: message }.
function sendError(res, status, message) {
res.status(status).json({ error: message });
}
// Trả về :id dưới dạng ObjectId, hoặc null sau khi trả về mã 400 nếu id không hợp lệ.
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');
});
Tất cả bài học trong Cơ sở dữ liệu với MongoDB
1Tài liệu
Một cuốn sách dưới dạng tài liệuBộ sưu tập và cơ sở dữ liệuKết nối với MongoDBĐọc tài liệu đã lưuChèn tài liệuTìm một tài liệuTìm theo ObjectIdSo sánh các sốBộ lọc truy vấnChọn các trường được trả vềSắp xếp kết quả truy vấnGiữ lại các kết quả đầu tiênPhép chiếu, sắp xếp và giới hạn4Cấu trúc
Một module cơ sở dữ liệuPhân trang trong cơ sở dữ liệuTự luyện tập: Playground Express