Menengah7 menit baca

Relasi Many-to-Many di DynamoDB

Seorang student mendaftar di banyak course; sebuah course menampung banyak student. Di SQL Anda meraih join table dan sebuah JOIN empat arah.

DynamoDB tidak punya join, jadi relasinya harus hidup di dalam key — dan triknya adalah menyimpan setiap edge enrollment dalam bentuk yang bisa di-Query langsung oleh kedua sisi.

Panduan ini menelusuri masalah student ↔ course dari ujung ke ujung: pola akses, pola adjacency-list yang menyelesaikannya, sebuah key schema orisinal yang bisa Anda salin, dan cara membaca kembali kedua arah tanpa pernah men-scan tabel.

Bagaimana cara memodelkan relasi many-to-many di DynamoDB?

DynamoDB tidak memiliki join, sehingga Anda memodelkan relasi many-to-many dengan pola adjacency-list: simpan setiap tautan sebagai Item edge tersendiri yang diberi key dari salah satu sisi, lalu tambahkan GSI terbalik yang menukar key tersebut. Satu edge, ditulis sekali, lalu menjawab query dari kedua arah dengan murah.

  • Simpan setiap enrollment sebagai Item edge tersendiri, bukan atribut list pada salah satu sisi.
  • Beri key edge berdasarkan student (PK = STU#…, SK = ENROLL#CRS#…) agar satu Query mengembalikan seluruh daftar course seorang student.
  • Tambahkan GSI terbalik yang menukar peran (GSI1PK = CRS#…) agar edge yang sama juga menjawab "siapa saja yang ada di course ini?".
  • Satu edge, ditulis sekali, dibaca murah dua arah — itulah seluruh permainannya.

Bingkai pola aksesnya lebih dulu

Pemodelan DynamoDB mengutamakan pola akses: Anda menentukan pembacaan sebelum memilih satu nama atribut pun. Relasi many-to-many hampir selalu punya dua pembacaan simetris ditambah lookup entitas:

  • Ambil profil seorang student, dan daftar setiap course yang diikuti student itu.
  • Ambil metadata sebuah course, dan daftar setiap student yang terdaftar di course itu.
  • Cari satu edge enrollment tunggal — untuk memperbarui nilai atau keluar dari course.

Rasa sakitnya: kedua pembacaan daftar itu menunjuk ke arah berlawanan melintasi himpunan edge yang sama. Desain naif melayani salah satunya dengan murah dan memaksa sebuah Scan untuk yang lain — footgun persis yang dibahas dalam Query vs Scan.

Tugasnya adalah membuat kedua arah menjadi satu Query.

Gunakan pola adjacency-list

Panduan DynamoDB sendiri untuk relasi adalah adjacency list: modelkan setiap relasi sebagai Item yang partition keynya adalah satu endpoint dan sort keynya adalah endpoint lainnya.

AWS mendokumentasikan ini pada halaman Best Practices for Managing Many-to-Many Relationships dari DynamoDB Developer Guide.

Mengapa key dan bukan tabel kedua? Karena primitif yang DynamoDB berikan adalah sebuah Query terhadap satu partition.

Sebuah Query membaca rentang nilai sort-key yang menyatu di bawah satu partition key dalam satu operasi yang ditagih — itulah satu-satunya "join" yang ditawarkan mesin ini.

Untuk mendapatkan relasi yang dibaca murah dari kedua sisi, Anda menggandakan edge: tulis sekali dengan key student, lalu gunakan secondary index untuk memproyeksikan edge yang sama dengan key course.

Ini adalah pemikiran overloaded-key dari Single-Table Design, diterapkan pada sebuah relasi alih-alih hierarki induk-anak.

Bentuknya adalah dua tampilan bertumpuk dari edge yang sama — tabel dasar berkey student, GSI terbalik berkey course:

GSI1 terbalik berkey courseTabel dasar berkey studentedge sama, key ditukaredge sama, key ditukarPK STU#a91SK ENROLL#CRS#math204PK STU#a91SK ENROLL#CRS#cs101GSI1PK CRS#math204GSI1SK STU#a91GSI1PK CRS#cs101GSI1SK STU#a91

Setiap edge ditulis sekali pada tabel dasar dan diproyeksikan ke dalam GSI dengan keynya ditukar, sehingga sebuah Query terhadap partition mana pun membaca relasi dengan murah.

Garis keturunannya kembali ke makalah Amazon Dynamo paper tahun 2007: partition key adalah satuan distribusi, dan akses single-key adalah jalur cepat.

Relasi di DynamoDB adalah latihan membengkokkan pembacaan many-to-many ke jalur cepat itu.

Kerjakan contohnya: student ↔ course

Gunakan satu tabel dengan key generik, PK dan SK, dan kodekan tipe entitas di dalam nilainya. Edge enrollment adalah jantungnya:

PKSKattributes
STU#a91PROFILEname, year, major
STU#a91ENROLL#CRS#math204 enrolledOn, grade
STU#a91ENROLL#CRS#cs101enrolledOn, grade
CRS#math204METADATAtitle, credits, term
CRS#cs101METADATAtitle, credits, term

Satu Query PK = "STU#a91" mengembalikan profil student dan setiap enrollment dalam satu pembacaan. Persempit dengan SK begins_with "ENROLL#" untuk mendapatkan hanya edge course saja. Itu menyelesaikan "daftar course seorang student".

Tetapi "daftar student sebuah course" menunjuk ke arah lain — dan tabel dasar tak bisa menjawabnya, karena id student ada di partition key, bukan sort key.

Tambahkan sebuah global secondary index terbalik yang menukar peran. Beri Item edge pasangan GSI1PK/GSI1SK generik yang menampung course di sisi partition dan student di sisi sort:

PKSKGSI1PKGSI1SK
STU#a91ENROLL#CRS#math204CRS#math204STU#a91
STU#b30ENROLL#CRS#math204CRS#math204STU#b30
STU#a91ENROLL#CRS#cs101CRS#cs101STU#a91

Kini Query GSI1 WHERE GSI1PK = "CRS#math204" mendaftar setiap student di course itu — pembacaan yang tak bisa dilayani tabel dasar. Satu Item edge, ditulis sekali, menjawab kedua arah.

Ia harus berupa GSI, bukan LSI: partition course sepenuhnya berbeda dari partition student, dan sebuah LSI berbagi partition key tabel dasar.

Index ini membentang banyak partition, jadi ia harus global — lihat GSI vs LSI.

Satu catatan: GSI di DynamoDB diisi secara asinkron. Sebuah enrollment yang betul-betul baru bisa butuh sesaat untuk muncul di arah CRS#….

Perlakukan pembacaan roster course sebagai eventually consistent — yang ditegaskan Developer Guide secara eksplisit untuk global secondary index.

Tulis dan baca di DynoTable

Menulis enrollment berarti menyetel empat atribut key plus data edge itu sendiri. Kondisi yang mencegah seorang student mendaftar dua kali di course yang sama adalah guard attribute_not_exists(PK) pada composite key.

Itu persis jenis kondisi yang bisa Anda rakit secara visual dengan DynamoDB Expression Builder alih-alih menulis tangan ExpressionAttributeNames dan nilai placeholder.

Di DynoTable Anda mengarahkan sebuah Query ke GSI1, menyetel GSI1PK = "CRS#math204", dan rosternya kembali sebagai tabel yang bisa Anda baca, urutkan, dan edit di tempat — kedua arah relasi dapat ditelusuri dari satu schema.

Mem-query GSI terbalik di DynoTable untuk mendaftar setiap student yang terdaftar di sebuah course.
Mem-query GSI terbalik di DynoTable untuk mendaftar setiap student yang terdaftar di sebuah course.

Jebakan dan langkah berikutnya

  • Jangan simpan salah satu sisi sebagai atribut list. Sebuah array courseIds pada Item student terasa rapi sampai sebuah course butuh rosternya, array menyentuh batas Item 400 KB, atau dua enrollment berlomba dan saling menimpa. Item edge diskret menskala dan diperbarui secara independen.
  • Jaga data edge tetap di edge. grade dan enrolledOn enrollment berada di Item edge, tidak digandakan ke student atau course — ada tepat satu baris per pasangan (student, course) yang diperbarui.
  • Perhatikan propagasi GSI. Arah inverted-index bersifat eventually consistent, jadi pembacaan tepat setelah enrollment bisa tertinggal sepersekian detik.
  • Proyeksikan hanya yang dibutuhkan roster. Proyeksi KEYS_ONLY atau yang sempit menjaga GSI tetap kecil saat tampilan roster hanya butuh id.

Untuk mendalami pola-pola di sekelilingnya, baca Single-Table Design untuk overloaded key dan GSI vs LSI untuk kapan inverted index harus global.

Lalu unduh DynoTable untuk memodelkan schema student ↔ course secara nyata — tulis edge-nya, bangun kondisi dengan Expression Builder, dan query kedua arah relasi tanpa satu scan pun.

Diperbarui