Menu
CoddyTech

Mengelompokkan dengan $group

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

Menghitung satu genre memerlukan satu query. Untuk menghitung semua genre sekaligus, database perlu mengelompokkan buku.

aggregate menjalankan pipeline: sebuah array berisi tahap, yang masing-masing meneruskan output-nya ke tahap berikutnya. Tahap $group membuat satu dokumen output untuk setiap nilai berbeda dari _id. Di sini, _id bukan ID dokumen. Nilai ini menentukan pengelompokan, dan setiap dokumen output menyimpan nilai tersebut di _id. Tanda $ sebelum nama field, seperti '$team', berarti "nilai field tersebut pada dokumen ini". Tanpa tanda itu, 'team' menjadi teks yang sama untuk setiap dokumen, sehingga semuanya masuk ke dalam satu kelompok. $sum: 1 menambahkan 1 untuk setiap dokumen.

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

Hasil pengelompokan hanya berisi _id dan field yang dihitung oleh akumulatornya. Field lain dari dokumen input tidak disertakan.

challenge icon

Tantangan

Mudah

Langkah ini membuka proyek API rak buku yang sudah disiapkan untuk kursus, bukan file proyekmu dari Counting Documents (termasuk server.js). Pekerjaanmu sebelumnya tetap berada di pelajarannya sendiri. Baca file yang disediakan sebelum mengedit server.js. Salinan ini berisi route CRUD rak buku, indeks ISBN unik, filter GET /books, dan jumlah GET /stats.

Halaman genre menunjukkan jumlah buku untuk setiap genre, semuanya dari satu query, termasuk genre yang tidak direncanakan siapa pun.

  1. Di server.js, tambahkan route GET /stats/genres.
  2. Jalankan agregasi pada books dengan satu tahap $group. Tahap ini menghasilkan satu hasil untuk setiap genre yang berbeda, beserta jumlah buku yang memilikinya.
  3. Berikan respons dengan status 200 dan array yang dikembalikan agregasi. Setiap entri menyimpan genre di _id dan jumlah buku di count. Di dalam $group, _id bukan ObjectId: nilainya adalah nilai yang digunakan untuk mengelompokkan buku. Urutan entri boleh apa saja. Dengan buku-buku awal, responsnya adalah:
    [ { "_id": "scifi", "count": 2 }, { "_id": "classic", "count": 1 } ]
    Coba: Kirim GET /stats/genres: kamu mendapatkan dua entri di atas. Kirim POST /books dengan { "title": "Leaves of Grass", "author": "Walt Whitman", "year": 1855, "isbn": "9780140421996", "genre": "poetry" }: kamu mendapatkan 201. Kirim GET /stats/genres lagi: entri ketiga muncul, { "_id": "poetry", "count": 1 }.

Coba sendiri

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

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

// Ubah :id menjadi ObjectId, atau kembalikan 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.listen(3000, () => {
    console.log('Bookshelf is running on port 3000');
});

Semua pelajaran di Database dengan MongoDB

Latihan sendiri: Playground Express