Menu
CoddyTech

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.

challenge icon

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.

  1. Trong server.js, bên cạnh const books = db.collection('books');, hãy mở thêm một collection có tên ratings. Mỗi lượt đánh giá là một tài liệu:
    { "bookId": "6720a1f4c2b9e81d3a5f0c11", "user": "ada", "stars": 4 }
    Lưu bookId dưới dạng ObjectId mà parseId trả về, không phải dưới dạng văn bản trong URL. Trong JSON, cả hai trông giống nhau.
  2. 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 }.
  3. 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 } ]
  4. Cả hai route đều dùng parseId khi ID không hợp lệ, khi đó sẽ trả về 400. Thử nhé: Gửi GET /books rồi sao chép _id của Dune. Gửi PUT /books/<Dune's _id>/ratings cù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ửi GET /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ị collection ratings. GET /books/<Kindred's _id>/ratings trả 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