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 עבור id לא תקין.
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

תרגלו בעצמכם: Express אונליין