Usando Bem a API do DynamoDB: Expressões e Evitando ORMs

Published on 2026-08-20·v1.0

Objective

Aprender o mecanismo por baixo de toda chamada de API do DynamoDB: expressões. "Expressões são declarações que operam nos seus itens. São meio que declarações mini-SQL", e existem cinco delas: KeyConditionExpression, FilterExpression, ProjectionExpression, ConditionExpression, e UpdateExpression, cada uma com escopo em uma operação específica e um trabalho específico. Entender as duas sintaxes de placeholder que carregam toda expressão (#name para nomes de atributo, :value para valores de atributo) e por que o DynamoDB força essa separação, em vez de deixar você escrever valores em linha. Então tomar a opinião mais afiada do livro neste par de capítulos ao pé da letra: "eu não recomendaria usar um ODM no DynamoDB", e entender as duas exceções estreitas que ele abre, porque essas exceções são onde o campo mais se moveu desde 2020.

Use Cases

  • Buscar um intervalo limitado a partir de uma sort key (pedidos entre duas datas, títulos de filme começando com uma letra) via uma KeyConditionExpression, que é o único tipo de expressão restrito a atributos de chave primária.
  • Reduzir tamanho de payload quando a condição de chave sozinha retornaria itens que você não precisa: filtrar os papéis de Tom Hanks para só os dramas com uma FilterExpression, enquanto aceita que isso não reduz o que o DynamoDB lê ou cobra.
  • Buscar itens com um atributo grande ou sensível (um blob de imagem binário, um campo de auditoria raramente necessário), mas excluir esse atributo da resposta usando uma ProjectionExpression.
  • Reforçar uma restrição de unicidade na escrita ("mais de 90% das minhas requisições PutItem incluem expressões de condição para afirmar que não existe um item existente com a mesma chave primária") via attribute_not_exists() em uma ConditionExpression.
  • Proteger um invariante de negócio no momento da escrita, sem uma leitura prévia: recusar deixar um saldo de conta cair abaixo de zero, ou limitar um contador de job em andamento a 10 com size(#inprogress) <= 10, em vez de ler o item, checar em código de aplicação, e disputar com outra requisição.
  • Verificar permissões inline com uma mutação: afirmar que um usuário está em um set Admins via contains() antes de permitir uma mudança de assinatura, ou através de dois itens diferentes de uma vez, usando um ConditionCheck dentro de uma chamada TransactWriteItems.
  • Incrementar ou decrementar um contador atomicamente (SET #views = #views + :inc) ou atualizar um único campo de map aninhado (SET #phone.#mobile = :cell) sem uma ida e volta de ler-modificar-escrever, via UpdateExpression.
  • Adicionar ou remover um membro de um atributo set (admins, tags, seguidores) idempotentemente com os verbos ADD/DELETE, para que retries e requisições duplicadas não corrompam o set.
  • Decidir, antes de recorrer a uma biblioteca cliente, se um wrapper fino de marshaling de tipo (um Document Client) é suficiente, ou se um auxiliar de modelagem orientado a padrão de acesso primeiro é justificado, e descartar um ORM de estilo relacional de qualquer forma.

Deep Dive

A sintaxe de placeholder: por que #name e :value existem

Toda expressão no DynamoDB é uma string curta colada a dois parâmetros de canal lateral. Um Query pelos papéis de Tom Hanks em filmes entre "A" e "M" se parece com isto:

plaintext
items = client.query( TableName='MoviesAndActors', KeyConditionExpression='#actor = :actor AND #movie BETWEEN :a AND :m', ExpressionAttributeNames={ '#actor': 'Actor', '#movie': 'Movie' }, ExpressionAttributeValues={ ':actor': {'S': 'Tom Hanks'}, ':a': {'S': 'A'}, ':m': {'S': 'M'} } )

Os tokens prefixados com : são valores de atributo de expressão. Eles existem porque "todo valor de atributo no DynamoDB tem um tipo. O DynamoDB não infere esse tipo: você precisa fornecê-lo explicitamente ao fazer uma requisição." Se você tentasse colocar o valor em linha em vez disso (KeyConditionExpression: "#actor = { 'S': 'Tom Hanks' }"), o servidor teria que analisar colchetes de dentro de uma string, "uma operação complicada, particularmente se você está escrevendo um objeto aninhado com múltiplos níveis." Separar valores em seu próprio parâmetro significa que o DynamoDB nunca os analisa de dentro do texto da expressão, e seu código cliente pode validar sua forma antes de a requisição sair do processo.

Os tokens prefixados com # são nomes de atributo de expressão, e diferente de valores, são opcionais: nomes de atributo não são tipados, então você pode escrever Actor = :actor diretamente. Duas situações os tornam necessários, em vez de estilísticos:

  • Palavras reservadas. O DynamoDB tem 573 palavras reservadas, e nomes de atributo aparentemente comuns colidem com elas constantemente: Name, Count, Timestamp, Year, Bucket. O próprio hábito do livro: "como a lista de nomes reservados é tão longa, eu prefiro não checar toda vez que estou escrevendo uma expressão. Na maioria dos casos, eu vou usar ExpressionAttributeNames só para ficar seguro."
  • Atributos aninhados e com ponto. O DynamoDB interpreta um . literal em uma expressão como descendo em um map aninhado, então um atributo cujo nome acontece de conter um ponto, ou acessar uma propriedade dentro de um tipo Map, precisa do placeholder de nome para evitar que o DynamoDB entenda mal sua intenção.

Os cinco tipos de expressão, e a qual operação cada um pertence

Expressão Usada em Escopo
KeyConditionExpression Só Query Só atributos de chave primária
FilterExpression Query, Scan Qualquer atributo, aplicado depois da leitura
ProjectionExpression Todas as operações de leitura Qualquer atributo, controla o que é retornado
ConditionExpression Todas as operações de escrita Qualquer atributo, afirmado antes da escrita
UpdateExpression Só UpdateItem Qualquer atributo, descreve a mutação

KeyConditionExpression é "a expressão que você vai usar mais" e a única restrita à chave primária. A metade da sort key aceita uma condição real (>, <, =, begins_with(), ou BETWEEN), não apenas uma correspondência exata, que é o que torna o Query capaz de retornar uma fatia inteira ordenada ("todos os pedidos entre 10 de janeiro e 20 de janeiro") em uma requisição. O próprio hábito do livro é revelador: "toda condição na sort key pode ser expressa com o operador BETWEEN. Por causa disso, eu quase sempre o uso nas minhas expressões": um operador cobre =, <, >, e intervalos igualmente.

FilterExpression parece uma cláusula WHERE e está disponível tanto em Query quanto Scan, mas roda na saída da leitura, não como parte dela. A ordem real de operações do DynamoDB: "primeiro, lê itens correspondentes ao seu Query ou Scan do banco de dados. Segundo, se uma expressão de filtro está presente, ela filtra fora dos resultados itens que não correspondem à expressão de filtro. Terceiro, retorna quaisquer itens restantes ao cliente." Criticamente, o teto de 1MB por requisição é reforçado no passo 1, antes de o filtro rodar. O exemplo do livro: uma tabela de 1GB onde todos os itens "Drama" correspondentes totalizam só 100KB: você poderia esperar uma requisição de volta, mas "já que a expressão de filtro não é aplicada até depois de os itens serem lidos, seu cliente vai precisar paginar por 1000 requisições para varrer sua tabela adequadamente", a maioria delas retornando vazio. O veredito: "uma expressão de filtro não é uma bala de prata que vai te salvar de modelar seu dado adequadamente... não vai te ajudar a encontrar dado mais rapidamente." Seus três trabalhos legítimos são reduzir tamanho de payload, mover um filtro trivial para o lado do servidor por conveniência, e apertar checagens de expiração de TTL (já que a janela real de deleção da AWS depois da expiração de TTL pode rodar até 48 horas).

ProjectionExpression é a versão no nível de atributo da mesma ideia: em vez de descartar itens inteiros não correspondentes (o trabalho de FilterExpression), descarta atributos não selecionados de itens que você já está recebendo de volta, útil para pular um atributo blob grande que você não precisa em uma dada chamada. Está sujeita à mesma ressalva de 1MB-antes-de-filtrar: um atributo excluído grande ainda é lido do disco e contado contra o teto antes de ser removido da resposta.

ConditionExpression é a contraparte do lado da escrita: disponível em toda ação que altera um item (PutItem, UpdateItem, DeleteItem, e suas formas de batch/transacional), ela afirma algo sobre o estado atual do item e cancela a escrita se a afirmação for falsa. Além dos operadores de comparação, adiciona funções feitas para esse trabalho: attribute_exists(), attribute_not_exists() (a proteção de unicidade padrão), attribute_type(), begins_with(), contains(), e size(). Como a chave do item já é fornecida separadamente, uma expressão de condição pode referenciar qualquer atributo, não só atributos de chave, diferente de KeyConditionExpression. O ponto do livro sobre por que isso importa: sem ela, "você precisaria adicionar requisições adicionais caras para buscar um item antes de manipulá-lo, e precisaria considerar como lidar com condições de corrida se outra requisição tentasse manipular seu item ao mesmo tempo." Um item ConditionCheck dentro de TransactWriteItems estende a mesma ideia através de dois itens diferentes: afirmando um fato em um item (um item de lista de admin) enquanto escreve outro (uma deleção de cobrança), tendo sucesso ou falhando como uma unidade.

UpdateExpression é a única expressão que muta, em vez de ler ou afirmar, e é construída a partir de quatro verbos: SET (sobrescrever ou criar um atributo, ou somar/subtrair em um número), REMOVE (deletar um atributo, ou uma entrada de list/map aninhada), ADD (incrementar um número, ou inserir em um set), e DELETE (remover um elemento de um set). Múltiplas cláusulas sob um verbo são separadas por vírgula; múltiplos verbos em uma expressão não precisam de separador além das próprias palavras-chave de verbo: SET Name = :name, UpdatedAt = :updatedAt REMOVE InProgress é válido como escrito. Dois padrões carregam peso real aqui: SET #views = #views + :inc incrementa um contador do lado do servidor, atomicamente, sem uma janela de corrida de ler-depois-escrever; e SET #phone.#mobile = :cell escreve um campo dentro de um map aninhado sem sobrescrever o atributo inteiro. Uma nota para quem lê o livro diretamente: o próprio texto define DELETE como o verbo para "remover um elemento de um atributo set", mas seu exemplo trabalhado para remover um admin de um set escreve UpdateExpression="REMOVE #a :user": isso é uma inconsistência com a própria definição do livro logo acima, não uma sintaxe variante para copiar (mais em "Book vs today" abaixo).

"Não use um ORM": o argumento do livro, e onde ainda se sustenta

A posição do livro sobre mapeadores objeto-relacional (ou, para um armazenamento de documento, objeto-documento) é inequívoca: "de qualquer forma, eu não recomendaria usar um ODM no DynamoDB... não há um termo ótimo aqui." Ele dá duas razões, ambas valem a pena citar diretamente, porque são a parte essencial deste capítulo:

"primeiro, ODMs te empurram a modelar dado incorretamente. ORMs fazem algum sentido em um mundo relacional porque há uma única forma de modelar dado. Todo tipo de objeto vai ganhar sua própria tabela, e relações são tratadas via chaves estrangeiras... esse não é o caso com o DynamoDB. Todos os seus tipos de objeto são enfiados em uma única tabela, e às vezes você tem múltiplos tipos de objeto em um único item DynamoDB. Além disso, buscar um objeto e seus objetos relacionados não é direto como em SQL: vai depender fortemente do design da sua chave primária."

"a segunda razão para evitar ODMs é que eles não economizam muito tempo ou código comparado ao SDK básico da AWS... o DynamoDB é orientado a API, então você vai ter um método nativo para cada ação de API que quer realizar. Seu ORM vai basicamente estar replicando os mesmos parâmetros do SDK da AWS, sem ganho real em facilidade ou legibilidade."

Ambos os argumentos giram em torno do mesmo fato para o qual este par inteiro de capítulos vem se construindo: o trabalho de um ORM relacional é esconder o schema atrás de grafos de objeto e relações carregadas preguiçosamente, porque em SQL esse esconderijo é seguro: o planejador de query escolhe uma estratégia de join razoável, independentemente de como os objetos são moldados no código. No DynamoDB, o "schema" é o padrão de acesso; um Query só é barato porque alguém deliberadamente escolheu a chave primária e a item collection para combinar com uma leitura específica. Uma abstração que esconde essa escolha do desenvolvedor não remove o custo, só remove a visibilidade sobre ele: exatamente o oposto do que uma ferramenta de modelagem de dados NoSQL deveria fazer.

O livro abre duas exceções estreitas, e é precisamente aqui que "não use um ORM" precisa ser atualizado para o ecossistema de hoje, em vez de repetido literalmente:

  1. Wrappers finos de marshaling de tipo. O livro aponta para o AWS.DynamoDB.DocumentClient do SDK Node.js, que converte o boilerplate {Actor: {S: 'Tom Hanks'}} em {Actor: 'Tom Hanks'} simples: resolvendo só o tédio de valor tipado, nenhuma parte da modelagem.
  2. Bibliotecas auxiliares orientadas a padrão de acesso primeiro. O livro nomeia o DynamoDB Toolbox de Jeremy Daly: "explicitamente não um ORM... ele ajuda a definir tipos de entidade na sua aplicação e mapeá-los para sua tabela DynamoDB. Não vai fazer todo o trabalho de consultar a tabela para você, mas simplifica muito do boilerplate." Seu próprio resumo do que torna essa categoria aceitável, onde um ORM de estilo relacional não é: "você ainda vai precisar modelar seu banco de dados adequadamente. Você ainda vai precisar entender como traduzir objetos de aplicação em objetos de banco de dados. E você ainda vai precisar interagir com a API do DynamoDB você mesmo." Um auxiliar reduz fricção; não esconde a decisão de modelagem.

Book vs today: ambas as exceções se moveram

Ambas as exceções do livro parecem diferentes em 2026, e ambos os movimentos reforçam, em vez de minar, seu argumento:

  • O próprio Document Client sumiu. AWS.DynamoDB.DocumentClient pertence ao AWS SDK for JavaScript v2, que entrou em modo de manutenção em setembro de 2024 e atingiu fim de suporte completo em setembro de 2025: não é só datado, é sem suporte. O sucessor direto é o pacote modular @aws-sdk/lib-dynamodb do SDK v3 da AWS, cujo DynamoDBDocumentClient (via DynamoDBDocumentClient.from(ddbClient)) fornece o comportamento idêntico de auto-marshaling em cima do DynamoDBClient atual. Mesmo trabalho estreito, pacote atual: nada sobre o argumento subjacente muda, só qual import faz isso.
  • O ElectroDB emergiu como o exemplo mais forte da categoria 2 que o livro não poderia ter avaliado. O próprio DynamoDB Toolbox continua ativo e amplamente usado (sua documentação ainda descreve uma camada de schema/query-builder, não um ORM completo), mas o ElectroDB, em grande parte construído depois do lançamento de 2020 do livro, se tornou a biblioteca mais comumente escolhida nesse nicho exato, com downloads semanais no npm que agora superam os do Toolbox. Seu próprio enquadramento combina quase exatamente com o teste do livro: ele compila definições de entidade e padrão de acesso para "parâmetros DynamoDB simples, fáceis de logar, inspecionar, e combinar com o que você já tem", em vez de escondê-los atrás de um grafo de objeto. Não faz joins, não carrega relações preguiçosamente, e não permite escrever um padrão de acesso que não foi modelado antecipadamente.

O teste que o livro dá ainda se aplica a qualquer biblioteca nesse espaço, antiga ou nova: ela te faz declarar seu padrão de acesso, ou te deixa fingir que o DynamoDB vai descobrir um por você, da forma que um planejador SQL faria? O primeiro tipo é um auxiliar; o segundo tipo é o ORM que este capítulo está te dizendo para não usar.

Mais um lugar onde o ferramental de hoje diverge do texto do livro, que vale a pena sinalizar precisamente porque contradiz as próprias definições do livro alguns parágrafos antes, em vez de apenas envelhecer: a documentação atual da AWS confirma que REMOVE deleta um atributo completamente (ou um elemento específico de list/map), enquanto DELETE é o único verbo que remove elementos de um set: "a ação DELETE só suporta tipos de dado Set." O exemplo de remoção de set do livro, escrevendo UpdateExpression="REMOVE #a :user", é uma inconsistência genuína tanto com a documentação da AWS quanto com a própria tabela de verbos do livro logo acima, não uma escolha estilística a imitar.

Trade-offs

  • ExpressionAttributeNames é opcional, mas um seguro barato. Pulá-lo economiza alguns caracteres quando um nome de atributo acontece de não colidir com as 573 palavras reservadas do DynamoDB: até que um futuro atributo chamado Status, Data, ou Year quebre uma chamada que funcionou bem por meses. O próprio padrão do livro ("eu vou usar ExpressionAttributeNames só para ficar seguro") é o padrão mais seguro para qualquer um mantendo uma tabela ao longo do tempo, não só especialistas em DynamoDB.
  • FilterExpression e ProjectionExpression reduzem transferência, não custo ou latência. Ambas rodam depois de o teto de leitura de 1MB já ter sido aplicado, então um filtro altamente seletivo sobre uma item collection grande ainda lê (e cobra por) tudo naquela collection: o conserto para "eu preciso encontrar X" é uma chave melhor ou um índice secundário, não um filtro colado em um Scan ou Query amplo.
  • ConditionExpression troca uma janela de corrida de ler-depois-escrever por uma única escrita atômica, ao custo de um caminho de erro de escrita cancelada. Afirmar attribute_not_exists() ou um limite de size() remove a necessidade de um GetItem prévio e a condição de corrida que vem junto com ele, mas todo chamador agora precisa tratar "a condição falhou" como um resultado normal, esperado, não uma exceção.
  • Os quatro verbos de UpdateExpression são compactos, mas fáceis de escolher o errado. REMOVE deleta um atributo completamente; DELETE remove membros de um set. O próprio exemplo trabalhado do livro para remover um membro de set na verdade escreve REMOVE, contradizendo suas próprias definições de verbo algumas páginas antes: um lembrete de verificar contra a documentação atual da AWS, em vez de copiar o padrão de um único exemplo do livro, por mais autoritativa que a fonte seja.
  • O wrapper estilo Document Client / lib-dynamodb é quase de graça: use-o. Ele resolve exatamente um problema (boilerplate de valor tipado) sem tocar em como você modela dado, então há pouca razão para escrever objetos {'S': ...} brutos à mão em 2026, tanto quanto não havia em 2020, só que via o pacote atual do SDK v3, em vez do legado v2.
  • Um auxiliar orientado a padrão de acesso primeiro (DynamoDB Toolbox, ElectroDB) compra ergonomia real, mas ainda exige fazer o trabalho de modelagem primeiro. Essas bibliotecas removem boilerplate em torno de definições de entidade e marshaling, não a decisão antecipada de quais item collections servem quais padrões de acesso: recorrer a uma antes de esse trabalho de design estar feito só move os mesmos erros de modelagem para uma API de aparência mais bonita.

Documentation Links