• Akademia
  • Blog
  • O Serverless
  • O stronie

DynamoDB: strategia wska藕nika


DynamoDB: strategia wska藕nika

Przej艣cie z relacyjnych bazy danych do 艣wiata NoSQL nie jest 艂atwe. Dos艂ownie wszystko jest inne, co czasami mo偶e przyt艂acza膰, a偶 do tego stopnia, 偶e nie wiadomo jak rozwi膮za膰 problem tak trywialny, 偶e praktycznie nieistniej膮cy w 艣wiecie SQLa.

Dzi艣 o takim problemie chce Ci opowiedzie膰. Wyobra藕 sobie, 偶e w swojej relacyjnej bazie danych przechowujesz w tabeli rekordy zam贸wie艅. I potrzebujesz informacji o ostatnim (najnowszym) numerze zam贸wienia. (Po co to ju偶 osobny temat.)

Ostatni numer zam贸wienia w SQL

W SQLu problem jest tak prosty do rozwi膮zania, 偶e a偶 trywialny. Wystarczy napisa膰:

1
SELECT orderId FROM Orders ORDER BY orderId DESC LIMIT 1;

Nie do艣膰, 偶e zapytanie jest proste to i bardzo wydajne (szczeg贸lnie gdy mamy indeksy). Brawo dla SQLa 馃憦

Ostatni numer zam贸wienia w DynamoDB

I tutaj zaczynaj膮 si臋 schody. Nie podam Ci od razu rozwi膮zania, gdy偶 do pe艂nego zrozumienia wymaga ono pewnej wiedzy na temat tego, jak DynamoDB jest zbudowane i dlaczego pewne sposoby pracy z nim s膮 niepo偶膮dane.

Troch臋 teorii o DynamoDB

Zacznijmy od tego, 偶e odpowiednikiem SQLowego SELECT w DynamoDB s膮 dwa polecenia scan i query. Oba s艂u偶膮 do pobierania informacji z tabeli (w DynamoDB mamy tylko koncept tabeli, a nie ca艂ej bazy danych). Jednak r贸偶ni膮 si臋 one znamiennie od siebie. Metoda scan skanuje zawarto艣膰 ca艂ej tabeli i zwraca j膮 nam w postaci kolekcji element贸w.

Dla poni偶szej tabeli scan by zwr贸ci艂 6 element贸w bo mamy sze艣膰 zam贸wie艅.
Tabela danych w DynamoDB

Korzystaj膮c ze scan mogliby艣my pobra膰 informacje o wszystkich zam贸wieniach i po stronie kodu np. w funkcji Lambda, przefiltrowa膰 dane i wybra膰 najnowszy (ostatni) numer zam贸wienia.

Takie podej艣cie jest oczywi艣cie najgorszym mo偶liwym, gdy偶 pobieramy z bazy du偶o wi臋cej danych ni偶 chcemy (kolekcja zam贸wie艅 vs pojedynczy numer zam贸wienia). To wp艂ynie na czas dzia艂ania naszej aplikacji, ale te偶 na koszty, gdy偶 w DynamoDB p艂acimy za ka偶de zapytanie / ilo艣膰 danych zwr贸conych. Na domiar z艂ego, w przypadku du偶ej ilo艣ci danych dochodzi jeszcze page鈥檕wanie wynik贸w.

Rozwa偶my zatem inne opcje.

Metoda query s艂u偶y do pobierania danych z kolekcji lokalnej, czyli element贸w w bazie, kt贸re maj膮 wsp贸lny (ta sama warto艣膰) klucz Partition Key.

Staram si臋, aby ten artyku艂 by艂 zwi臋z艂y oraz sp贸jny, dlatego nie zamierzam tutaj omawia膰 budowy DynamoDB. Natomiast bardzo Ci臋 zach臋cam do poznania jej na w艂asn膮 r臋k臋, gdy偶 to po prostu dobra szko艂a architektoniczna i pomo偶e Ci w zrozumieniu, dlaczego z tej bazy danych korzysta si臋 w taki, a nie inny spos贸b.

W naszej tabeli z zam贸wieniami (powy偶ej) ka偶de zam贸wienie ma inn膮 warto艣膰 Partition Key, dlatego u偶ycie metody query po prostu nie ma tutaj sensu, gdy偶 nie mamy kolekcji lokalnych.

