Menu
CoddyTech

Chỉ mục duy nhất

Thuộc phần Cơ sở dữ liệu với MongoDB trong hành trình Node.js của Coddy. Bài học 17 trên 29.

Không bao giờ lưu hai cuốn sách có cùng ISBN. Việc gọi findOne trước mỗi lần chèn vẫn để lại sơ hở: hai yêu cầu có thể cùng không tìm thấy gì rồi đều chèn dữ liệu.

Chỉ mục (index) là danh sách đã sắp xếp các giá trị của một trường mà cơ sở dữ liệu lưu bên cạnh một collection để tìm tài liệu nhanh hơn. Chỉ mục duy nhất (unique) cũng từ chối tài liệu thứ hai có giá trị đã tồn tại trong chỉ mục: thao tác chèn gây ra MongoServerError với mã 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;
    // email này đã có người dùng
}

Tạo chỉ mục lúc khởi động, trước khi thêm dữ liệu. Tạo một chỉ mục đã tồn tại sẽ không làm gì cả.

challenge icon

Thử thách

Dễ

Không có hai cuốn sách nào được dùng chung ISBN, ngay cả khi hai yêu cầu đến cùng một lúc. Hãy để cơ sở dữ liệu từ chối bản trùng lặp.

  1. Trong server.js, ở đầu tệp, hãy tạo chỉ mục duy nhất trên isbn. Thực hiện việc này trước khi phần code khởi tạo lưu ba cuốn sách bằng insertMany, và chờ thao tác hoàn tất bằng await.
  2. Trong POST /books, thao tác thêm giờ sẽ phát sinh lỗi nếu ISBN đã được lưu. Hãy bắt lỗi đó. Khi code của lỗi là 11000, hãy trả về trạng thái 409 cùng một lỗi, chẳng hạn { "error": "That ISBN is already on the shelf" }. Phát sinh lại mọi lỗi khác để Express trả về 500.
  3. Sách có ISBN mới vẫn nhận được trạng thái 201 cùng cuốn sách đã lưu như trước. Thử nhé: Gửi POST /books với bản sao thứ hai của Dune: { "title": "Dune", "author": "Frank Herbert", "year": 1965, "isbn": "9780441013593", "genre": "scifi" }. Bạn nhận được trạng thái 409 cùng lỗi. Gửi GET /books: bạn vẫn nhận được ba cuốn sách, và thẻ Data hiển thị Dune một lần. Gửi POST /books với { "title": "Beloved", "author": "Toni Morrison", "year": 1987, "isbn": "9781400033416", "genre": "literary" }: bạn nhận được trạng thái 201.

Tự làm thử

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());

// Mọi lỗi đều trả về { error: message }.
function sendError(res, status, message) {
    res.status(status).json({ error: message });
}

// Trả về :id dưới dạng ObjectId, hoặc null sau khi trả về mã 400 nếu id không hợp lệ.
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 iconTự kiểm tra

Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.

Tất cả bài học trong Cơ sở dữ liệu với MongoDB

Tự luyện tập: Playground Express