Pular para o conteúdo

Banco de dados

SQLite com Arcane.Database: consultas parametrizadas, transações e modelos.

Conectar#

dataforge
adopt Arcane.Database as DB

conn := DB.memory()                  # em memória: some ao terminar
conn := DB.connect("dados.db")       # arquivo SQLite

memory() é ideal para teste: cada execução começa limpa, e nada fica no disco.

Parâmetros, sempre#

dataforge
DB.query(conn, "SELECT nome FROM produtos WHERE preco < ?", [100])

As operações#

ChamadaPara
execute(conn, sql, params)INSERT, UPDATE, DELETE, CREATE
execute_many(conn, sql, lista)vários INSERT de uma vez
query(conn, sql, params)várias linhas, como lista de vaults
query_one(conn, sql, params)uma linha, ou void
count(conn, tabela)atalho para COUNT(*)
dataforge
DB.execute(conn, """CREATE TABLE produtos (
    id INTEGER PRIMARY KEY,
    nome TEXT NOT NULL,
    preco REAL NOT NULL
)""")

DB.execute(conn, "INSERT INTO produtos (nome, preco) VALUES (?, ?)", ["Mouse", 80.0])

DB.execute_many(conn, "INSERT INTO produtos (nome, preco) VALUES (?, ?)", [
    ["Teclado", 200.0],
    ["Monitor", 1200.0]
])

execute_many#

Uma ida ao banco em vez de N. Para inserção em lote, a diferença de desempenho é grande.

Consultar#

dataforge
todos := DB.query(conn, "SELECT nome, preco FROM produtos ORDER BY preco DESC")
cycle p in todos:
    out $"{p["nome"].pad_end(10)} R$ {p["preco"]}"

caro := DB.query_one(conn, "SELECT nome FROM produtos ORDER BY preco DESC LIMIT 1")

query devolve vaults com os nomes das colunas como chaves — dá para usar p["nome"] direto, sem índices.

Transações#

dataforge
DB.begin(conn)
DB.execute(conn, "UPDATE contas SET saldo = saldo - 100 WHERE id = 1")
DB.execute(conn, "UPDATE contas SET saldo = saldo + 100 WHERE id = 2")
DB.commit(conn)     # ou DB.rollback(conn)

As duas operações acontecem juntas ou nenhuma. Sem transação, uma falha entre elas deixaria dinheiro sumido.

O padrão seguro combina com monitor:

dataforge
DB.begin(conn)
monitor:
    transferir(de, para, valor)
    DB.commit(conn)
handle e:
    DB.rollback(conn)
    propagate e.message

Converter na fronteira#

dataforge
record Produto:
    id: Integer
    nome: String
    preco: Number

action buscar_todos():
    linhas := DB.query(conn, "SELECT id, nome, preco FROM produtos")
    yield linhas >> morph l: Produto(l["id"], l["nome"], l["preco"])

Linhas do banco entram; records tipados saem. A partir dali, p.nome com verificação, não l["nome"] com risco de digitar errado.

Regra de negócio no banco#

dataforge
action emprestar(livro_id, leitor):
    ja := DB.query_one(conn, "SELECT id FROM emprestimos WHERE livro_id = ?", [livro_id])
    given ja isnt void:
        yield {"ok": no, "erros": ["livro ja emprestado"]}
    ...

"Um livro só pode estar emprestado uma vez" depende do estado atual — não dá para validar sem consultar. Por isso essa checagem fica na camada de persistência, não nas regras puras.

Introspecção#

dataforge
DB.tables(conn)                  # as tabelas
DB.columns(conn, "produtos")     # as colunas
DB.table_exists(conn, "x")
DB.table_info(conn, "produtos")  # tipos e restrições
DB.close(conn)