Na marginesie, przyk艂adem kolekcji lokalnej do zam贸wienia mog艂aby by膰 lista towar贸w w zam贸wieniu. W takim wypadku ka偶dy klucz g艂贸wny elementu Primary Key by si臋 sk艂ada艂 z dw贸ch warto艣ci (klucz kompozytowy):

  1. Partition Key
  2. Sort Key

Tabela by si臋 zmienia艂a jak na poni偶szym przyk艂adzie, wtedy wywo艂anie query z parametrem zamowienie#2 zwr贸ci nam dwa elementy, poniewa偶 w tym zam贸wieniu kto艣 kupi艂 dwa produkty: Szkolenie DataLake i Konsultacje.
Tabela danych w DynamoDB

To jest jednak inna strategia dost臋pu do danych w DynamoDB i tylko przy okazji j膮 tutaj Tobie przedstawi艂em.

Zatem, co mo偶emy zrobi膰 skoro ani scan ani query si臋 nie nadaj膮, aby zwr贸ci膰 ostatni / najnowszy numer zam贸wienia?

Metoda GetItem

Istnieje jeszcze GetItem, metoda kt贸ra zwraca nam pojedynczy element z bazy po podaniu konkretnej warto艣ci Primary Key. To takie getById(id) lub SQLowe:

1
SELECT * FROM Orders WHERE orderId = 'numerZamowienia';

Wszystko pi臋knie, tylko sk膮d mamy wzi膮膰 ten numerZamowienia skoro w艂a艣nie jego chcemy z bazy wyci膮gn膮膰? 馃

Strategia wska藕nika

W tym miejscu w艂a艣nie pojawia si臋 tytu艂owa strategia wska藕nika.

Warto艣膰 nieznan膮 mo偶emy zast膮pi膰 czym艣 z g贸ry znanym, pewn膮 sta艂膮 i przez ni膮 si臋 odwo艂ywa膰 do bazy. Ten element o sta艂ym Partition Key b臋dzie tylko jeden w ca艂ej tabeli i dlatego b臋dziemy go mogli wykorzysta膰 jako wska藕nik do przechowywania warto艣ci najnowszego zam贸wienia.

Do naszej pierwszej tabeli, dodajemy kolejny element o Partition Key wynosz膮cym zawsze LAST_ORDER (nasza sta艂a). Taki element ma jeden atrybut o nazwie OrderId z warto艣ci膮 najnowszego zam贸wienia. Za ka偶dym razem, gdy dodajemy do tabeli nowe zam贸wienie, aktualizujemy r贸wnie偶 warto艣膰 elementu LAST_ORDER na warto艣膰 nowego zam贸wienia.

Tabela danych w DynamoDB

Natomiast na poziomie zapytania, wywo艂ujemy proste getItem('LAST_ORDER'), kt贸re zwraca nam w optymalny spos贸b najwi臋kszy numer zam贸wienia.

Zaawansowane techniki

Bior膮c pod uwag臋, 偶e tabela DynamoDB raczej nie istnieje sama dla siebie i jest zwykle cz臋艣ci膮 wi臋kszego systemu zastan贸wmy si臋, co si臋 stanie, gdy do bazy zapisuje r贸wnolegle wiele proces贸w (bezpo艣rednio lub przez kolejk臋 SQS - bez r贸偶nicy). Na pewno zdarzy si臋 sytuacja, 偶e ostatnio zapisany element do bazy, wcale nie b臋dzie najnowszym (ostatnim) zam贸wieniem. W贸wczas wska藕nik LAST_ORDER b臋dzie b艂臋dnie wskazywa艂 na starsze zam贸wienie.

Ten problem mogliby艣my rozwi膮za膰 przez zastosowanie kolejki SQS FIFO, ale jest na to du偶o prostsze i ta艅sze rozwi膮zanie.

Wystarczy, 偶e zastosujemy wyra偶enie warunkowe ConditionExpression przy zapisie nowej wersji elementu LAST_ORDER, kt贸re sprawdzi, czy nowa warto艣膰 orderId jest wi臋ksza od obecnie zapisanej w bazie. Je艣li jest to zaktualizuje, je艣li nie to nie.

Dzi臋ki temu, pos艂uguj膮c si臋 jednym zapisem do bazy, mo偶emy zaktualizowa膰 warto艣膰 bez jej uprzedniego pobierania i sprawdzania po stronie kodu. Dodatkowo jest to metoda idempotentna (w przypadku gdy dostaniemy wielokrotnie ten sam event, nie zmieni stanu bazy przy kolejnych wywo艂aniach).

Jak to zrobi膰?

