Persistência
Declarando onde e o quê
Declare dataSources e entities uma vez em spec; use os steps db.* no pipeline para ler e
escrever.
spec:
dataSources:
- { id: main, provider: postgres, connectionString: "${env:PIPEVINE_PG_CONNECTION}" }
entities:
- name: widget
table: widgets
dataSource: main
key: id
properties:
- { name: id, type: uuid }
- { name: name, type: string }
postgres, mysql e sqlite são os três providers relacionais do core — o mesmo pipeline, sem
alterar uma linha, roda contra qualquer um dos três trocando só provider:/connectionString:
# postgres
- { id: main, provider: postgres, connectionString: "${env:PIPEVINE_PG_CONNECTION}" }
# mysql
- { id: main, provider: mysql, connectionString: "${env:PIPEVINE_MYSQL_CONNECTION}" }
# sqlite — o único que não precisa de servidor/container
- { id: main, provider: sqlite, connectionString: "Data Source=widgets.db" }
sqlite é o alvo natural para os testes da sua própria aplicação (nenhuma infra para subir) e para
rodar a imagem base genérica sem nenhum container de banco.
Os providers não relacionais também são do core, não de terceiros: mongodb/documentdb
(Pipevine.Data.MongoDb), cassandra (Pipevine.Data.Cassandra) e dynamodb
(Pipevine.Data.DynamoDb). Eles usam os mesmos steps db.* com a mesma sintaxe, dentro do que cada
um declara saber fazer — e Cassandra e DynamoDB publicam ainda steps próprios (cassandra.*,
dynamo.*) para o que o db.* não expressa. Um provider para um banco ainda não coberto pode ser
adicionado por um pacote de terceiro sem tocar no core; veja
IDataProvider.
Tipos de coluna aceitos hoje: uuid, string, int/integer, long, decimal (com
precision/scale), bool/boolean, datetime, timestamptz, jsonb/json. Nem todos são
nativos nos três bancos — onde não são, o provider converte, e a tabela completa de conversões (e
das poucas perdas documentadas, como o offset original de timestamptz) está em
Configuração YAML § Conversão de tipos por provider relacional.
Bootstrap de tabela é uma conveniência do CLI (pipevine db script minha-api.pipevine.yaml | psql ... — ou o cliente do banco escolhido), não uma ferramenta de migração — não é seguro rodar duas
vezes contra um schema que já mudou por fora.
Os steps db.*
| Step | Função |
|---|---|
db.persist |
Insert ou upsert (onConflict.columns opcional). Não publica resultado |
db.update |
Update parcial — faz merge do patch com o registro existente. Casa pela chave declarada da entidade, informada em key: (uma expressão), não por where:. Não publica resultado |
db.delete |
Remoção, também por key:. Não publica resultado |
db.sql |
Escape hatch para SQL arbitrário — exige dataSource: (não entity:). Parâmetros por expressão, nunca por interpolação de string. Não publica resultado |
db.find |
Uma entidade (where) ou null. Publica em steps.<id>.result |
db.exists |
Booleano (where). Publica em steps.<id>.result |
db.query |
Lista (where, sort, skip/take). Publica em steps.<id>.result |
where é sempre igualdade por campo. db.find, db.exists e db.query exigem id: — é assim
que publicam o resultado em steps.<id>.result, para o resto do pipeline ler.
- type: db.persist
entity: widget
data: vars.widget
- { type: db.find, id: found, entity: widget, where: { id: "input.route.id" } }
- { type: fail, when: "steps.found.result = null", status: 404, code: WIDGET_NOT_FOUND }
db.sql é o escape hatch para o que os steps tipados acima não cobrem. Os parâmetros são sempre
parâmetros ADO.NET reais, resolvidos por expressão e vinculados pelo provider — nunca interpolados
no texto do SQL. É isso que torna um valor malicioso em um campo de entrada apenas dado comum, em
vez de uma injeção: o texto do SQL em si é fixo desde o build e nunca toca os valores avaliados.
Transações
Para uma rota que faz mais de uma escrita e precisa que tudo aconteça atomicamente, declare
transaction: required na rota:
- id: createOrder
transaction: required
pipeline:
- { type: db.persist, entity: Order, data: vars.order }
- { type: db.update, entity: Inventory, key: "vars.order.sku", patch: "{ 'reserved': true }" }
Uma sessão transacional é uma única conexão — não é seguro fazer db.* concorrente dentro dela.
Isso importa especificamente quando você combina transação com os steps de controle de fluxo
paralelo (parallel, foreach com parallelism > 1); veja
Controle de fluxo — a seção "Um detalhe que morde: transação e
concorrência" — para o detalhe completo. É um erro fácil de cometer e vale a leitura antes de
combinar as duas coisas.
Para onde ir daqui
- Controle de fluxo — decisões condicionais e iteração sobre os dados que você acabou de ler ou gravar.
- Cache — evitar ir ao banco quando o dado já foi lido recentemente.
- Um exemplo real com transação e múltiplas escritas:
samples/03-inventory-ordersno repositório do Pipevine.