Bentuk data tidak pernah selesai: bulan depan produk butuh field rating, kuartal depan rating jadi wajib, dan tahun depan muncul format document versi kedua. Database relasional menjawab dengan ALTER TABLE dan migrasi besar-besaran; MongoDB memberi kita peralatan yang lebih halus — field opsional, updateMany, collMod, validationLevel, dan validationAction. Chapter penutup ini merangkai semuanya menjadi strategi evolusi schema yang aman.

Tiga Langkah Evolusi yang Aman #

Polanya selalu sama untuk setiap perubahan bentuk:

  1. Tambahkan sebagai opsional — daftarkan field baru di properties tanpa memasukkannya ke required. Document lama tetap valid, document baru boleh mengisinya.
  2. Isi nilai default untuk data lama — backfill lewat updateMany dengan filter $exists: false, dipecah menjadi bila data besar.
  3. Perketat menjadi wajib — begitu semua document sudah punya field tersebut, pindahkan ke required lewat collMod.

Selama proses itu, validationLevel: "moderate" menjaga data lama yang belum dibereskan tetap bisa diedit, dan validationAction: "warn" memberi kita masa pengamatan sebelum penolakan dinyalakan.

Studi Kasus: Menambahkan rating #

Kita mulai dari collection warisan tanpa validator — tiga produk lama tanpa rating:

js
use latihan_validasi
db.produk.drop()
db.createCollection("produk")
db.produk.insertMany([
  { nama: "Kopi Gayo 250g", harga: 85000, kategori: "Minuman" },
  { nama: "Teh Melati 100g", harga: 12000, kategori: "Minuman" },
  { nama: "Gula Pasir 1kg", harga: 15000, kategori: "Makanan" }
])

Output yang Dihasilkan:

text
switched to db latihan_validasi
true
{ ok: 1 }
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('6ad39225ac74d38a5a53c8b8'),
    '1': ObjectId('6ad39225ac74d38a5a53c8b9'),
    '2': ObjectId('6ad39225ac74d38a5a53c8ba')
  }
}

Langkah pertama: pasang validator dengan rating opsional (angka 0–5) plus field penanda schema_version yang juga opsional. Rating sengaja memakai "number" alih-alih "double" agar backfill nilai bulat 0 di langkah kedua tidak tersandung aturan tipe — seperti yang kita lihat di Chapter Tipe BSON dalam Validasi, literal bernilai bulat tersimpan sebagai int:

js
db.runCommand({ collMod: "produk", validator: { $jsonSchema: {
  bsonType: "object",
  required: ["nama", "harga"],
  properties: {
    nama: { bsonType: "string" },
    harga: { bsonType: "number", minimum: 0 },
    kategori: { enum: ["Minuman", "Makanan", "Alat Tulis"] },
    rating: { bsonType: "number", minimum: 0, maximum: 5 },
    schema_version: { bsonType: "int" }
  }
} } }).ok
db.produk.insertOne({ nama: "Susu UHT 1L", harga: 19500, kategori: "Minuman", rating: 4.5, schema_version: 1 })

Output yang Dihasilkan:

text
1
{
  acknowledged: true,
  insertedId: ObjectId('6ad39225ac74d38a5a53c8bb')
}

Tiga document lama tidak tersentuh, dan document baru yang mengisi rating langsung diterima. Langkah kedua: backfill nilai default 0.0 untuk seluruh document yang belum punya rating, lalu — karena semuanya kini terisi — naikkan rating menjadi wajib:

js
db.produk.updateMany({ rating: { $exists: false } }, { $set: { rating: 0.0 } })
db.runCommand({ collMod: "produk", validator: { $jsonSchema: {
  bsonType: "object",
  required: ["nama", "harga", "rating"],
  properties: {
    nama: { bsonType: "string" },
    harga: { bsonType: "number", minimum: 0 },
    kategori: { enum: ["Minuman", "Makanan", "Alat Tulis"] },
    rating: { bsonType: "number", minimum: 0, maximum: 5 },
    schema_version: { bsonType: "int" }
  }
} } }).ok
db.produk.insertOne({ nama: "Kopi Tubruk 200g", harga: 12000 })
db.produk.find()

Output yang Dihasilkan:

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

