Menu
CoddyTech

Gömme veya Referans Verme

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

Yorumlar ve puanlar bir kitaba aittir, ancak her birini saklamak için en uygun yer farklıdır.

Gömme, ilişkili verileri belgenin içine koyar; böylece tek bir okumada kitaba ve yorumlarına ulaşılır. Üst öğesiyle birlikte okunan ve küçük kalan veriler için uygundur. Referans verme ise veriyi, ratings örneğinde olduğu gibi, üst öğenin kimliğiyle birlikte kendi koleksiyonunda tutar. Sınırsız büyüyen veya her puanın ortalaması gibi tek başına sorgulanan veriler için uygundur.

Bir belge en fazla 16 MB tutabilir, bu nedenle gömülü bir dizinin sınırı olmalıdır. $push içinde $each eklenecek değerleri listeler ve $slice: -3 son üç değeri tutar.

await players.updateOne(
    { name: 'Ada' },
    { $push: { recentScores: { $each: [120], $slice: -3 } } }
);
challenge icon

Görev

Kolay

Yorumlar kısadır, her zaman kitaplarıyla birlikte gösterilir ve bir kitapta yalnızca en yeni 5 yorum gösterilir. Bu nedenle yorumları kitap belgesinin içinde, bir reviews dizisinde tut ve bu dizinin hiçbir zaman 5 öğeyi aşmasına izin verme.

  1. server.js dosyasına POST /books/:id/reviews ekle. İstek gövdesi şöyle görünür: { "user": "ada", "text": "Loved it" }. Geçersiz bir kimlik için parseId kullan; bu durumda 400 yanıtı verilir.
  2. Tek bir güncellemeyle, örneğin { "user": "ada", "text": "Loved it" } yorumunu kitabın reviews dizisinin sonuna ekle. Aynı güncellemede diziyi son 5 öğesine indir, böylece en eski yorum diziden çıkar. Yorumu olmayan bir kitap için yeni bir dizi oluşturulur.
  3. Bu _id değerine sahip bir kitap yoksa 404 yanıtı ver.
  4. Aksi takdirde, 201 durum koduyla yalnızca kitabın reviews dizisini, kitabın tamamını değil, en eski yorum başta olacak şekilde yanıt olarak gönder. İlk yorumdan sonra yanıt şu olur:
    [ { "user": "ada", "text": "Loved it" } ]
    Deneyin: GET /books isteği gönder ve Dune'un _id değerini kopyala. { "user": "ada", "text": "Loved it" } içeriğiyle POST /books/<Dune's _id>/reviews isteği gönder: 201 yanıtını ve yukarıdaki diziyi alırsın. Metinleri "Review 2" ile "Review 6" arasında olan beş yorum daha gönder. Son yanıt beş yorum içerir: Review 2 ile Review 6; Loved it artık yoktur. GET /books/<Dune's _id> isteği gönder: Dune, reviews içinde aynı beş yorumu içerir ve Data sekmesi bunları kitabın içinde gösterir.

Kendin dene

import express from 'express';
import { z } from 'zod';
import { ObjectId } from 'mongodb';
import { books, ratings } from './db.js';

const app = express();
app.use(express.json());

// Her hata için { error: message } biçiminde yanıt verilir.
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;
}

const BookInput = z.object({
    title: z.string().min(1),
    author: z.string().min(1),
    year: z.number().int(),
    isbn: z.string().min(1),
    genre: z.string().min(1),
});

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 page = Number(req.query.page) || 1;
    const limit = Number(req.query.limit) || 10;
    const items = await books
        .find(filter)
        .skip((page - 1) * limit)
        .limit(limit)
        .toArray();
    const total = await books.countDocuments(filter);
    res.json({ items, total });
});

app.post('/books', async (req, res) => {
    const parsed = BookInput.safeParse(req.body);
    if (!parsed.success) {
        const issue = parsed.error.issues[0];
        return sendError(res, 400, `${issue.path.join('.')}: ${issue.message}`);
    }
    const book = parsed.data;
    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.get('/stats/genres', async (req, res) => {
    const genres = await books
        .aggregate([{ $group: { _id: '$genre', count: { $sum: 1 } } }])
        .toArray();
    res.json(genres);
});

app.get('/stats/top', async (req, res) => {
    const top = await ratings
        .aggregate([
            { $group: { _id: '$bookId', avg: { $avg: '$stars' }, count: { $sum: 1 } } },
            { $match: { count: { $gte: 2 } } },
            { $sort: { avg: -1 } },
            { $limit: 3 },
        ])
        .toArray();
    res.json(top);
});

app.get('/books/:id/full', async (req, res) => {
    const id = parseId(req, res);
    if (!id) return;
    const [book] = await books
        .aggregate([
            { $match: { _id: id } },
            {
                $lookup: {
                    from: 'authors',
                    localField: 'author',
                    foreignField: 'name',
                    as: 'authorInfo',
                },
            },
        ])
        .toArray();
    if (!book) {
        return sendError(res, 404, 'No such book');
    }
    res.json(book);
});

app.listen(3000, () => {
    console.log('Bookshelf is running on port 3000');
});
quiz iconKendini test et

Bu ders kısa bir quiz içerir. Soruları yanıtlamak ve ilerlemeni kaydetmek için derse başla.

MongoDB ile Veritabanları bölümündeki tüm dersler

Kendi başına pratik yap: Express playground