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.sql não existe. Mongo não tem dialeto SQL, então não declara a capacidade de comandos crus. Um db.sql apontando para um data source Mongo falha o startup nomeando a limitação.
  • transaction: required depende do servidor, não do YAML. Transação multi-documento exige um replica set; contra um mongod standalone 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.