Bancos NoSQL
MongoDB, Cassandra e DynamoDB são providers do core, como Postgres e MySQL — não pacotes de
terceiros. O que muda em relação aos relacionais não é a sintaxe: é quanto do vocabulário db.*
cada banco consegue honrar, e o que cada um oferece além dele.
O eixo: subconjunto portátil e potência total
| O que é | Portabilidade | |
|---|---|---|
db.* |
O subconjunto portátil: entidade, chave, filtro por igualdade, ordenação, paginação | Roda em todos os providers. Uma incompatibilidade é erro de build, não surpresa em produção |
<provider>.* |
Tudo que aquele banco tem de específico | Nenhuma, por construção e conscientemente |
Nenhum banco é empobrecido para caber num denominador comum. Se o Cassandra sabe fazer uma escrita
com IF NOT EXISTS e o Postgres não, isso não vira um db.* capenga: vira cassandra.persist.
O que torna isso seguro é que cada provider declara o que sabe fazer, e os steps db.* são
checados contra essa declaração no startup. Você descobre a incompatibilidade subindo a
aplicação, não em produção.
MongoDB e DocumentDB
Mesmo provider:/connectionString, mesmos steps db.*, mesma sintaxe de
where/sort/skip/take dos relacionais:
spec:
dataSources:
- { id: mongo-main, provider: mongodb, connectionString: "${config:Mongo}" }
# ou, contra o Amazon DocumentDB:
- id: docdb-main
provider: documentdb
connectionString: "${config:DocumentDb}"
config: { caFile: /etc/pipevine/global-bundle.pem }
Duas diferenças que aparecem no build:
db.sqlnão existe. Mongo não tem dialeto SQL, então não declara a capacidade de comandos crus. Umdb.sqlapontando para um data source Mongo falha o startup nomeando a limitação.transaction: requireddepende do servidor, não do YAML. Transação multi-documento exige um replica set; contra ummongodstandalone a aplicação recusa subir, com uma mensagem nomeando a exigência. Essa capacidade é resolvida uma vez, na primeira conexão — não a cada requisição.
documentdb é o mesmo provider com um perfil de compatibilidade (retryWrites=false forçado,
config.caFile para validar contra uma CA específica).
Cassandra e DynamoDB: não existe chave padrão
Estes dois diferem dos demais num ponto estrutural: não têm chave primária padrão. Toda entidade
precisa declarar a sua em options, ou a aplicação não sobe:
spec:
entities:
- name: OrderEvent
table: order_events
dataSource: cass-main
key: id
options:
partitionKey: [customerId] # Cassandra: lista — aceita partição composta
clusteringKeys:
- { field: createdAt, desc: true }
allowFiltering: false # ALLOW FILTERING nunca é implícito
- name: OrderEventDynamo
table: order_events
dataSource: dynamo-main
key: id
options:
partitionKey: customerId # DynamoDB: campo único, sem partição composta
sortKey: createdAt # opcional
indexes:
- { name: byStatus, partitionKey: status }
allowScan: false # Scan nunca é implícito
options é uma bag livre: o core não conhece seus campos — quem valida é o provider, no startup,
nomeando a entidade e o campo desconhecido.
O formato da consulta é validado no startup
db.query/db.find/db.exists continuam funcionando com a mesma sintaxe, mas com três restrições
que estes bancos impõem — todas checadas quando a aplicação sobe:
| Restrição | O que acontece se você violar |
|---|---|
where: precisa cobrir a partition key |
Erro nomeando as chaves que faltam, e sugerindo options.allowFiltering: true (Cassandra) ou options.allowScan: true (DynamoDB) |
sort: só ordena pela clustering key / sort key declarada |
Erro dizendo qual campo foi pedido e qual é o permitido |
skip: nunca é aceito |
Erro dizendo que o provider é cursor-only, e apontando o step de paginação próprio dele |
A terceira é a mais importante de entender: uma consulta sem a partition key vira um scan da
tabela inteira disfarçado de consulta. Em vez de deixar isso passar e degradar silenciosamente em
produção, o build recusa — e se você realmente quer o scan, declara allowFiltering/allowScan
na entidade, de forma explícita e revisável.
Os steps próprios
Para o que o db.* não expressa, cada um publica os próprios steps:
| Step | O que oferece além do db.* |
|---|---|
cassandra.query |
consistency: por consulta, paginação nativa (pageSize/pageState), allowFiltering: no próprio step |
cassandra.persist |
ttl: por escrita, consistency:, e ifNotExists: — lightweight transaction, a forma real de garantir unicidade nesse provider |
dynamo.query |
GSI/LSI explícito (index:), consistentRead:, projection:, cursor (exclusiveStartKey/lastEvaluatedKey) |
dynamo.persist |
condition: (ConditionExpression) — concorrência otimista e unicidade real nesse provider |
Paginação por cursor publica um objeto, não um array
Diferente do db.query, que publica um array puro, os dois steps de consulta próprios publicam um
objeto estruturado — porque têm uma segunda coisa a reportar: o cursor da próxima página.
- type: cassandra.query
id: events
entity: OrderEvent
where: { customerId: "input.route.id" }
sort: ["-createdAt"]
pageSize: 50
pageState: "input.query.cursor"
// steps.events.result
{ "items": [ /* ... */ ], "pageState": "<cursor, ou null na última página>" }
No DynamoDB é o mesmo formato, com lastEvaluatedKey no lugar de pageState. Ou seja: o corpo da
resposta é steps.events.result.items, e o cursor da próxima página é
steps.events.result.pageState — não confunda com o array direto que o db.query publica.
Unicidade: onde cada um garante
Um detalhe que costuma escapar: onConflict do db.persist não tem efeito no Cassandra. Se
você precisa de unicidade real, é cassandra.persist com ifNotExists: true (uma lightweight
transaction) ou dynamo.persist com condition: "attribute_not_exists(id)".
No DynamoDB, uma condição não satisfeita levanta ConditionalCheckFailedException no commit, e o
step não esconde isso — envolva num try/catch se quiser tratar a colisão como regra de
negócio. Veja Tratamento de erros.
Para onde ir daqui
- Referência de steps — os quatro steps
cassandra.*/dynamo.*, com contrato completo. - Persistência — os steps
db.*e os providers relacionais. IDataProvider— as capacidades declaradas por provider, e como adicionar um banco novo.