Menu
CoddyTech

Upsert

Termasuk bagian Database dengan MongoDB dalam perjalanan Node.js di Coddy. Pelajaran 18 dari 29.

Setiap pembaca memberi nilai pada buku satu kali. Jika memberi nilai lagi, bintangnya harus berubah, bukan menambahkan nilai kedua.

Dengan { upsert: true }, updateOne memperbarui dokumen yang cocok dengan filter, dan jika tidak ada yang cocok, metode ini menyisipkan dokumen berdasarkan bidang filter ditambah pembaruan. Satu pemanggilan mencakup kali pertama dan setiap kali sesudahnya.

// satu dokumen pengaturan untuk setiap pengguna
await settings.updateOne(
    { user: 'ada' },
    { $set: { theme: 'dark' } },
    { upsert: true }
);

Filter menentukan hal yang membuat dokumen unik, dalam contoh ini, satu dokumen per pengguna. upsertedId pada hasil akan berisi nilai jika pemanggilan tersebut menyisipkan dokumen. insertOne biasa tidak bisa melakukan ini: metode itu menambahkan dokumen setiap kali.

challenge icon

Tantangan

Mudah

Pembaca memberi nilai buku dari 1 sampai 5 bintang. Setiap pembaca hanya memberi nilai untuk sebuah buku satu kali: jika memberi nilai lagi, jumlah bintangnya berubah, bukan menambahkan nilai kedua.

  1. Di server.js, di sebelah const books = db.collection('books');, buka koleksi kedua bernama ratings. Setiap penilaian berupa satu dokumen:
    { "bookId": "6720a1f4c2b9e81d3a5f0c11", "user": "ada", "stars": 4 }
    Simpan bookId sebagai ObjectId yang dikembalikan oleh parseId, bukan sebagai teks dari URL. Dalam JSON, keduanya terlihat sama.
  2. Tambahkan PUT /books/:id/ratings. Bodynya terlihat seperti { "user": "ada", "stars": 4 }. Jika pengguna ini sudah memberi nilai untuk buku ini, ubah jumlah bintangnya. Jika belum, tambahkan penilaian baru. Lakukan keduanya dengan satu panggilan. Berikan respons dengan status 200 dan penilaiannya: { "user": "ada", "stars": 4 }.
  3. Tambahkan GET /books/:id/ratings. Responsnya berstatus 200 dan berupa array berisi penilaian buku tersebut seperti yang disimpan. Penilaian untuk buku lain tidak boleh muncul:
    [ { "_id": "6720a1f4c2b9e81d3a5f0c21", "bookId": "6720a1f4c2b9e81d3a5f0c11", "user": "ada", "stars": 2 } ]
  4. Kedua rute menggunakan parseId untuk id yang tidak valid, sehingga menghasilkan 400. Coba: Kirim GET /books dan salin _id milik Dune. Kirim PUT /books/<Dune's _id>/ratings dengan { "user": "ada", "stars": 4 }: kamu mendapatkan 200 dan { "user": "ada", "stars": 4 }. Kirim lagi dengan { "user": "ada", "stars": 2 }: kamu mendapatkan 200 dan { "user": "ada", "stars": 2 }. Kirim GET /books/<Dune's _id>/ratings: kamu mendapatkan satu penilaian milik Ada, dengan 2 bintang. Beri nilai Dune dengan { "user": "bo", "stars": 5 } lalu tampilkan daftarnya lagi: kamu mendapatkan dua penilaian. Tab Data sekarang menampilkan koleksi ratings. GET /books/<Kindred's _id>/ratings mengembalikan [].

Coba sendiri

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

// Setiap error mengembalikan { error: message }.
function sendError(res, status, message) {
    res.status(status).json({ error: message });
}

// Kembalikan :id sebagai ObjectId, atau null setelah mengirim respons 400 untuk id yang tidak valid.
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');
});

Semua pelajaran di Database dengan MongoDB

Latihan sendiri: Playground Express