Upsert
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 18 trên 29.
Mỗi độc giả chỉ đánh giá một cuốn sách một lần. Nếu đánh giá lại, số sao sẽ thay đổi chứ không tạo thêm đánh giá thứ hai.
Với { upsert: true }, updateOne cập nhật tài liệu khớp với bộ lọc. Nếu không có tài liệu nào khớp, hàm sẽ chèn một tài liệu được tạo từ các trường trong bộ lọc cùng với phần cập nhật. Một lần gọi hàm xử lý cả lần đầu tiên và mọi lần sau đó.
// Mỗi người dùng có một tài liệu cài đặt
await settings.updateOne(
{ user: 'ada' },
{ $set: { theme: 'dark' } },
{ upsert: true }
);Bộ lọc cho biết điều gì giúp xác định tính duy nhất của tài liệu; ở đây là mỗi người dùng có một tài liệu. upsertedId trong kết quả sẽ được thiết lập khi lần gọi hàm chèn tài liệu. insertOne thông thường không làm được điều này: mỗi lần gọi, hàm lại thêm một tài liệu.
Thử thách
DễĐộc giả chấm sách từ 1 đến 5 sao. Mỗi độc giả chỉ chấm một lần cho mỗi cuốn sách: nếu chấm lại, số sao sẽ thay đổi thay vì thêm một lượt đánh giá mới.
- Trong
server.js, bên cạnhconst books = db.collection('books');, hãy mở thêm một collection có tênratings. Mỗi lượt đánh giá là một tài liệu:Lưu{ "bookId": "6720a1f4c2b9e81d3a5f0c11", "user": "ada", "stars": 4 }bookIddưới dạng ObjectId màparseIdtrả về, không phải dưới dạng văn bản trong URL. Trong JSON, cả hai trông giống nhau. - Thêm
PUT /books/:id/ratings. Phần nội dung gửi lên có dạng{ "user": "ada", "stars": 4 }. Nếu người dùng này đã chấm cuốn sách, hãy thay đổi số sao của họ. Nếu chưa, hãy thêm lượt đánh giá mới. Thực hiện cả hai việc bằng một lệnh gọi. Trả về trạng thái 200 cùng lượt đánh giá:{ "user": "ada", "stars": 4 }. - Thêm
GET /books/:id/ratings. Lệnh này trả về trạng thái 200 cùng một mảng gồm các lượt đánh giá của cuốn sách đó như chúng đang được lưu. Không được hiển thị lượt đánh giá của các cuốn sách khác:[ { "_id": "6720a1f4c2b9e81d3a5f0c21", "bookId": "6720a1f4c2b9e81d3a5f0c11", "user": "ada", "stars": 2 } ] - Cả hai route đều dùng
parseIdkhi ID không hợp lệ, khi đó sẽ trả về 400. Thử nhé: GửiGET /booksrồi sao chép_idcủa Dune. GửiPUT /books/<Dune's _id>/ratingscùng{ "user": "ada", "stars": 4 }: bạn nhận được 200 và{ "user": "ada", "stars": 4 }. Gửi lại cùng{ "user": "ada", "stars": 2 }: bạn nhận được 200 và{ "user": "ada", "stars": 2 }. GửiGET /books/<Dune's _id>/ratings: bạn nhận được một lượt đánh giá của Ada với 2 sao. Chấm Dune với{ "user": "bo", "stars": 5 }rồi liệt kê lại: bạn nhận được hai lượt đánh giá. Tab Data giờ hiển thị collectionratings.GET /books/<Kindred's _id>/ratingstrả về[].
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.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());
// 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 phản hồi 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;
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');
});
Tất cả bài học trong Cơ sở dữ liệu với MongoDB
Tự luyện tập: Playground Express