Menu
CoddyTech

$group ile Gruplama

Coddy'nin Node.js Journey'sinin MongoDB ile Veritabanları bölümünün bir parçası. Ders 21 / 29.

Tek bir türü saymak tek bir sorgudur. Tüm türleri bir kerede saymak için veritabanının kitapları gruplara ayırması gerekir.

aggregate bir işlem hattı çalıştırır: her biri çıktısını bir sonrakine aktaran aşamalardan oluşan bir dizi. $group aşaması, _id değerinin her farklı değeri için bir çıktı belgesi oluşturur. Burada _id bir belge kimliği değildir. Neye göre gruplandırılacağını belirtir ve her çıktı belgesi bu değeri _id içinde tutar. Bir alan adının önündeki $, '$team', “bu belgenin o alandaki değeri” anlamına gelir. Bu olmazsa 'team' her belge için aynı metindir ve hepsi tek bir grupta toplanır. $sum: 1 her belge için 1 ekler.

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

Bir grup sonucu yalnızca _id değerini ve biriktiricilerinin hesapladığı alanları içerir; girdi belgelerindeki diğer alanlar korunmaz.

challenge icon

Görev

Kolay

Bu adım, Counting Documents dersindeki proje dosyalarınız yerine ders için hazırlanmış kitaplık API projesini açar (server.js dahil). Önceki çalışmalarınız kendi dersinde kalır. server.js dosyasını düzenlemeden önce sağlanan dosyaları okuyun. Bu kopyada kitaplık CRUD rotaları, benzersiz ISBN indeksi, GET /books filtreleri ve GET /stats sayımları bulunur.

Bir tür sayfası, hiç planlanmamış türler de dahil olmak üzere, her türde kaç kitap bulunduğunu tek bir sorguyla gösterir.

  1. server.js içinde bir GET /stats/genres rotası ekleyin.
  2. books üzerinde tek bir $group aşamasıyla bir toplama işlemi çalıştırın. Bu işlem, her farklı genre için, o türe sahip kitapların sayısını içeren bir sonuç oluşturur.
  3. 200 durum koduyla ve toplama işleminin döndürdüğü diziyle yanıt verin. Her öğede tür _id içinde, kitap sayısı ise count içinde bulunur. $group içinde _id bir ObjectId değildir: kitapların gruplandırıldığı değerdir. Öğeler herhangi bir sırada gelebilir. Başlangıç kitaplarıyla yanıt şöyledir:
    [ { "_id": "scifi", "count": 2 }, { "_id": "classic", "count": 1 } ]
    Deneyin: GET /stats/genres isteğini gönderin: yukarıdaki iki öğeyi alırsınız. { "title": "Leaves of Grass", "author": "Walt Whitman", "year": 1855, "isbn": "9780140421996", "genre": "poetry" } içeriğiyle POST /books isteğini gönderin: 201 alırsınız. GET /stats/genres isteğini tekrar gönderin: üçüncü bir öğe belirir, { "_id": "poetry", "count": 1 }.

Kendin dene

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

// Her hata, { error: message } biçiminde yanıt verir.
function sendError(res, status, message) {
    res.status(status).json({ error: message });
}

// :id geçerliyse ObjectId olarak döndürülür; geçersiz bir id için 400 yanıtı verildikten sonra null döndürülür.
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 ile Veritabanları bölümündeki tüm dersler

Kendi başına pratik yap: Express playground