Aplikasi toko buku sering menghadapi percabangan yang menjemukan: “kalau judulnya sudah ada, perbarui harganya; kalau belum, sisipkan”. Tanpa bantuan khusus, kita harus findOne() dulu, memeriksa hasilnya, lalu memilih updateOne() atau insertOne() — dua bolak-balik ke server dan celah kondisi berubah di tengah jalan. Opsi { upsert: true } memangkas semuanya menjadi satu perintah: update bila cocok, insert bila tidak.

update atau insert dalam Satu Perintah #

Kita mulai dari katalog yang belum memuat Rumah Kaca:

js
use latihan_crud2
db.buku.drop()
db.buku.insertMany([
  { judul: "Laskar Pelangi", penulis: "Andrea Hirata", harga: 95000, stok: 12, kategori: "Fiksi", tag: ["novel", "bestseller"], rating: 4.6 },
  { judul: "Bumi Manusia", penulis: "Pramoedya Ananta Toer", harga: 110000, stok: 8, kategori: "Fiksi", tag: ["novel", "klasik"], rating: 4.8 },
  { judul: "Atomic Habits", penulis: "James Clear", harga: 135000, stok: 20, kategori: "Pengembangan Diri", tag: ["nonfiksi", "bestseller"], rating: 4.7 },
  { judul: "Filosofi Teras", penulis: "Henry Manampiring", harga: 89000, stok: 0, kategori: "Pengembangan Diri", tag: ["nonfiksi"], rating: 4.5 },
  { judul: "Clean Code", penulis: "Robert C. Martin", harga: 165000, stok: 5, kategori: "Teknologi", tag: ["pemrograman", "bestseller"], rating: 4.4 }
])

Output yang Dihasilkan:

text
switched to db latihan_crud2
true
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('6ad38839df7d7bd42c35aa25'),
    '1': ObjectId('6ad38839df7d7bd42c35aa26'),
    '2': ObjectId('6ad38839df7d7bd42c35aa27'),
    '3': ObjectId('6ad38839df7d7bd42c35aa28'),
    '4': ObjectId('6ad38839df7d7bd42c35aa29')
  }
}

Kirim upsert untuk judul yang belum ada:

js
db.buku.updateOne(
  { judul: "Rumah Kaca" },
  { $set: { penulis: "Pramoedya Ananta Toer", harga: 98000 } },
  { upsert: true }
)
db.buku.findOne({ judul: "Rumah Kaca" })

Output yang Dihasilkan:

text
{
  acknowledged: true,
  insertedId: ObjectId('6ad388399d08d4e3fde1a3b0'),
  matchedCount: 0,
  modifiedCount: 0,
  upsertedCount: 1
}
{
  _id: ObjectId('6ad388399d08d4e3fde1a3b0'),
  judul: 'Rumah Kaca',
  harga: 98000,
  penulis: 'Pramoedya Ananta Toer'
}

Mari kita bedah hasilnya:

  1. matchedCount: 0, upsertedCount: 1 — filter tidak menemukan document, sehingga upsert menyisipkan document baru.
  2. insertedId memuat _id document baru itu (mongosh menamai field-nya insertedId; pada driver resmi field ini disebut upsertedId).
  3. Document baru terbentuk dari gabungan field filter (judul) dan dokumen update (penulis, harga) — findOne() membuktikan ketiganya menyatu.

Jalankan perintah yang sama lagi — kini document sudah ada, sehingga upsert berlaku sebagai update biasa:

js
db.buku.updateOne(
  { judul: "Rumah Kaca" },
  { $set: { harga: 95000 } },
  { upsert: true }
)

Output yang Dihasilkan:

text
{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}

Sifat ini membuat upsert idempoten: perintah yang sama boleh diulang berkali-kali — entri pertama menyisipkan, entri berikutnya memperbarui — tanpa pernah menghasilkan duplikat. Inilah alasan upsert menjadi fondasi pola sinkronisasi data.

upsert pada replaceOne dan updateMany #

Opsi yang sama berlaku pada replaceOne() dan updateMany():

