Menu
CoddyTech

عمليات الإدراج أو التحديث

جزء من قسم قواعد البيانات باستخدام MongoDB في رحلة Node.js على Coddy. الدرس 18 من 29.

يقيّم كل قارئ الكتاب مرة واحدة. وينبغي أن يؤدي تقييمه مرة أخرى إلى تغيير النجوم، لا إلى إضافة تقييم ثانٍ.

باستخدام { upsert: true }، تحدّث updateOne المستند الذي يطابقه المرشّح، وإذا لم يطابق أي مستند، تُدرج مستندًا مبنيًا على حقول المرشّح بالإضافة إلى التحديث. يغطي استدعاء واحد المرة الأولى وكل مرة بعدها.

// مستند إعدادات واحد لكل مستخدم
await settings.updateOne(
    { user: 'ada' },
    { $set: { theme: 'dark' } },
    { upsert: true }
);

يحدّد المرشّح ما يجعل المستند فريدًا؛ وهنا، مستند واحد لكل مستخدم. وتُعيَّن قيمة upsertedId في النتيجة عند إدراج المستند بواسطة الاستدعاء. لا يمكن لـ insertOne العادي فعل ذلك: فهو يضيف مستندًا في كل مرة.

challenge icon

التحدي

سهل

يقيّم القراء الكتب من نجمة واحدة إلى 5 نجوم. يقيّم كل قارئ الكتاب مرة واحدة: فتغيير التقييم مرة أخرى يغيّر عدد النجوم بدلًا من إضافة تقييم ثانٍ.

  1. في server.js، بجوار const books = db.collection('books');، افتح مجموعة ثانية باسم ratings. كل تقييم هو مستند واحد:
    { "bookId": "6720a1f4c2b9e81d3a5f0c11", "user": "ada", "stars": 4 }
    خزّن bookId على هيئة ObjectId الذي تُرجعه parseId، وليس كنص الرابط. كلاهما يبدو متماثلًا في JSON.
  2. أضف PUT /books/:id/ratings. يبدو محتوى الطلب هكذا: { "user": "ada", "stars": 4 }. إذا كان هذا المستخدم قد قيّم هذا الكتاب من قبل، فغيّر عدد نجومه. وإذا لم يكن قد قيّمه، فأضف تقييمًا جديدًا. نفّذ الأمرين باستدعاء واحد. أرسل الاستجابة بالحالة 200 والتقييم: { "user": "ada", "stars": 4 }.
  3. أضف GET /books/:id/ratings. يستجيب بالحالة 200 ومصفوفة تقييمات هذا الكتاب كما هي مخزّنة. يجب ألا تظهر تقييمات الكتب الأخرى:
    [ { "_id": "6720a1f4c2b9e81d3a5f0c21", "bookId": "6720a1f4c2b9e81d3a5f0c11", "user": "ada", "stars": 2 } ]
  4. يستخدم المساران 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