קיבוץ עם $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.
דף של ז'אנרים מציג כמה ספרים יש מכל ז'אנר, והכול באמצעות שאילתה אחת, כולל ז'אנרים שאיש לא ציפה להם.
- ב-
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, או null לאחר החזרת 400 עבור id לא תקין.
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 אונליין