DynamoDB
Imaginemos una aplicacion de Propietarios que tienen pisos y los alquilan a inquilinos. Un Propietario puede tener varios pisos.
Conceptos basicos para diseñar una table
Al igual que en kafka, para escalar horizontalmente Dynamo guarda los elementos en base a su clave de particion.
Partition KEY
Buscador basico usando PropietarioId
En un sistema de jerarquia tradicional de bbdd relacional, buscariamos al propietario y de ahi buscariamos los pisos que le corresponden. Es decir creamos un registro con PK PropietarioID y buscamos por el, cuando obtenemos el body leemos los pisos y buscamos.
Desventajas: Tendriamos una query para el propietario y N querys por cada piso.
Partition KEY + Global secondary index
Arrancando DynamoDB
Creamos la tabla en la dynamo local
aws dynamodb create-table \
--table-name Shops \
--billing-mode PAY_PER_REQUEST \
--attribute-definitions \
AttributeName=ShopId,AttributeType=S \
AttributeName=OwnerId,AttributeType=S \
AttributeName=CreatedAt,AttributeType=S \
--key-schema \
AttributeName=ShopId,KeyType=HASH \
--global-secondary-indexes '[
{
"IndexName": "OwnerIdIndex",
"KeySchema": [
{"AttributeName": "OwnerId", "KeyType": "HASH"},
{"AttributeName": "CreatedAt", "KeyType": "RANGE"}
],
"Projection": {"ProjectionType": "ALL"}
}
]' \
--endpoint-url http://localhost:8000
Creamos la primera tienda (La tienda no puede existir si no hay propietarios, por eso la clave nos obliga a apuntarlo.)
aws dynamodb put-item --table-name Shops \
--item '{
"ShopId": {"S": "shop-001"},
"OwnerId": {"S": "owner-123"},
"CreatedAt": {"S": "2024-03-15T10:30:00Z"},
"Name": {"S": "My Coffee Shop"},
"Address": {"S": "123 Main St"},
"Active": {"BOOL": true}
}' \
--endpoint-url http://localhost:8000
DynamoDB type descriptors
┌────────────┬─────────────────────┬─────────────────────────────────┐ │ Descriptor │ Type │ Example │ ├────────────┼─────────────────────┼─────────────────────────────────┤ │ S │ String │ {“S”: “hello”} │ ├────────────┼─────────────────────┼─────────────────────────────────┤ │ N │ Number │ {“N”: “42”} │ ├────────────┼─────────────────────┼─────────────────────────────────┤ │ BOOL │ Boolean │ {“BOOL”: true} │ ├────────────┼─────────────────────┼─────────────────────────────────┤ │ NULL │ Null │ {“NULL”: true} │ ├────────────┼─────────────────────┼─────────────────────────────────┤ │ L │ List │ {“L”: [{“S”: “a”}, {“N”: “1”}]} │ ├────────────┼─────────────────────┼─────────────────────────────────┤ │ M │ Map (nested object) │ {“M”: {“key”: {“S”: “val”}}} │ ├────────────┼─────────────────────┼─────────────────────────────────┤ │ SS │ String Set │ {“SS”: [“a”, “b”]} │ ├────────────┼─────────────────────┼─────────────────────────────────┤ │ NS │ Number Set │ {“NS”: [“1”, “2”]} │ └────────────┴─────────────────────┴─────────────────────────────────┘
La busqueda basica por PK seria por shopId
aws dynamodb get-item \
--table-name Shops \
--key '{"ShopId": {"S": "shop-001"}}' \
--endpoint-url http://localhost:8000
Creamos un par de tiendas mas
aws dynamodb put-item --table-name Shops \
--item '{
"ShopId": {"S": "shop-002"},
"OwnerId": {"S": "owner-123"},
"CreatedAt": {"S": "2024-03-15T10:30:00Z"},
"Name": {"S": "My Cool Shop"},
"Address": {"S": "000 Main St"},
"Active": {"BOOL": true}
}' \
--endpoint-url http://localhost:8000
aws dynamodb put-item --table-name Shops \
--item '{
"ShopId": {"S": "shop-003"},
"OwnerId": {"S": "owner-123"},
"CreatedAt": {"S": "2024-03-15T10:30:00Z"},
"Name": {"S": "My third shop"},
"Address": {"S": "003 Main St"},
"Active": {"BOOL": true}
}' \
--endpoint-url http://localhost:8000
Y creamos tiendas para u nuevo Owner 456
aws dynamodb put-item --table-name Shops \
--item '{
"ShopId": {"S": "owner456-shop1"},
"OwnerId": {"S": "owner-456"},
"CreatedAt": {"S": "2024-03-15T10:30:00Z"},
"Name": {"S": "My Amazing shop"},
"Address": {"S": "0066 Main St"},
"Active": {"BOOL": true}
}' \
--endpoint-url http://localhost:8000
aws dynamodb put-item --table-name Shops \
--item '{
"ShopId": {"S": "owner456-shop2"},
"OwnerId": {"S": "owner-456"},
"CreatedAt": {"S": "2024-03-15T10:30:00Z"},
"Name": {"S": "My Second Amazing shop"},
"Address": {"S": "0456066 Main St"},
"Active": {"BOOL": true}
}' \
--endpoint-url http://localhost:8000
aws dynamodb put-item --table-name Shops \
--item '{
"ShopId": {"S": "owner456-shop3"},
"OwnerId": {"S": "owner-456"},
"CreatedAt": {"S": "2022-03-15T10:30:00Z"},
"Name": {"S": "My Legacy shop"},
"Address": {"S": "0456066 Main St"},
"Active": {"BOOL": true}
}' \
--endpoint-url http://localhost:8000
Busqueda Sencilla por Owner
Vamos a buscar por el Owner (es decir un listado de todas las tiendas del Owner 1) Usamos el parametro –index-name para decirle que busque por el GSI en vez del primary key (ShopId)
aws dynamodb query --table-name Shops \
--index-name OwnerIdIndex \
--key-condition-expression "OwnerId = :oid" \
--expression-attribute-values '{":oid": {"S": "owner-123"}}' \
--endpoint-url http://localhost:8000
Busqueda usando ordenacion SortKeys (–scan-index-forward)
Vamos a listar por Owner y que nos filtre por createdAd para ello usamos una de las 2 opciones:
Esto funciona porque tenemos un atributo de tipo RANGEPor defecto este parametro (–scan-index-forward) y nos devolvera los elementos siguiendo el orden natural del rango, asi que nos devolvera los que primero creamos antes. Con el parametro false hace las busqueda inversa en el rango (–no-scan-index-forward), es decir empieza por el final, es muy util por ejemplo para listados de citas, facturas, etc..
aws dynamodb query --table-name Shops --index-name OwnerIdIndex --scan-index-forward --key-condition-expression "OwnerId = :oid" --expression-attribute-values '{":oid": {"S": "owner-456"}}' --endpoint-url http://localhost:8000
aws dynamodb query --table-name Shops --index-name OwnerIdIndex --no-scan-index-forward --key-condition-expression "OwnerId = :oid" --expression-attribute-values '{":oid": {"S": "owner-456"}}' --endpoint-url http://localhost:8000
CONTROL AVANZANDO SOBRE EL RANGO
Los parametros RANGE pordemos usarlos en query avanzada
aws dynamodb query \
--table-name Shops \
--index-name OwnerIdIndex \
--key-condition-expression "OwnerId = :oid AND CreatedAt BETWEEN :from AND :to" \
--expression-attribute-values '{
":oid": {"S": "owner-456"},
":from": {"S": "2024-01-01T00:00:00Z"},
":to": {"S": "2024-12-31T23:59:59Z"}
}' \
--endpoint-url http://localhost:8000
aws dynamodb query \
--table-name Shops \
--index-name OwnerIdIndex \
--key-condition-expression "OwnerId = :oid AND CreatedAt BETWEEN :from AND :to" \
--expression-attribute-values '{
":oid": {"S": "owner-456"},
":from": {"S": "2021-01-01T00:00:00Z"},
":to": {"S": "2023-12-31T23:59:59Z"}
}' \
--endpoint-url http://localhost:8000
EXPERIMENTAL
Vamos a crear una tabla con 2 o mas GSI para ver si lo sporta
tabla Tenant, es lo mismo pero a parte del OwnerId como GSI vamos a poner tambien GSI country
aws dynamodb create-table --table-name ShopsGEO --billing-mode PAY_PER_REQUEST \
--attribute-definitions \
AttributeName=ShopId,AttributeType=S \
AttributeName=OwnerId,AttributeType=S \
AttributeName=CreatedAt,AttributeType=S \
AttributeName=GeohashPrefix,AttributeType=S \
AttributeName=Geohash,AttributeType=S \
--key-schema \
AttributeName=ShopId,KeyType=HASH \
--global-secondary-indexes '[
{
"IndexName": "OwnerIdIndex",
"KeySchema": [
{"AttributeName": "OwnerId", "KeyType": "HASH"},
{"AttributeName": "CreatedAt", "KeyType": "RANGE"}
],
"Projection": {"ProjectionType": "ALL"}
},
{
"IndexName": "GeohashIndex",
"KeySchema": [
{"AttributeName": "GeohashPrefix", "KeyType": "HASH"},
{"AttributeName": "Geohash", "KeyType": "RANGE"}
],
"Projection": {"ProjectionType": "ALL"}
}
]' \
--endpoint-url http://localhost:8000
CREAMOS LA PRIMERA TIENDA: Usamos la funcion geohash que hay varias libs para convertir latitude y longitude en geohash http://geohash.co/
Madrid en este caso tiene esta latitude longitude 40.4165, -3.70256 su GEOHASH es ezjmgtxg y su prefix por lo tanto lo cogemos con poca exactitud y nos valen las 5 primeras letras ezjmg
Guadarrama tiene estas 40.6817357, -4.0909462 GEOHASH: ezjp6vvh (vemos que el prefix es el mismo)
Inertamos la tienda en guadarrama
aws dynamodb put-item \
--table-name ShopsGEO \
--item '{
"ShopId": {"S": "shop-001"},
"OwnerId": {"S": "owner-123"},
"CreatedAt": {"S": "2024-03-15T10:30:00Z"},
"Name": {"S": "Guadarrama Shop"},
"GeohashPrefix": {"S": "ezjp"},
"Geohash": {"S": "ezjp6vvh"}
}' \
--endpoint-url http://localhost:8000
Aluche tiene estas 40.3875, -3.7542 GEOHASH : ezjmf866
aws dynamodb put-item \
--table-name ShopsGEO \
--item '{
"ShopId": {"S": "shop-002"},
"OwnerId": {"S": "owner-123"},
"CreatedAt": {"S": "2024-03-15T10:30:00Z"},
"Name": {"S": "Aluche Shop"},
"GeohashPrefix": {"S": "ezjm"},
"Geohash": {"S": "ezjmf866"}
}' \
--endpoint-url http://localhost:8000
BUSQUEDA GEO
La busqueda se produce en funcion de una localizacion dada y un radio
aws dynamodb query \
--table-name ShopsGEO \
--index-name GeohashIndex \
--key-condition-expression "GeohashPrefix = :prefix AND begins_with(Geohash, :geo)" \
--expression-attribute-values '{
":prefix": {"S": "gcpv"},
":geo": {"S": "gcpvh6"}
}' \
--endpoint-url http://localhost:8000
┌────────────────┬────────┐ │ Geohash length │ Area │ ├────────────────┼────────┤ │ 4 chars │ ~40 km │ ├────────────────┼────────┤ │ 5 chars │ ~5 km │ ├────────────────┼────────┤ │ 6 chars │ ~1 km │ ├────────────────┼────────┤ │ 7 chars │ ~150 m │ ├────────────────┼────────┤ │ 8 chars │ ~40 m │
En este caso la query no devuleve resultados porque estoy usando
“:prefix”: {“S”: “gcpv”},
“:geo”: {“S”: “gcpvh6”}
Vamos a probar desde madrid capital 40.4165, -3.7026 geohash: ezjmgtxg
Reduciendo el hash consigo que me encuentre Aluche
aws dynamodb query \
--table-name ShopsGEO \
--index-name GeohashIndex \
--key-condition-expression "GeohashPrefix = :prefix AND begins_with(Geohash, :geo)" \
--expression-attribute-values '{
":prefix": {"S": "ezjm"},
":geo": {"S": "ezj"}
}' \
--endpoint-url http://localhost:8000
Vamos a ver si coge las 2 quitando y poniendo el prefijo mas corto, ya que guadarrama me di cuenta de que tiene otro prefix “ezjp”, asi que vamos con menos prefix
aws dynamodb query \
--table-name ShopsGEO \
--index-name GeohashIndex \
--key-condition-expression "GeohashPrefix = :prefix AND begins_with(Geohash, :geo)" \
--expression-attribute-values '{
":prefix": {"S": "ezjp"},
":geo": {"S": "ezjp"}
}' \
--endpoint-url http://localhost:8000
HE reducido pero no encontraba, he tenido que afinar el prefix, debe ser que con un prefix de 3 caracteres no busca ni lo intenta, es demasiado ambiguo