js
db.buku.replaceOne(
  { judul: "Arus Balik" },
  { judul: "Arus Balik", penulis: "Pramoedya Ananta Toer", harga: 92000 },
  { upsert: true }
)

Output yang Dihasilkan:

text
{
  acknowledged: true,
  insertedId: ObjectId('6ad3883a9d08d4e3fde1a3b1'),
  matchedCount: 0,
  modifiedCount: 0,
  upsertedCount: 1
}

Pada replaceOne(), dokumen pengganti menyediakan seluruh isi document baru — tidak ada penggabungan dengan field lama, sesuai aturan Chapter replaceOne(). Adapun updateMany() dengan upsert: true menyisipkan paling banyak satu document — dibentuk dari gabungan filter dan update, bukan satu per kecocokan:

js
db.buku.updateMany({ kategori: "Puisi" }, { $set: { harga: 60000 } }, { upsert: true })
db.buku.find({ kategori: "Puisi" })

Output yang Dihasilkan:

text
{
  acknowledged: true,
  insertedId: ObjectId('6ad3883a9d08d4e3fde1a3b2'),
  matchedCount: 0,
  modifiedCount: 0,
  upsertedCount: 1
}
[
  {
    _id: ObjectId('6ad3883a9d08d4e3fde1a3b2'),
    kategori: 'Puisi',
    harga: 60000
  }
]

Perhatikan isi document hasilnya: hanya kategori dan harga — field yang tidak disebut filter maupun dokumen update tidak ikut dibuat, bahkan judul sekalipun. Bila skema menuntut field tertentu selalu ada, sertakan nilainya di filter atau di update; penegakan aturan otomatis dibahas pada Section Schema Validation.

Race Condition dan Unique Index #

Sisi lemah upsert muncul saat dua client menulis bersamaan. Keduanya memeriksa filter di saat yang hampir sama, sama-sama mendapati document belum ada, lalu sama-sama menyisipkan — hasilnya dua document kembar. Perlombaan semacam ini tidak bisa dihindari dari sisi aplikasi, tetapi bisa dijaga dari sisi data: unique index pada field kunci mengubah sisipan kembar menjadi galat E11000 yang tertangkap:

js
db.buku.createIndex({ judul: 1 }, { unique: true })
db.buku.insertOne({ judul: "Rumah Kaca", harga: 50000 })

Output yang Dihasilkan:

text
judul_1
Uncaught 
MongoServerError: E11000 duplicate key error collection: latihan_crud2.buku index: judul_1 dup key: { judul: "Rumah Kaca" }

Kapan Memakai Upsert #

Empat pola yang paling umum:

  • Sinkronisasi data eksternal — data dari pemasok masuk berulang; upsert menjadikan proses aman dijalankan berkali-kali.
  • Penghitung — statistik kunjungan per judul naik satu tiap peristiwa, dan baris pertama dibuat otomatis.
  • Keranjang belanja — menambah item membuat keranjang bila belum ada, memperbarui bila sudah.
  • Konfigurasi aplikasi — pengaturan dibuat dengan nilai bawaan saat pertama kali dibutuhkan.

Untuk field yang cukup diisi sekali saat penyisipan — seperti cap waktu pembuatan — pasangkan dengan operator $setOnInsert yang sudah kita uji di Chapter Update Operators. Terakhir, bersihkan database latihan:

js
db.dropDatabase()

Output yang Dihasilkan:

text
{ ok: 1, dropped: 'latihan_crud2' }

Rangkuman #

  • { upsert: true } mengubah updateOne(), updateMany(), dan replaceOne() menjadi “update bila ada, insert bila tidak”.
  • Document baru terbentuk dari gabungan field filter dan dokumen update; _id-nya dilaporkan pada insertedId (mongosh).
  • updateMany() dengan upsert menyisipkan paling banyak satu document.
  • Upsert idempoten — aman diulang; itulah dasar pola sinkronisasi.
  • Dua client paralel bisa menyisipkan kembar; unique index menjaganya dengan galat E11000 yang tinggal diulang.

Kita sudah menulis dan membaca hasilnya; Chapter penutup Section ini membahas lapisan jaminannya: write concern dan struktur hasil penulisan.