Menu
CoddyTech

تقسيم صفحات قاعدة البيانات

جزء من قسم قواعد البيانات باستخدام 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' });
challenge icon

التحدي

سهل

لا يمكن إرسال رفّ يضم عشرة آلاف كتاب في استجابة واحدة. يرسل GET /books الآن صفحة واحدة في كل مرة. ويبيّن أيضًا عدد الكتب المطابقة إجمالًا، حتى يتمكن العميل من عرض «الصفحة 1 من 5».

  1. في server.js، غيّر GET /books. أبقِ عوامل التصفية author وfrom وto.
  2. اقرأ ?page= (الافتراضي 1) و?limit= (الافتراضي 10). إذا كان أحدهما مفقودًا أو لم يكن رقمًا، فاستخدم قيمته الافتراضية. تبدأ الصفحة 1 من أول كتاب، لذا تتخطى الصفحة p عدد (p - 1) * limit من الكتب، ثم تُرجع الكتب limit التالية. دع قاعدة البيانات تتولى التخطي والاقتطاع.
  3. احسب total في قاعدة البيانات، باستخدام عامل التصفية نفسه. فهو يحسب كل كتاب مطابق، وليس الكتب الموجودة في هذه الصفحة فقط.
  4. أعِد استجابة برمز الحالة 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