Menu
CoddyTech

הטמעה או הפניה

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

ביקורות ודירוגים שייכים שניהם לספר, והמקום הטוב ביותר לאחסן כל אחד מהם שונה.

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

מסמך יכול להכיל לכל היותר 16 MB, לכן למערך מוטמע נדרשת הגבלה. בתוך $push, ‏$each מפרט את הערכים שיש להוסיף, ו-$slice: -3 משאיר את שלושת האחרונים.

await players.updateOne(
    { name: 'Ada' },
    { $push: { recentScores: { $each: [120], $slice: -3 } } }
);
challenge icon

אתגר

קל
  • POST /books/:id/reviews עם { "user": "ada", "text": "Loved it" } מטמיע את הביקורת במערך reviews של הספר.
  • ספר שומר רק את 5 הביקורות האחרונות שלו.
  • הוא משיב 201 עם הביקורות של הספר, או 404 עבור מזהה לא מוכר.

שלחו שש ביקורות, ואז פתחו את הספר בכרטיסייה Data.

נסו בעצמכם

import express from 'express';
import { z } from 'zod';
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;
}

const BookInput = z.object({
    title: z.string().min(1),
    author: z.string().min(1),
    year: z.number().int(),
    isbn: z.string().min(1),
    genre: z.string().min(1),
});

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 page = Number(req.query.page) || 1;
    const limit = Number(req.query.limit) || 10;
    const items = await books
        .find(filter)
        .skip((page - 1) * limit)
        .limit(limit)
        .toArray();
    const total = await books.countDocuments(filter);
    res.json({ items, total });
});

app.post('/books', async (req, res) => {
    const parsed = BookInput.safeParse(req.body);
    if (!parsed.success) {
        const issue = parsed.error.issues[0];
        return sendError(res, 400, `${issue.path.join('.')}: ${issue.message}`);
    }
    const book = parsed.data;
    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');
});
quiz iconבחנו את עצמכם

השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.

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