Menu
CoddyTech

צירוף עם $lookup

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

ספר שומר את שם המחבר שלו, ופרטי המחבר נמצאים באוסף אחר. הלקוח רוצה לקבל את שניהם בתשובה אחת.

שלב $lookup מבצע צירוף. עבור כל מסמך, הוא מוצא באוסף from את המסמכים שבהם foreignField שווה ל-localField של המסמך הזה, ומכניס אותם לשדה מערך חדש ששמו נקבע על ידי as.

await orders.aggregate([
    { $match: { _id: orderId } },
    { $lookup: {
        from: 'customers',
        localField: 'customerEmail',
        foreignField: 'email',
        as: 'customer',
    } },
]).toArray();
// [{ _id: ..., customerEmail: 'ada@x.io', customer: [{ email: 'ada@x.io', ... }] }]

toArray() מחזירה מערך של הזמנות, ובמקרה הזה יש בו הזמנה אחת. קח את הפריט הראשון כדי לקבל את ההזמנה עצמה.

localField שייך למסמכים בצינור, ו-foreignField שייך לאוסף האחר. התוצאה היא מערך, גם כשיש התאמה אחת בלבד.

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

challenge icon

אתגר

קל

השלב הזה פותח את פרויקט ה־API של מדף הספרים שהוכן לקורס, ולא את קובצי הפרויקט שלך מ־Aggregation Pipelines (כולל server.js). העבודה הקודמת שלך נשארת בשיעור שלה. קרא את הקבצים שסופקו לפני עריכת server.js. העותק הזה כולל את הנתיבים של מדף הספרים, ספירות הז׳אנרים ואת צינור העיבוד של הספרים בעלי הדירוג הגבוה ביותר, וכן נתוני מחברים שהוזנו מראש וטיפול בתגובות.

בנתיב המוכן GET /books/:id/full שב־server.js, הוסף שלב lookup אחרי שלב match הקיים. צרף מחברים כאשר book.author שווה ל־author.name. שמור את ההתאמות ב־authorInfo. השאר את הקוד שסופק, שלוקח את התוצאה הראשונה ומחזיר 404 כשהספר חסר.

נסה זאת: GET /books והעתק את _id של Emma. הבקשה GET /books/<Emma’s _id>/full מחזירה את Emma עם מערך authorInfo שמכיל את Jane Austen ואת שנת הלידה 1775. ספר עם מחבר לא ידוע מחזיר authorInfo: []. גם מזהה תקין של ספר חסר עדיין מקבל 404.

נסו בעצמכם

import express from 'express';
import { MongoClient, ObjectId } from 'mongodb';
import { authorList } from './authors.js';

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

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' },
]);
await authors.insertMany(authorList);

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 } },

        ])
        .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 אונליין