عمليات الإدراج أو التحديث
جزء من قسم قواعد البيانات باستخدام MongoDB في رحلة Node.js على Coddy. الدرس 18 من 29.
يقيّم كل قارئ الكتاب مرة واحدة. وينبغي أن يؤدي تقييمه مرة أخرى إلى تغيير النجوم، لا إلى إضافة تقييم ثانٍ.
باستخدام { upsert: true }، تحدّث updateOne المستند الذي يطابقه المرشّح، وإذا لم يطابق أي مستند، تُدرج مستندًا مبنيًا على حقول المرشّح بالإضافة إلى التحديث. يغطي استدعاء واحد المرة الأولى وكل مرة بعدها.
// مستند إعدادات واحد لكل مستخدم
await settings.updateOne(
{ user: 'ada' },
{ $set: { theme: 'dark' } },
{ upsert: true }
);يحدّد المرشّح ما يجعل المستند فريدًا؛ وهنا، مستند واحد لكل مستخدم. وتُعيَّن قيمة upsertedId في النتيجة عند إدراج المستند بواسطة الاستدعاء. لا يمكن لـ insertOne العادي فعل ذلك: فهو يضيف مستندًا في كل مرة.
التحدي
سهليقيّم القراء الكتب من نجمة واحدة إلى 5 نجوم. يقيّم كل قارئ الكتاب مرة واحدة: فتغيير التقييم مرة أخرى يغيّر عدد النجوم بدلًا من إضافة تقييم ثانٍ.
- في
server.js، بجوارconst books = db.collection('books');، افتح مجموعة ثانية باسمratings. كل تقييم هو مستند واحد:خزّن{ "bookId": "6720a1f4c2b9e81d3a5f0c11", "user": "ada", "stars": 4 }bookIdعلى هيئة ObjectId الذي تُرجعهparseId، وليس كنص الرابط. كلاهما يبدو متماثلًا في JSON. - أضف
PUT /books/:id/ratings. يبدو محتوى الطلب هكذا:{ "user": "ada", "stars": 4 }. إذا كان هذا المستخدم قد قيّم هذا الكتاب من قبل، فغيّر عدد نجومه. وإذا لم يكن قد قيّمه، فأضف تقييمًا جديدًا. نفّذ الأمرين باستدعاء واحد. أرسل الاستجابة بالحالة 200 والتقييم:{ "user": "ada", "stars": 4 }. - أضف
GET /books/:id/ratings. يستجيب بالحالة 200 ومصفوفة تقييمات هذا الكتاب كما هي مخزّنة. يجب ألا تظهر تقييمات الكتب الأخرى:[ { "_id": "6720a1f4c2b9e81d3a5f0c21", "bookId": "6720a1f4c2b9e81d3a5f0c11", "user": "ada", "stars": 2 } ] - يستخدم المساران
parseIdللتعامل مع المعرّف غير الصالح، وتكون الاستجابة 400. جرّب ذلك: أرسلGET /booksوانسخ_idالخاص بكتاب Dune. أرسلPUT /books/<Dune's _id>/ratingsمع{ "user": "ada", "stars": 4 }: ستحصل على 200 و{ "user": "ada", "stars": 4 }. أرسله مرة أخرى مع{ "user": "ada", "stars": 2 }: ستحصل على 200 و{ "user": "ada", "stars": 2 }. أرسلGET /books/<Dune's _id>/ratings: ستحصل على تقييم واحد، تقييم Ada، بنجمتين. قيّم Dune باستخدام{ "user": "bo", "stars": 5 }وأعِد عرض القائمة: ستحصل على تقييمين. تعرض علامة التبويب Data الآن مجموعةratings. يعيدGET /books/<Kindred's _id>/ratingsالقيمة[].
جرّب بنفسك
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');
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.listen(3000, () => {
console.log('Bookshelf is running on port 3000');
});
جميع دروس قواعد البيانات باستخدام MongoDB
تدرّب بنفسك: Playground لـ Express