Menu
CoddyTech

Nhóm dữ liệu với $group

Thuộc phần Cơ sở dữ liệu với MongoDB trong hành trình Node.js của Coddy. Bài học 21 trên 29.

Đếm một thể loại chỉ cần một truy vấn. Đếm tất cả thể loại cùng lúc cần cơ sở dữ liệu phân loại sách thành các nhóm.

aggregate chạy một đường ống: một mảng gồm các giai đoạn, mỗi giai đoạn chuyển đầu ra của mình cho giai đoạn tiếp theo. Giai đoạn $group tạo một tài liệu đầu ra cho mỗi giá trị riêng biệt của _id. Ở đây, _id không phải là id của tài liệu. Nó cho biết cần nhóm theo giá trị nào, và mỗi tài liệu đầu ra chứa giá trị đó trong _id. Dấu $ trước tên trường, như trong '$team', có nghĩa là “giá trị của trường đó trong tài liệu này”. Nếu không có dấu đó, 'team' là cùng một văn bản trong mọi tài liệu, nên tất cả đều vào cùng một nhóm. $sum: 1 cộng thêm 1 cho mỗi tài liệu.

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

Kết quả của một nhóm chỉ chứa _id đó và các trường do bộ tích lũy tính toán; những trường khác từ tài liệu đầu vào sẽ không được giữ lại.

challenge icon

Thử thách

Dễ

Bước này mở dự án API giá sách đã chuẩn bị sẵn cho khóa học, thay vì các tệp dự án của bạn từ Đếm tài liệu (bao gồm server.js). Phần bạn đã làm trước đó vẫn nằm trong bài học riêng. Đọc các tệp được cung cấp trước khi chỉnh sửa server.js. Bản sao này có các tuyến CRUD cho giá sách, chỉ mục ISBN duy nhất, bộ lọc GET /books và số lượng GET /stats.

Trang thể loại cho biết mỗi thể loại có bao nhiêu cuốn sách, tất cả chỉ từ một truy vấn, bao gồm cả những thể loại mà không ai dự tính trước.

  1. Trong server.js, thêm tuyến GET /stats/genres.
  2. Chạy phép tổng hợp trên books với một giai đoạn $group. Giai đoạn này tạo một kết quả cho mỗi giá trị genre riêng biệt, kèm theo số sách thuộc thể loại đó.
  3. Phản hồi với trạng thái 200 và mảng mà phép tổng hợp trả về. Mỗi mục chứa thể loại trong _id và số sách trong count. Bên trong $group, _id không phải là ObjectId: đó là giá trị dùng để nhóm các cuốn sách. Các mục có thể xuất hiện theo bất kỳ thứ tự nào. Với các cuốn sách ban đầu, phản hồi là:
    [ { "_id": "scifi", "count": 2 }, { "_id": "classic", "count": 1 } ]
    Thử nhé: Gửi GET /stats/genres: bạn nhận được hai mục ở trên. Gửi POST /books với { "title": "Leaves of Grass", "author": "Walt Whitman", "year": 1855, "isbn": "9780140421996", "genre": "poetry" }: bạn nhận được trạng thái 201. Gửi lại GET /stats/genres: một mục thứ ba xuất hiện, { "_id": "poetry", "count": 1 }.

Tự làm thử

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

// Mọi lỗi đều trả về { error: message }.
function sendError(res, status, message) {
    res.status(status).json({ error: message });
}

// Trả về :id dưới dạng ObjectId, hoặc null sau khi trả về 400 nếu id không hợp lệ.
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');
});

Tất cả bài học trong Cơ sở dữ liệu với MongoDB

Tự luyện tập: Playground Express