Menu
CoddyTech

Menggabungkan dengan $lookup

Termasuk bagian Database dengan MongoDB dalam perjalanan Node.js di Coddy. Pelajaran 25 dari 29.

Sebuah buku menyimpan nama penulisnya, dan detail penulis tersimpan di koleksi lain. Klien menginginkan keduanya dalam satu jawaban.

Tahap $lookup menggabungkan data. Untuk setiap dokumen, tahap ini mencari dokumen di koleksi from yang nilai foreignField-nya sama dengan nilai localField dokumen ini, lalu menyimpannya dalam field array baru yang namanya ditentukan oleh 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() menghasilkan array pesanan, yang dalam contoh ini berisi satu pesanan. Ambil item pertamanya untuk mendapatkan pesanan itu sendiri.

localField merujuk pada dokumen dalam pipeline, sedangkan foreignField merujuk pada koleksi lain. Hasilnya berupa array, bahkan jika hanya ada satu kecocokan.

Koleksi from menyediakan setiap dokumen yang cocok dalam array yang namanya ditentukan oleh as. Rute yang disediakan mengambil hasil pertama dari pipeline untuk mengembalikan buku yang diminta.

challenge icon

Tantangan

Mudah

Langkah ini membuka proyek API rak buku yang sudah disiapkan untuk kursus, bukan file proyekmu dari Aggregation Pipelines (termasuk server.js). Pekerjaanmu sebelumnya tetap ada di pelajarannya sendiri. Baca file yang disediakan sebelum mengedit server.js. Salinan ini berisi rute rak buku, pipeline penghitungan genre dan buku dengan rating tertinggi, serta data penulis awal dan penanganan respons.

Di rute GET /books/:id/full yang sudah disiapkan di server.js, tambahkan tahap lookup setelah match yang sudah ada. Gabungkan penulis dengan mencocokkan book.author dengan author.name. Simpan hasil yang cocok di authorInfo. Pertahankan kode yang disediakan untuk mengambil hasil pertama dan mengembalikan 404 saat buku tidak ditemukan.

Coba: GET /books dan salin _id milik Emma. GET /books/<Emma’s _id>/full mengembalikan Emma dengan array authorInfo yang berisi Jane Austen dan born 1775. Buku dengan penulis yang tidak dikenal mengembalikan authorInfo: []. ID buku yang valid tetapi tidak ditemukan tetap mendapat 404.

Coba sendiri

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());

// Setiap error mengembalikan { error: message }.
function sendError(res, status, message) {
    res.status(status).json({ error: message });
}

// Mengembalikan :id sebagai ObjectId, atau null setelah mengirim respons 400 jika id tidak valid.
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');
});

Semua pelajaran di Database dengan MongoDB

Latihan sendiri: Playground Express