DynamoDB'de Çoka-Çok İlişkiler
Bir öğrenci birçok kursa kaydolur; bir kurs birçok öğrenciyi barındırır. SQL'de bir
join tablosu ve dört yönlü bir JOIN'e uzanırsın.
DynamoDB'nin join'leri yoktur, bu yüzden ilişkinin anahtarlarda yaşaması
gerekir — ve püf nokta, her kayıt kenarını her iki tarafın da doğrudan Query
edebileceği bir şekilde saklamaktır.
Bu kılavuz öğrenciler ↔ kurslar problemini baştan sona işler: erişim desenleri, bunları çözen komşuluk-listesi deseni, kopyalayabileceğin özgün bir anahtar şeması ve tabloyu hiç taramadan her iki yönü nasıl geri okuyacağın.
DynamoDB'de çoka-çok ilişki nasıl modellenir?
DynamoDB'nin join'leri yoktur; bu nedenle çoka-çok ilişkileri komşuluk-listesi (adjacency-list) deseniyle modelliyorsunuz: her bağlantıyı bir tarafı anahtar alan kendi kenar öğesi olarak saklayın, ardından anahtarları değiştiren ters çevrilmiş bir GSI ekleyin. Bir kez yazılan tek bir kenar, her iki yönden gelen sorguları ucuza yanıtlar.
- Her kaydı, herhangi bir tarafta bir liste attribute'u olarak değil, kendi kenar öğesi olarak sakla.
- Kenarı öğrenciye göre anahtarla (
PK = STU#…,SK = ENROLL#CRS#…), böylece tek birQuerybir öğrencinin tüm kurs listesini döndürür. - Rolleri değiştiren bir ters çevrilmiş GSI ekle (
GSI1PK = CRS#…), böylece aynı kenar "bu kursta kim var?" sorusuna da yanıt verir. - Bir kez yazılan bir kenar, her iki yönde de ucuza okunur — tüm oyun budur.
Önce erişim desenlerini çerçevele
DynamoDB modelleme erişim-deseni önceliklidir: tek bir attribute adı seçmeden önce okumalara karar verirsin. Bir çoka-çok ilişkinin neredeyse her zaman varlık aramalarına ek olarak iki simetrik okuması vardır:
- Bir öğrencinin profilini al ve o öğrencinin kayıtlı olduğu her kursu listele.
- Bir kursun meta verilerini al ve o kursa kayıtlı her öğrenciyi listele.
- Tek bir kayıt kenarını ara — bir notu güncellemek veya kurstan çıkmak için.
Sorun şu: iki liste okuması, aynı kenar kümesi boyunca zıt yönleri gösterir. Naif
bir tasarım birini ucuza sunar ve diğeri için bir Scan'e zorlar —
Query ve Scan'de işlenen tam ayak tuzağı (footgun).
Görev, her iki yönü de tek bir Query haline getirmektir.
Komşuluk-listesi desenini kullan
DynamoDB'nin ilişkiler için kendi rehberliği komşuluk listesidir (adjacency list): her ilişkiyi, partition key'i bir uç nokta ve sort key'i diğeri olan bir öğe olarak modelle.
AWS bunu DynamoDB Developer Guide'ın Best Practices for Managing Many-to-Many Relationships sayfasında belgeler.
Neden anahtarlar, ikinci bir tablo değil? Çünkü DynamoDB'nin sana verdiği ilkel
yapı, tek bir partition'a karşı bir Query'dir.
Bir Query, tek bir partition key altındaki sort-key değerlerinin bitişik bir
aralığını tek bir faturalandırılan işlemde okur — bu, motorun sunduğu tek "join"dir.
Her iki taraftan da ucuza okunan bir ilişki elde etmek için kenarı çoğaltırsın: onu bir kez öğrenciye göre anahtarlanmış olarak yaz, sonra aynı kenarı kursa göre anahtarlanmış olarak yansıtmak için bir secondary index kullan.
Bu, Single-Table Tasarım'dan gelen aşırı-yüklenmiş-anahtar düşüncesidir; bir üst-alt hiyerarşi yerine bir ilişkiye uygulanmıştır.
Şekil, aynı kenarın iki üst üste yığılmış görünümüdür — temel tablo öğrenciye göre anahtarlanmış, ters çevrilmiş GSI kursa göre anahtarlanmış:
Her kenar temel tabloya bir kez yazılır ve anahtarları takas edilmiş şekilde GSI'ye
yansıtılır, böylece her iki partition'a karşı bir Query ilişkiyi ucuza okur.
Köken 2007 Amazon Dynamo makalesine kadar uzanır: partition key dağıtım birimidir ve tek anahtarlı erişim hızlı yoldur.
DynamoDB'deki ilişkiler, çoka-çok okumaları o hızlı yola büküme egzersizidir.
Örneği işle: öğrenciler ↔ kurslar
Genel anahtarlar PK ve SK ile tek bir tablo kullan ve varlık türünü değere
kodla. Kayıt kenarı işin kalbidir:
| PK | SK | attributes |
|---|---|---|
| STU#a91 | PROFILE | name, year, major |
| STU#a91 | ENROLL#CRS#math204 enrolledOn, grade | |
| STU#a91 | ENROLL#CRS#cs101 | enrolledOn, grade |
| CRS#math204 | METADATA | title, credits, term |
| CRS#cs101 | METADATA | title, credits, term |
Tek bir Query PK = "STU#a91", öğrencinin profilini ve her kaydını tek bir
okumada döndürür. Yalnızca kurs kenarlarını almak için onu
SK begins_with "ENROLL#" ile daralt. Bu, "bir öğrencinin kurslarını listele"
sorununu çözer.
Ancak "bir kursun öğrencilerini listele" diğer yönü gösterir — ve temel tablo buna yanıt veremez, çünkü öğrenci id'si partition key'dedir, sort key'de değil.
Rolleri takas eden ters çevrilmiş bir global secondary index ekle. Kenar
öğelerine, kursu partition tarafında ve öğrenciyi sort tarafında tutan genel bir
GSI1PK/GSI1SK çifti ver:
| PK | SK | GSI1PK | GSI1SK |
|---|---|---|---|
| STU#a91 | ENROLL#CRS#math204 | CRS#math204 | STU#a91 |
| STU#b30 | ENROLL#CRS#math204 | CRS#math204 | STU#b30 |
| STU#a91 | ENROLL#CRS#cs101 | CRS#cs101 | STU#a91 |
Artık Query GSI1 WHERE GSI1PK = "CRS#math204" o kursta kayıtlı her öğrenciyi
listeler — temel tablonun sunamadığı okuma. Bir kez yazılan tek bir kenar öğesi, her
iki yöne de yanıt verir.
Bu bir LSI değil, bir GSI olmalı: kurs partition'ı öğrenci partition'ından tamamen farklıdır ve bir LSI temel tablonun partition key'ini paylaşır.
Index birden çok partition'a yayılır, bu yüzden global olmalıdır — bkz. GSI ve LSI.
Bir incelik: DynamoDB'deki GSI'ler asenkron olarak doldurulur. Yepyeni bir kayıt,
CRS#… yönünde görünmesi bir an alabilir.
Kurs listesi okumasını nihai tutarlı (eventually consistent) olarak ele al — bunu Developer Guide global secondary index'ler için açıkça belirtir.
DynoTable'da yaz ve oku
Kaydı yazmak, kenarın kendi verisine ek olarak dört anahtar attribute'u ayarlamak
demektir. Bir öğrencinin aynı kursa iki kez kaydolmasını durduran koşul, bileşik
anahtar üzerinde bir attribute_not_exists(PK) korumasıdır.
Bu tam olarak, ExpressionAttributeNames'i ve placeholder değerlerini elle yazmak
yerine DynamoDB Expression Builder ile görsel
olarak oluşturabileceğin türden bir koşuldur.
DynoTable'da bir Query'yi GSI1'e yönlendirir, GSI1PK = "CRS#math204" ayarlar
ve liste, yerinde okuyabileceğin, sıralayabileceğin ve düzenleyebileceğin bir tablo
olarak geri gelir — ilişkinin her iki yönü de tek bir şemadan taranabilir.

Tuzaklar ve sonraki adımlar
- Bir tarafı liste attribute'u olarak saklama. Öğrenci öğesi üzerinde bir
courseIdsdizisi, bir kursun listesine ihtiyaç duyana, dizi 400 KB öğe tavanına çarpana veya iki kayıt yarışıp birbirini ezene kadar düzenli görünür. Ayrık kenar öğeleri bağımsız olarak ölçeklenir ve güncellenir. - Kenar verisini kenarda tut. Kaydın
gradeveenrolledOn'u, öğrenciye veya kursa çoğaltılmış değil, kenar öğesine aittir — güncellemek için her (öğrenci, kurs) çifti başına tam olarak bir satır vardır. - GSI yayılımına dikkat et. Ters-index yönü nihai tutarlıdır, bu yüzden bir kayıttan hemen sonraki bir okuma saniyenin bir kesri kadar gecikebilir.
- Yalnızca listenin ihtiyaç duyduğunu yansıt. Liste görünümü yalnızca id'lere
ihtiyaç duyduğunda, bir
KEYS_ONLYveya dar bir yansıtma GSI'yi küçük tutar.
Çevreleyen desenlere daha derinlemesine inmek için, aşırı yüklenmiş anahtarlar için Single-Table Tasarım'ı ve ters index'in ne zaman global olması gerektiği için GSI ve LSI'yi oku.
Sonra öğrenciler ↔ kurslar şemasını gerçekten modellemek için DynoTable'ı indir — kenarları yaz, koşulu Expression Builder ile oluştur ve ilişkinin her iki yönünü de tek bir tarama yapmadan sorgula.


