التجميع باستخدام $group
جزء من قسم قواعد البيانات باستخدام MongoDB في رحلة Node.js على Coddy. الدرس 21 من 29.
يُعدّ حساب نوع أدبي واحد استعلامًا واحدًا. أما حساب جميع الأنواع الأدبية دفعةً واحدة فيتطلب من قاعدة البيانات فرز الكتب إلى مجموعات.
تنفّذ aggregate مسارًا: مصفوفة من المراحل، تمرّر كلٌّ منها ناتجها إلى المرحلة التالية. تنشئ مرحلة $group مستند ناتج واحدًا لكل قيمة مميزة من _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 وحده، وعلى الحقول التي تحسبها المُجمِّعات؛ أما الحقول الأخرى من مستندات الإدخال فلا تبقى.
التحدي
سهلتفتح هذه الخطوة مشروع واجهة API لرف الكتب المُعَدّ للدورة، بدلًا من ملفات مشروعك من درس عدّ المستندات (بما فيها server.js). يبقى عملك السابق في درسه الخاص. اقرأ الملفات المقدَّمة قبل تعديل server.js. تحتوي هذه النسخة على مسارات CRUD لرف الكتب، وفهرس ISBN فريد، ومرشّحات GET /books وإحصاءات GET /stats.
تعرض صفحة الأنواع عدد الكتب في كل نوع، وكل ذلك عبر استعلام واحد، بما يشمل الأنواع التي لم يتوقعها أحد.
- أضف مسارًا لـ
GET /stats/genresفيserver.js. - نفّذ تجميعًا على
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، أو null بعد إرجاع 400 إذا كان المعرّف غير صالح.
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
3التجميع
عدّ المستنداتمسار من المراحلالتجميع باستخدام $groupالمتوسطات داخل المجموعاتتصفية نتائج المجموعاتمسارات التجميعالربط باستخدام $lookupتدرّب بنفسك: Playground لـ Express