Pora na troch臋 kodu. Przyk艂adowa implementacja w JavaScript wygl膮da nast臋puj膮co:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
async createPointer(orderId) {
const pointer = new Pointer({ orderId })
const params = {
Item: pointer.toItem(),
ReturnConsumedCapacity: 'TOTAL',
TableName: process.env.ordersTableName,
ConditionExpression: 'attribute_not_exists(#orderId) OR #orderId < :newId',
ExpressionAttributeNames: {
'#orderId': 'orderId'
},
ExpressionAttributeValues: {
':newId': { N: `${orderId}` }
}
}
log('createPointer params', params)
try {
await this.dynamoDbAdapter.create(params)
} catch (error) {
if (error.code === 'ConditionalCheckFailedException') {
log(`LAST_ORDER pointer already exists and is greater than ${orderId}. Skipping update.`)
} else {
log('Error', error)
throw error
}
}
return pointer
}

Om贸wienie kodu:

  • Linijka 4 - klasa Pointer implementuje metod臋, ktora zamienia obiekt Pointer na JSONa, kt贸rego oczekuje API DynamoDB. Poni偶ej implementacja tej metody.
  • Linijka 7 - wyra偶enie warunkowe: kiedy zapisa膰 do bazy, a kiedy nie? attribute_not_exists(#orderId) jest potrzebne, aby przy pustej bazie kod te偶 si臋 wykona艂 i po raz pierwszy zapisa艂 element. Od tego momentu tylko drugi cz艂on #orderId < :newId warunku b臋dzie mia艂 znaczenie.
  • Linijka 19 - je艣li warunek nie zosta艂 spe艂niony, to API DynamoDB zwraca wyj膮tek ConditionalCheckFailedException, kt贸ry w naszym wypadku jest pr臋dzej czy p贸藕niej oczekiwany.

A tutaj obiecana implementacja klasy Pointer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Pointer {
constructor({ orderId, createdAt = new Date() }) {
this.orderId = parseInt(orderId)
this.createdAt = createdAt instanceof Date ? createdAt : new Date(createdAt)
}

key() {
return {
PK: { S: 'LAST_ORDER' }
}
}

static fromItem(item) {
return new Pointer({
orderId: item.orderId.N,
createdAt: item.createdAt.S
})
}

toItem() {
return {
...this.key(),
orderId: { N: this.orderId.toString() },
createdAt: { S: this.createdAt.toISOString() },
}
}
}

Uwaga, styl tej klasy jest bezczelnie zer偶ni臋ty od Alexa DeBrie 馃槂

Je艣li jeste艣 zapisany(na) do mojego newslettera Serverless Polska to na pewno wiesz, 偶e wielokrotnie pisa艂em o nim i jego ksi膮偶ce The DynamoDB Book. To najlepsza pozycja na rynku do nauki tej bazy jaka istnieje!

Je艣li nie czytasz mojego newslettera to mo偶esz si臋 zapisa膰 tutaj - gor膮co Ci臋 do tego zach臋cam!

Poza cotygodniow膮 dawk膮 informacji na temat serverless i AWS, otrzymasz zawsze najlepsze oferty na materia艂y do nauki. Przyk艂adowo, dla czytelnik贸w Serverless Polska za艂atwi艂em kupon zni偶kowy na ksi膮偶k臋 Alexa. Przy zam贸wieniu, w pole Discount code wpisz serverlesspolska. Uwaga: to nie akcja afiliacyjna - nic na tym nie zarabiam, polecam z czystym 鉂わ笍 bo to niespotykanie dobra ksi膮偶ka, kt贸ra nauczy艂a mnie jak korzysta膰 z DynamoDB.

Podsumowanie

Mam nadziej臋, 偶e ten uproszczony, ale wzi臋ty z mojego rzeczywistego systemu, przyk艂ad pom贸g艂 Ci zrozumie膰 zawi艂o艣ci korzystania z DynamoDB. Om贸wi艂em tylko ma艂y obszar wiedzy, ale r贸wnocze艣nie pokaza艂em, jak rozwi膮za膰 konkretny problem, kt贸ry mo偶esz napotka膰 w swoich projektach.

Mam 艣wiadomo艣膰, 偶e pr贸g wej艣cia w DynamoDB jest wysoki - nie ma co tego ukrywa膰 - ale szczerze, nie wyobra偶am sobie system贸w serverless bez bazy DynamoDB. W 9 na 10 przypadk贸w wybior臋 w艂a艣nie DynamoDB ponad AWS RDS (wliczaj膮c w to Auror臋 Serverless).