Form pendaftaran menerima data dari manusia yang bisa lupa mengisi kolom, salah mengetik email, atau mengisi umur 15 tahun untuk layanan 17+. Aplikasi bisa memvalidasi input itu sebelum mengirimnya ke database — cepat, dengan pesan berbahasa manusia. Tetapi validasi di aplikasi bukan satu-satunya pertahanan: validator collection yang sudah kita pelajari di Section Schema Validation tetap berjaga di sisi server, tidak bisa dilewati oleh client yang curang maupun skrip lain yang menulis ke collection yang sama. Chapter ini membahas cara kedua lapis itu bekerja sebagai satu tim: validator menolak, aplikasi menerjemahkan penolakan itu menjadi pesan yang dipahami pengguna.
Dua Lapis dengan Dua Tugas #
| Aspek | Validasi di aplikasi | Validator collection |
|---|---|---|
| Waktu | sebelum data dikirim | saat operasi tulis tiba di server |
| Kecepatan umpan balik | instan, tanpa round-trip | satu round-trip ke server |
| Bentuk pesan | dirancang untuk pengguna ("Email tidak valid") | teknis (code 121 + errInfo) |
| Jangkauan | hanya aplikasi itu | semua penulis: aplikasi lain, skrip, mongosh |
| Bisa dilewati? | ya — lewat jalur tulis lain | tidak |
Kesimpulannya satu kalimat: validator collection adalah sumber kebenaran, validasi aplikasi adalah pengalaman pengguna. Aplikasi memvalidasi agar kesalahan ketik tidak perlu menunggu round-trip; database memvalidasi agar aturan tetap berlaku apa pun jalur datanya. Yang tersisa adalah persoalan teknik: bagaimana aplikasi membaca penolakan validator?
Validator sebagai Jaring, Aplikasi sebagai Penerjemah #
Kita pakai skenario pendaftaran dengan aturan realistis — nama minimal 3 karakter, email wajib ada, umur minimal 17, plus field schemaVersion yang kita bahas setelah ini:
db = db.getSiblingDB("latihan_validasi")
db.pendaftaran.drop()
db.createCollection("pendaftaran", { validator: { $jsonSchema: {
bsonType: "object",
required: ["nama", "email"],
properties: {
nama: { bsonType: "string", minLength: 3 },
email: { bsonType: "string", pattern: "^.+@.+$" },
umur: { bsonType: "int", minimum: 17 },
schemaVersion: { bsonType: "int", enum: [1] }
}
} } })
db.pendaftaran.insertOne({ nama: "Dewi Lestari", email: "[email protected]", umur: 21, schemaVersion: 1 })Output yang Dihasilkan:
latihan_validasi
true
{ ok: 1 }
{
acknowledged: true,
insertedId: ObjectId('6ad3e21b0fee04e93c941b0b')
}Document yang sehat lolos seperti biasa. Sekarang input bermasalah tiba — seolah-olah dari HTTP request: nama terlalu pendek, umur di bawah batas, dan email lupa diisi. Kita tangkap penolakannya mentah-mentah:
try {
db.pendaftaran.insertOne({ nama: "Si", umur: 15, schemaVersion: 1 })
} catch (e) {
print("code:", e.code)
printjson(e.errInfo)
}Output yang Dihasilkan:
code: 121
{
failingDocumentId: ObjectId('6ad3e21b0fee04e93c941b0c'),
details: {
operatorName: '$jsonSchema',
schemaRulesNotSatisfied: [
{
operatorName: 'properties',
propertiesNotSatisfied: [
{
propertyName: 'nama',
details: [
{
operatorName: 'minLength',
specifiedAs: {
minLength: 3
},
reason: 'specified string length was not satisfied',
consideredValue: 'Si'
}
]
},
{
propertyName: 'umur',
details: [
{
operatorName: 'minimum',
specifiedAs: {
minimum: 17
},
reason: 'comparison failed',
consideredValue: 15
}
]
}
]
},
{
operatorName: 'required',
specifiedAs: {
required: [
'nama',
'email'
]
},
missingProperties: [
'email'
]
}
]
}
}Struktur errInfo itu teratur: schemaRulesNotSatisfied berisi satu entri per kelompok aturan yang dilanggar — properties merinci field beserta operator dan nilainya, required mendaftar field yang hilang. Keteraturan inilah yang membuatnya bisa diterjemahkan secara programatik. Fungsi berikut mengubah struktur teknis itu menjadi kalimat untuk pengguna:
function terjemahkanValidasi(err) {
const pesan = []
for (const aturan of err.errInfo?.details?.schemaRulesNotSatisfied ?? []) {
if (aturan.operatorName === "required") {
for (const kolom of aturan.missingProperties ?? []) {
pesan.push(`Kolom ${kolom} wajib diisi`)
}
}
if (aturan.operatorName === "properties") {
for (const p of aturan.propertiesNotSatisfied ?? []) {
for (const d of p.details ?? []) {
if (d.operatorName === "minimum") pesan.push(`Kolom ${p.propertyName} minimal ${d.specifiedAs.minimum}`)
if (d.operatorName === "minLength") pesan.push(`Kolom ${p.propertyName} minimal ${d.specifiedAs.minLength} karakter`)
if (d.operatorName === "pattern") pesan.push(`Format ${p.propertyName} tidak valid`)
}
}
}
}
return pesan
}
try {
db.pendaftaran.insertOne({ nama: "Si", umur: 15, schemaVersion: 1 })
} catch (e) {
printjson(terjemahkanValidasi(e))
}Output yang Dihasilkan:
[Function: terjemahkanValidasi]
[
'Kolom nama minimal 3 karakter',
'Kolom umur minimal 17',
'Kolom email wajib diisi'
]Baris pertama adalah gema REPL mendeklarasikan fungsi; yang penting adalah array berikutnya — tiga pelanggaran berubah menjadi tiga kalimat yang bisa langsung ditampilkan di form. Mari kita bedah fungsinya: perulangan pertama menelusuri schemaRulesNotSatisfied; cabang required membaca missingProperties dan menghasilkan pesan per kolom hilang; cabang properties menuruni propertiesNotSatisfied lalu details per field, dan pilihan pesan menyesuaikan operator (minimum, minLength, pattern). Di aplikasi nyata, array ini dikembalikan handler sebagai respons 400 — dan validasi di sisi aplikasi yang kita tulis di form seharusnya menangkap sebagian besar kasus sebelum round-trip terjadi.
schemaVersion di Dalam Document #
Field schemaVersion: { enum: [1] } pada validator di atas bukan hiasan: setiap document menyatakan generasi schema-nya sendiri. Ketika aturan berkembang (misalnya umur wajib diisi mulai versi 2), validator diperbarui menerima enum: [1, 2], document baru ditulis dengan versi baru, dan aplikasi bisa memperlakukan document lama secara berbeda saat membacanya. Strategi evolusinya — termasuk migrasi bertahap — sudah kita bahas di Chapter Evolusi Document Schema.
Menjaga Kedua Lapis Sejajar #
Bahaya terbesar pola dua lapis adalah drift: aturan di aplikasi dan di database perlahan berbeda. Dua arah kerusakannya:
| Arah drift | Gejala | Dampak |
|---|---|---|
| Aplikasi lebih longgar dari database | input lolos form, ditolak server dengan 121 | pengguna menerima error internal yang tidak mereka pahami |
| Database lebih longgar dari aplikasi | jalur tulis lain (skrip, aplikasi lama) memasukkan data menyimpang | data tidak konsisten, pembacaan di aplikasi gagal diam-diam |
Tiga praktik yang menjaga keduanya sejajar: definisikan aturan sekali (satu berkas JSON Schema yang digenerate menjadi validator dan validasi aplikasi), jalankan contract test yang menegaskan dokumen invalid ditolak database, dan setiap perubahan aturan menyentuh kedua lapis dalam satu commit.
Terakhir, kita rapikan latihan:
db = db.getSiblingDB("latihan_validasi")
db.dropDatabase()Output yang Dihasilkan:
latihan_validasi
{ ok: 1, dropped: 'latihan_validasi' }Rangkuman #
- Validator collection adalah sumber kebenaran (berlaku untuk semua jalur tulis); validasi aplikasi bertugas kecepatan dan pesan yang ramah — keduanya saling melengkapi, bukan menggantikan.
- Error
121membawaerrInfo.schemaRulesNotSatisfiedyang terstruktur; fungsi penerjemah mengubahnya menjadi pesan per kolom untuk pengguna. schemaVersiondi dalam document menyatakan generasi aturan — dasar evolusi schema bertahap.- Drift dicegah dengan satu definisi aturan, contract test, dan perubahan yang menyentuh kedua lapis sekaligus.