$group으로 그룹화하기
Coddy Node.js 여정의 MongoDB 데이터베이스 섹션에 포함된 레슨. 29개 중 21번째.
장르 하나를 세는 것은 쿼리 하나입니다. 모든 장르를 한 번에 세려면 데이터베이스가 책을 그룹으로 정렬해야 합니다.
aggregate는 파이프라인을 실행합니다. 파이프라인은 단계의 배열이며, 각 단계는 출력을 다음 단계로 전달합니다. $group 단계는 _id의 고유한 값마다 출력 문서 하나를 만듭니다. 여기서 _id는 문서 ID가 아닙니다. 무엇을 기준으로 그룹화할지를 지정하며, 각 출력 문서에는 해당 값이 _id에 담깁니다. 필드 이름 앞의 $, 즉 '$team'은 “이 문서에서 해당 필드의 값”을 의미합니다. 이를 생략하면 'team'은 모든 문서에서 같은 텍스트이므로, 모든 문서가 하나의 그룹에 들어갑니다. $sum: 1은 문서마다 1을 더합니다.
await players
.aggregate([{ $group: { _id: '$team', players: { $sum: 1 } } }])
.toArray();
// [{ _id: 'red', players: 4 }, { _id: 'blue', players: 3 }]그룹 결과에는 _id와 누산기가 계산하는 필드만 포함되며, 입력 문서의 다른 필드는 유지되지 않습니다.
챌린지
쉬움이 단계에서는 Counting Documents에서 사용한 프로젝트 파일(여기에는 server.js도 포함)이 아니라, 강의에서 준비한 책장 API 프로젝트를 엽니다. 이전 작업은 해당 강의에 그대로 남아 있습니다. server.js를 수정하기 전에 제공된 파일을 읽어 보세요. 이 프로젝트 사본에는 책장 CRUD 경로, 고유 ISBN 인덱스, GET /books 필터 및 GET /stats 집계가 들어 있습니다.
장르 페이지는 한 번의 쿼리만으로 각 장르에 몇 권의 책이 있는지 보여 줍니다. 아무도 예상하지 못한 장르도 포함됩니다.
server.js에GET /stats/genres경로를 추가합니다.books에 단일$group단계를 사용해 집계를 실행합니다. 이 단계는 고유한genre마다 하나의 결과를 만들고, 해당 장르에 속한 책의 수를 담습니다.- 상태 코드 200과 집계에서 반환된 배열을 응답으로 보냅니다. 각 항목의
_id에는 장르가,count에는 책의 수가 들어갑니다.$group안에서_id는 ObjectId가 아니라 책을 그룹화하는 데 사용된 값입니다. 항목은 어떤 순서로든 반환될 수 있습니다. 시작 코드에 포함된 책을 기준으로 응답은 다음과 같습니다:직접 해보기:[ { "_id": "scifi", "count": 2 }, { "_id": "classic", "count": 1 } ]GET /stats/genres를 보내면 위의 두 항목이 반환됩니다.POST /books와 함께{ "title": "Leaves of Grass", "author": "Walt Whitman", "year": 1855, "isbn": "9780140421996", "genre": "poetry" }를 보내면 201이 반환됩니다. 다시GET /stats/genres를 보내면 세 번째 항목인{ "_id": "poetry", "count": 1 }이 나타납니다.
직접 해보기
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());
// 모든 오류는 { error: message } 형식으로 응답합니다.
function sendError(res, status, message) {
res.status(status).json({ error: message });
}
// :id를 ObjectId로 반환하거나, 잘못된 id에 대해 400으로 응답한 뒤 null을 반환합니다.
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');
});
MongoDB 데이터베이스의 모든 레슨
직접 연습해 보세요: Express 플레이그라운드