Menu
CoddyTech

קיבוץ עם $group

חלק מהיחידה מסדי נתונים עם MongoDB במסלול ה-Node.js של Coddy. שיעור 21 מתוך 29.

ספירה של ז׳אנר אחד היא שאילתה אחת. כדי לספור את כל הז׳אנרים בבת אחת, מסד הנתונים צריך למיין את הספרים לקבוצות.

aggregate מריצה צינור עיבוד: מערך של שלבים, שכל אחד מהם מעביר את הפלט שלו לשלב הבא. השלב $group יוצר מסמך פלט אחד לכל ערך מובחן של _id. כאן _id אינו מזהה של מסמך. הוא מציין לפיו מה לקבץ, וכל מסמך פלט מכיל את הערך הזה ב-_id. סימן $ לפני שם שדה, כמו '$team', פירושו „הערך של השדה הזה במסמך הנוכחי”. בלעדיו, 'team' הוא אותו טקסט בכל המסמכים, ולכן כולם ישתייכו לאותה קבוצה. $sum: 1 מוסיף 1 עבור כל מסמך.

await players
    .aggregate([{ $group: { _id: '$team', players: { $sum: 1 } } }])
    .toArray();
// [{ _id: 'red', players: 4 }, { _id: 'blue', players: 3 }]

תוצאת קיבוץ מכילה רק את _id ואת השדות שהמצברים שלה מחשבים; שדות אחרים ממסמכי הקלט אינם נשמרים.

challenge icon

אתגר

קל

השלב הזה פותח את פרויקט ה-API המוכן של מדף הספרים בקורס, ולא את קובצי הפרויקט שלך מתוך ספירת מסמכים (כולל server.js). העבודה הקודמת שלך נשארת בשיעור משלה. קרא את הקבצים שסופקו לפני שתערוך את server.js. העותק הזה כולל נתיבי CRUD של מדף הספרים, אינדקס ISBN ייחודי, מסנני GET /books וספירות GET /stats.

דף של ז'אנרים מציג כמה ספרים יש מכל ז'אנר, והכול באמצעות שאילתה אחת, כולל ז'אנרים שאיש לא ציפה להם.

  1. ב-server.js, הוסף נתיב GET /stats/genres.
  2. הרץ צבירה על books עם שלב $group אחד. הוא יוצר תוצאה אחת לכל genre ייחודי, ובה מספר הספרים ששייכים אליו.
  3. החזר תשובה עם סטטוס 200 והמערך שהצבירה מחזירה. בכל איבר נמצאים הז'אנר ב-_id ומספר הספרים ב-count. בתוך $group, _id אינו ObjectId: הוא הערך שלפיו הספרים קובצו. האיברים יכולים להופיע בכל סדר. עם הספרים ההתחלתיים, התשובה היא:
    [ { "_id": "scifi", "count": 2 }, { "_id": "classic", "count": 1 } ]
    נסה זאת: שלח GET /stats/genres: תקבל את שני האיברים שלמעלה. שלח POST /books עם { "title": "Leaves of Grass", "author": "Walt Whitman", "year": 1855, "isbn": "9780140421996", "genre": "poetry" }: תקבל 201. שלח שוב GET /stats/genres: יופיע איבר שלישי, { "_id": "poetry", "count": 1 }.

נסו בעצמכם

import express from 'express';
import { MongoClient, ObjectId } from 'mongodb';

const client = new MongoClient('mongodb://localhost:27017');
const db = client.db('shelf');
const books = db.collection('books');
const ratings = db.collection('ratings');

await books.createIndex({ isbn: 1 }, { unique: true });
await books.insertMany([
    { title: 'Dune', author: 'Frank Herbert', year: 1965, isbn: '9780441013593', genre: 'scifi' },
    { title: 'Kindred', author: 'Octavia E. Butler', year: 1979, isbn: '9780807083697', genre: 'scifi' },
    { title: 'Emma', author: 'Jane Austen', year: 1815, isbn: '9780141439587', genre: 'classic' },
]);

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.listen(3000, () => {
    console.log('Bookshelf is running on port 3000');
});

כל השיעורים ביחידה מסדי נתונים עם MongoDB

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