Menu
CoddyTech

الفهارس الفريدة

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

يجب ألا يُخزَّن كتابان لهما رقم ISBN واحد أبدًا، كما أن إجراء findOne قبل كل عملية إدراج يترك مجالًا لحدوث تعارض: فقد لا يعثر طلبان على أي نتيجة، ثم يُدرجان الكتابين كليهما.

الفهرس قائمة مرتبة بقيم أحد الحقول، تحتفظ بها قاعدة البيانات بجانب المجموعة للعثور على المستندات بسرعة. ويرفض الفهرس الفريد أيضًا مستندًا ثانيًا يحمل قيمة موجودة فيه بالفعل: إذ تؤدي عملية الإدراج إلى طرح MongoServerError بالرمز 11000، "E11000 duplicate key error".

await users.createIndex({ email: 1 }, { unique: true });

try {
    await users.insertOne({ email: 'ada@example.com' });
} catch (err) {
    if (err.code !== 11000) throw err;
    // عنوان البريد الإلكتروني مستخدم بالفعل
}

أنشئ الفهرس عند بدء التشغيل، قبل إدخال البيانات. ولا يفعل إنشاء فهرس موجود أي شيء.

challenge icon

التحدي

سهل

لا يجوز أن يشترك كتابان في رقم ISBN نفسه، حتى لو وصل طلبان في اللحظة نفسها. دع قاعدة البيانات ترفض النسخة المكررة.

  1. في server.js، أضف في الأعلى فهرسًا فريدًا على isbn. افعل ذلك قبل أن يخزّن البرنامج التمهيدي الكتب الثلاثة باستخدام insertMany، وانتظر اكتماله باستخدام await.
  2. في POST /books، تؤدي عملية الإدراج الآن إلى رمي خطأ عندما يكون رقم ISBN مخزنًا بالفعل. التقط هذا الخطأ. عندما تكون قيمة code هي 11000، أرسل استجابة بالحالة 409 وخطأ، مثل { "error": "That ISBN is already on the shelf" }. أعد رمي أي خطأ آخر، لكي يرسل Express استجابة بالحالة 500.
  3. يظل الكتاب الذي يحمل رقم ISBN جديدًا يحصل على الحالة 201 والكتاب المخزن، كما كان من قبل. جرّب ذلك: أرسل POST /books مع نسخة ثانية من Dune: { "title": "Dune", "author": "Frank Herbert", "year": 1965, "isbn": "9780441013593", "genre": "scifi" }. ستحصل على الحالة 409 والخطأ. أرسل GET /books: ستحصل على ثلاثة كتب كما في السابق، وتعرض علامة تبويب Data كتاب Dune مرة واحدة. أرسل POST /books مع { "title": "Beloved", "author": "Toni Morrison", "year": 1987, "isbn": "9781400033416", "genre": "literary" }: ستحصل على الحالة 201.

جرّب بنفسك

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.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;
    await books.insertOne(book);
    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');
});
quiz iconاختبر نفسك

يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.

جميع دروس قواعد البيانات باستخدام MongoDB

تدرّب بنفسك: Playground لـ Express