Menegakkan keunikan pada beberapa atribut di DynamoDB
DynamoDB menjamin keunikan untuk persis satu hal: primary key. Tidak ada
constraint UNIQUE (email), tidak ada UNIQUE (username), dan tak ada yang
membentang dua atribut. Datang dari SQL, ketiadaan itu adalah kejutan pertama —
dan tempat pertama orang diam-diam merilis sebuah race condition.
Bagaimana cara menegakkan constraint unik pada beberapa atribut di DynamoDB?
DynamoDB tidak memiliki constraint UNIQUE selain primary key, sehingga Anda menegakkan keunikan sendiri: modelkan setiap nilai yang dilindungi sebagai marker item tersendiri yang key-nya adalah nilai tersebut, lalu tulis record dan setiap marker bersama dalam satu TransactWriteItems, masing-masing put dijaga oleh attribute_not_exists. Tabrakan yang sudah ditegakkan engine menjadi constraint Anda.
- Tidak ada constraint unik — hanya primary key yang ditegakkan unik oleh engine. Setiap atribut "harus unik" lainnya adalah tugas Anda.
- Modelkan setiap aturan keunikan sebagai item-nya sendiri. Sebuah marker item khusus yang key-nya adalah nilai yang Anda lindungi mengubah "apakah email ini sudah dipakai?" menjadi sebuah tabrakan key yang sudah ditegakkan engine.
- Tulis mereka secara atomik dengan
TransactWriteItems. Satu transaksi, setiap put dijaga olehattribute_not_exists, sehingga semua marker dan record asli ter-commit bersama atau tidak satu pun. - Jangan check-then-write. Sebuah read-before-insert adalah race buku teks; dua signup konkuren keduanya membaca "kosong" dan keduanya menulis.
Mengapa pendekatan yang jelas itu salah
Nalurinya adalah me-Query (atau lebih buruk, Scan) untuk email, melihat
nihil, lalu PutItem akun baru. Itu adalah race check-then-act.
Dua orang mendaftar ada@lovelace.io pada milidetik yang sama. Kedua pembacaan
mengembalikan kosong. Kedua penulisan berhasil. Anda kini punya dua akun pada satu
email — dan tak ada di tabel yang menandainya.
Sebuah GSI pada email juga tak menyelamatkan Anda. GSI eventually
consistent, jadi pembacaan yang menggerbangi penulisan Anda
bisa basi by design. Solusinya bukan pemeriksaan yang lebih cepat; ia membuat
penulisannya sendiri menolak mendarat pada nilai yang sudah dipakai.
Modelkan setiap constraint sebagai marker item
Engine sudah menegakkan satu aturan keunikan secara gratis: Anda tak bisa menulis dua item dengan key yang sama. Jadi encode setiap aturan keunikan sebagai sebuah key.
Di samping item akun asli, tulis satu marker item per atribut yang dilindungi. Partition key marker adalah nilai yang ber-namespace. Jika nilainya dipakai, key-nya ada, dan sebuah put yang dijaga tak bisa menimpanya.
Untuk sebuah signup yang harus menjaga email dan username keduanya unik, tiga
item bergerak bersama — ber-key dalam tata letak single-table (lihat
single-table design):
| Item | PK | SK | Tujuan |
|---|---|---|---|
| Record akun | ACCT#a1f9c3 | PROFILE | Akun asli |
| Lock email | UNIQ#EMAIL#ada@lovelace.io | LOCK | Memesan email |
| Lock username | UNIQ#HANDLE#ada | LOCK | Memesan username |
PK akun itu sendiri adalah id yang dihasilkan (ACCT#a1f9c3) — bukan email —
sehingga pengguna bisa mengubah email mereka nanti tanpa menulis ulang primary
key. Item lock tak membawa data profil; mereka ada semata agar key mereka
terisi.
Tulis ketiganya secara atomik
TransactWriteItems
menerapkan hingga 100 penulisan sebagai satu unit all-or-nothing. Jaga setiap put
dengan attribute_not_exists(PK) agar ia gagal jika key itu sudah hadir.
Jika satu kondisi gagal — lock email, lock handle, atau akunnya sendiri —
DynamoDB menggulung balik seluruh transaksi dan melempar
TransactionCanceledException. Tak ada signup parsial, tak ada lock yatim.
{
"TransactItems": [
{
"Put": {
"TableName": "accounts",
"Item": {
"PK": {"S": "ACCT#a1f9c3"},
"SK": {"S": "PROFILE"},
"email": {"S": "ada@lovelace.io"},
"username": {"S": "ada"}
},
"ConditionExpression": "attribute_not_exists(PK)"
}
},
{
"Put": {
"TableName": "accounts",
"Item": {
"PK": {"S": "UNIQ#EMAIL#ada@lovelace.io"},
"SK": {"S": "LOCK"}
},
"ConditionExpression": "attribute_not_exists(PK)"
}
},
{
"Put": {
"TableName": "accounts",
"Item": {
"PK": {"S": "UNIQ#HANDLE#ada"},
"SK": {"S": "LOCK"}
},
"ConditionExpression": "attribute_not_exists(PK)"
}
}
]
}Kondisi itu adalah seluruh mekanismenya. Tanpa attribute_not_exists, sebuah
signup kedua dengan email yang sama diam-diam menimpa lock pertama. Dengannya,
put-nya menolak, transaksinya batal, dan aplikasi Anda memunculkan "email sudah
dipakai."
Membangun ConditionExpression dan value map dengan tangan adalah tempat salah
ketik menyelinap. DynamoDB Expression
Builder mengeluarkan kondisi dan Item
bertipe untuk setiap put sehingga Anda bisa menempel sebuah transaksi yang benar
langsung ke panggilan SDK Anda.
Baca kegagalannya, jangan menebaknya
Saat transaksi dibatalkan, DynamoDB mengembalikan sebuah array
CancellationReasons secara posisional — satu entri per item, dalam urutan
permintaan. Sebuah ConditionalCheckFailed di slot 1 berarti email-nya dipakai;
slot 2 berarti username-nya. Petakan slot-nya kembali ke sebuah error tingkat-field
yang presisi alih-alih sebuah "signup gagal" generik.
Periksa lock di DynoTable
Marker item tak terlihat di UI aplikasi Anda — mereka adalah plumbing. Saat sebuah signup gagal secara misterius, Anda perlu melihat apakah lock-nya benar-benar ada.
Buka tabel di DynoTable dan Query prefix UNIQ#. Akun dan dua item lock-nya
duduk bersama, jadi sebuah signup yang macet (sebuah lock yang tertinggal oleh
delete yang gagal) jelas pada pandangan sekilas.

Jaga lock tetap jujur saat ubah dan hapus
Lock bukan write-once. Mereka mencerminkan nilai yang hidup, jadi siklus hidupnya harus menjaga mereka tetap sinkron — setiap operasi yang menyentuh atribut yang dilindungi adalah sebuah transaksi juga.
- Ubah email. Satu transaksi: put lock
UNIQ#EMAIL#…baru denganattribute_not_exists, hapus lock lama, perbarui akunnya. Jaminan all-or-nothing yang sama. - Hapus akun. Hapus item akun dan kedua item lock dalam satu transaksi, atau Anda akan menelantarkan sebuah lock yang memblokir nilai itu selamanya.
- Retry dengan aman. Lewatkan sebuah
ClientRequestTokenagar sebuah transaksi yang dikirim ulang (setelah sebuah blip jaringan) idempoten alih-alih sebuah penulisan ganda.
Jebakannya adalah memperlakukan lock sebagai fire-and-forget. Sebuah lock yang dibuat saat signup tapi tak pernah dihapus saat penghapusan akun adalah sebuah nilai yang tak seorang pun bisa pakai ulang — dan itu tak akan muncul sampai seorang pengguna asli tak bisa mengklaim handle lamanya sendiri.
Langkah berikutnya
Marker keunikan adalah pola single-table, jadi mereka duduk secara alami di
samping item lain Anda — baca single-table
design untuk tata letak key-nya, dan Query vs
Scan agar Anda tak pernah menjangkau sebuah Scan untuk
memeriksa sebuah lock. Polanya pertama kali ditelusuri dalam sesi AWS re:Invent /
AWS Summit 2018 DAT374 — DynamoDB Transactions.
Draf put yang dijaga-kondisi dengan DynamoDB Expression Builder, lalu coba DynoTable untuk memeriksa item lock terhadap tabel Anda sendiri.


