تقسيم صفحات قاعدة البيانات
جزء من قسم قواعد البيانات باستخدام MongoDB في رحلة Node.js على Coddy. الدرس 27 من 29.
لا يمكن إرسال رفّ يضم عشرة آلاف كتاب في استجابة واحدة. يطلب العميل صفحة واحدة في كل مرة، وينبغي لقاعدة البيانات أن ترسل تلك الصفحة فقط.
تتجاوز skip(n) أول n من النتائج المطابقة، وتُبقي limit(n) النتائج n التالية. تبدأ الصفحة 1 من البداية، لذا تتجاوز الصفحة p مقدار (p - 1) * size. أرسل العدد الإجمالي مع الصفحة، محسوبًا باستخدام عامل التصفية نفسه، كي يعرف العميل عدد الصفحات.
// الصفحة 3 من سجل الأخطاء، 20 إدخالًا في كل صفحة
const entries = await log
.find({ level: 'error' })
.skip((3 - 1) * 20)
.limit(20)
.toArray();
const total = await log.countDocuments({ level: 'error' });التحدي
سهللا يمكن إرسال رفّ يضم عشرة آلاف كتاب في استجابة واحدة. يرسل GET /books الآن صفحة واحدة في كل مرة. ويبيّن أيضًا عدد الكتب المطابقة إجمالًا، حتى يتمكن العميل من عرض «الصفحة 1 من 5».
- في
server.js، غيّرGET /books. أبقِ عوامل التصفيةauthorوfromوto. - اقرأ
?page=(الافتراضي 1) و?limit=(الافتراضي 10). إذا كان أحدهما مفقودًا أو لم يكن رقمًا، فاستخدم قيمته الافتراضية. تبدأ الصفحة 1 من أول كتاب، لذا تتخطى الصفحةpعدد(p - 1) * limitمن الكتب، ثم تُرجع الكتبlimitالتالية. دع قاعدة البيانات تتولى التخطي والاقتطاع. - احسب
totalفي قاعدة البيانات، باستخدام عامل التصفية نفسه. فهو يحسب كل كتاب مطابق، وليس الكتب الموجودة في هذه الصفحة فقط. - أعِد استجابة برمز الحالة 200 وكائن، وليس مصفوفة. يُرجع
GET /books?limit=2ما يلي:جرّب ذلك: أرسل{ "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: ستحصل على Dune وKindred، مع"total": 3. أرسلGET /books?limit=2&page=2: ستحصل على Emma فقط، مع بقاء"total": 3. أرسلGET /books?limit=2&page=3: ستحصل على"items": []و"total": 3. أرسلGET /books?author=Jane%20Austen&limit=1: ستحصل على Emma، مع"total": 1. أرسلGET /books: ستحصل على الكتب الثلاثة كلها فيitems.
جرّب بنفسك
import express from 'express';
import { ObjectId } from 'mongodb';
import { books, ratings } from './db.js';
const app = express();
app.use(express.json());
// تُرجع كل حالة خطأ { error: message }.
function sendError(res, status, message) {
res.status(status).json({ error: message });
}
// يُرجع :id على هيئة ObjectId، أو null بعد إرسال استجابة 400 إذا كان المعرّف غير صالح.
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');
});
جميع دروس قواعد البيانات باستخدام MongoDB
تدرّب بنفسك: Playground لـ Express