MongoServerError: Document failed validation
Additional information: {
  failingDocumentId: ObjectId('6ad39225ac74d38a5a53c8bc'),
  details: {
    operatorName: '$jsonSchema',
    schemaRulesNotSatisfied: [
      {
        operatorName: 'required',
        specifiedAs: { required: [ 'nama', 'harga', 'rating' ] },
        missingProperties: [ 'rating' ]
      }
    ]
  }
}
[
  {
    _id: ObjectId('6ad39225ac74d38a5a53c8b8'),
    nama: 'Kopi Gayo 250g',
    harga: 85000,
    kategori: 'Minuman',
    rating: 0
  },
  {
    _id: ObjectId('6ad39225ac74d38a5a53c8b9'),
    nama: 'Teh Melati 100g',
    harga: 12000,
    kategori: 'Minuman',
    rating: 0
  },
  {
    _id: ObjectId('6ad39225ac74d38a5a53c8ba'),
    nama: 'Gula Pasir 1kg',
    harga: 15000,
    kategori: 'Makanan',
    rating: 0
  },
  {
    _id: ObjectId('6ad39225ac74d38a5a53c8bb'),
    nama: 'Susu UHT 1L',
    harga: 19500,
    kategori: 'Minuman',
    rating: 4.5,
    schema_version: 1
  }
]

updateMany mengisi tiga document lama (modifiedCount: 3), validator kedua menaikkan rating ke required, dan insert tanpa rating pun ditolak — kontrak baru sudah berlaku penuh tanpa jedah migrasi.

Pola schema_version #

Ketika perubahan bentuk makin sering, tandai setiap document dengan versi schema yang diikutinya. Kode pembaca lalu bisa berperilaku berbeda per versi, dan migrasi bisa dijalankan per kelompok:

js
db.produk.insertOne({ nama: "Susu Kental Manis", harga: 12500, kategori: "Makanan", rating: 4.2, schema_version: 2, berat: 0.39 })
db.produk.countDocuments({ schema_version: 1 })
db.produk.countDocuments({ schema_version: 2 })

Output yang Dihasilkan:

text
{
  acknowledged: true,
  insertedId: ObjectId('6ad39225ac74d38a5a53c8bd')
}
1
1

Document versi 2 membawa field berat yang belum diatur validator — sah, karena field baru selalu lahir sebagai opsional. Query countDocuments per versi menjadi alat bantu pantau migrasi: berapa document yang sudah pindah, berapa yang menunggu.

Jembatan moderate dan warn #

Dua kenop dari Chapter sebelumnya sering dipakai berpasangan saat perubahan besar diterapkan pada collection yang masih aktif ditulis:

js
db.runCommand({ collMod: "produk", validationLevel: "moderate", validationAction: "warn" }).ok
db.produk.insertOne({ nama: "Sirup Markisa", hrga: 21000 })
db.produk.countDocuments()
db.runCommand({ collMod: "produk", validationLevel: "strict", validationAction: "error" }).ok

Output yang Dihasilkan:

text
1
{
  acknowledged: true,
  insertedId: ObjectId('6ad39225ac74d38a5a53c8be')
}
6
1

Selama jembatan itu aktif, insert bermasalah diterima dan pelanggarannya tercatat di log mongod (Chapter Validation Actions) — transaksi tidak putus, tim punya waktu memperbaiki penulis data. Baris terakhir menutup jembatan dan mengembalikan penjagaan penuh. Bersihkan database latihan:

js
db.dropDatabase()

Output yang Dihasilkan:

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

Checklist Evolusi Schema #

  • Tambahkan field baru sebagai opsional di properties — jangan langsung ke required.
  • Backfill nilai default dengan updateMany + $exists: false; pecah menjadi batch kecil untuk collection besar.
  • Naikkan field ke required hanya setelah semua document terisi.
  • Tandai document dengan schema_version dan pantau progres migrasi lewat countDocuments per versi.
  • Pakai validationLevel: "moderate" selama data lama belum bersih; kembali ke "strict" setelah selesai.
  • Pakai validationAction: "warn" untuk masa pengamatan sebelum menolak; kembali ke "error" setelah penulis data diperbaiki.
  • Ubah validator bertahap — satu perubahan per rilis jauh lebih mudah dianalisis daripada perombakan sekaligus.

Dengan checklist ini, siklus hidup schema berjalan seiring produk: kontrak data mengencang selangkah demi selangkah, tidak pernah memutus aliran transaksi, dan selalu meninggalkan jejak yang bisa diperiksa.