Menyematkan atau Mereferensikan
Termasuk bagian Database dengan MongoDB dalam perjalanan Node.js di Coddy. Pelajaran 29 dari 29.
Ulasan dan penilaian sama-sama terkait dengan sebuah buku, tetapi tempat terbaik untuk menyimpan masing-masing berbeda.
Penyematan menempatkan data terkait di dalam dokumen, sehingga satu kali pembacaan mengambil buku beserta ulasannya. Cara ini cocok untuk data yang dibaca bersama dokumen induknya dan ukurannya tetap kecil. Referensi menyimpan data di koleksinya sendiri bersama id dokumen induk, seperti yang dilakukan ratings. Cara ini cocok untuk data yang terus bertambah tanpa batas atau dikueri secara terpisah, seperti menghitung rata-rata dari semua penilaian.
Sebuah dokumen dapat menampung paling banyak 16 MB, jadi array yang disematkan perlu dibatasi. Di dalam $push, $each mencantumkan nilai yang akan ditambahkan dan $slice: -3 menyimpan tiga nilai terakhir.
await players.updateOne(
{ name: 'Ada' },
{ $push: { recentScores: { $each: [120], $slice: -3 } } }
);Tantangan
MudahReview berukuran singkat, selalu ditampilkan bersama bukunya, dan sebuah buku hanya menampilkan 5 review terbarunya. Jadi, simpan review di dalam dokumen buku, dalam array reviews, dan jangan biarkan array itu bertambah hingga lebih dari 5.
- Di
server.js, tambahkanPOST /books/:id/reviews. Body-nya berbentuk seperti{ "user": "ada", "text": "Loved it" }. GunakanparseIduntuk id yang tidak valid, yang akan mendapat respons 400. - Dengan satu pembaruan, tambahkan review, misalnya
{ "user": "ada", "text": "Loved it" }, ke akhir arrayreviewsbuku. Dalam pembaruan yang sama, batasi array hingga 5 item terakhirnya, sehingga review tertua terhapus. Buku tanpa review akan mendapat array baru. - Jika tidak ada buku dengan
_idtersebut, berikan respons 404. - Jika tidak, berikan respons dengan status 201 dan hanya array
reviewsmilik buku, bukan seluruh buku, dengan review tertua di urutan pertama. Setelah review pertama, responsnya adalah:Coba: Kirim[ { "user": "ada", "text": "Loved it" } ]GET /booksdan salin_idmilik Dune. KirimPOST /books/<Dune's _id>/reviewsdengan{ "user": "ada", "text": "Loved it" }: kamu akan mendapat respons 201 dan array di atas. Kirim lima review lagi, dengan teks"Review 2"sampai"Review 6". Respons terakhir berisi lima review,Review 2hinggaReview 6, danLoved itsudah tidak ada. KirimGET /books/<Dune's _id>: Dune menyimpan lima review yang sama dalamreviews, dan tab Data menampilkannya di dalam buku.
Coba sendiri
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());
// Setiap error mengembalikan { error: message }.
function sendError(res, status, message) {
res.status(status).json({ error: message });
}
// Ubah :id menjadi ObjectId, atau kembalikan null setelah mengirim respons 400 untuk id yang tidak valid.
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');
});
Pelajaran ini berisi kuis singkat. Mulai pelajarannya untuk menjawab kuis dan melacak progresmu.
Semua pelajaran di Database dengan MongoDB
1Dokumen
Buku sebagai DokumenKoleksi dan DatabaseMenghubungkan ke MongoDBMembaca Dokumen TersimpanMenyisipkan DokumenMenemukan Satu DokumenMenemukan Dokumen berdasarkan ObjectIdMembandingkan AngkaFilter QueryMemilih Field yang DikembalikanMengurutkan Hasil QueryMempertahankan Hasil PertamaProyeksi, Pengurutan, dan Batas4Struktur
Sebuah module databasePaginasi di databaseLatihan sendiri: Playground Express