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ả.
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.
- Trong
server.js, ở đầu tệp, hãy tạo chỉ mục duy nhất trênisbn. 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ằnginsertMany, và chờ thao tác hoàn tất bằngawait. - 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 đó. Khicodecủ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. - 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 /booksvớ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ửiGET /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ửiPOST /booksvớ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');
});
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