Capítulo 15, Intermediário
De SQLite puro ao ORM
Antes do SQLAlchemy vale entender o que ele automatiza. O `sqlite3` vem com o Python, grava em um arquivo e deixa claro o que é um banco de dados, e também mostra o maior perigo de escrever SQL à mão.
Um banco em um arquivo
O SQLite guarda tudo em um arquivo. Os dados sobrevivem ao reinício do servidor, ao contrário de uma lista em memória. O módulo sqlite3 faz parte do Python, sem instalar nada:
import sqlite3
conn = sqlite3.connect("loja.db")
conn.row_factory = sqlite3.Row
conn.execute(
"""CREATE TABLE IF NOT EXISTS tarefas (
id INTEGER PRIMARY KEY,
titulo TEXT NOT NULL,
concluida INTEGER NOT NULL DEFAULT 0
)"""
)
conn.execute("DELETE FROM tarefas")
conn.execute("INSERT INTO tarefas (titulo) VALUES (?)", ("Estudar SQL",))
conn.execute("INSERT INTO tarefas (titulo, concluida) VALUES (?, ?)", ("Escrever testes", 1))
conn.commit()
for linha in conn.execute("SELECT id, titulo, concluida FROM tarefas ORDER BY id"):
print(dict(linha))
{'id': 1, 'titulo': 'Estudar SQL', 'concluida': 0}
{'id': 2, 'titulo': 'Escrever testes', 'concluida': 1}
O commit() grava as alterações de vez. Sem ele, o INSERT fica pendente e some se a conexão fechar. Você pode inspecionar o arquivo loja.db com uma ferramenta visual, como a extensão "SQLite Viewer" do VS Code.
O perigo: SQL montado com texto
O jeito mais fácil de escrever uma consulta é montar uma string com o valor dentro. É também o jeito mais perigoso, porque o valor passa a poder mudar a consulta. Esta é a injeção de SQL, e dá para vê-la acontecer:
conn2 = sqlite3.connect(":memory:")
conn2.execute("CREATE TABLE usuarios (nome TEXT, senha TEXT)")
conn2.execute("INSERT INTO usuarios VALUES ('ana', 's3gredo')")
conn2.execute("INSERT INTO usuarios VALUES ('bia', 'outra-senha')")
entrada_maliciosa = "x' OR '1'='1"
inseguro = f"SELECT nome FROM usuarios WHERE nome = '{entrada_maliciosa}'"
print("montando texto:", conn2.execute(inseguro).fetchall())
seguro = "SELECT nome FROM usuarios WHERE nome = ?"
print("com parâmetro: ", conn2.execute(seguro, (entrada_maliciosa,)).fetchall())
montando texto: [('ana',), ('bia',)]
com parâmetro: []
Na primeira consulta, a entrada fechou a aspa e acrescentou OR '1'='1', que é sempre verdadeiro: a consulta devolveu todos os usuários. Na segunda, o ? envia o valor separado do SQL, e ele é sempre tratado como dado, nunca como código. Regra sem exceção: nunca monte SQL com f-string ou + com dados de fora. Use sempre parâmetros. O SQLAlchemy faz isso por você.
Usando o `sqlite3` no FastAPI
Cada requisição deve ter a sua própria conexão, aberta e fechada com uma dependência com yield. Isso evita o check_same_thread=False e o compartilhamento de uma única conexão entre threads, que é um atalho comum e perigoso:
from typing import Annotated
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
def obter_conexao():
conexao = sqlite3.connect("loja.db")
conexao.row_factory = sqlite3.Row
try:
yield conexao
finally:
conexao.close()
Conexao = Annotated[sqlite3.Connection, Depends(obter_conexao)]
@app.get("/tarefas")
def listar(conexao: Conexao):
linhas = conexao.execute("SELECT id, titulo, concluida FROM tarefas ORDER BY id").fetchall()
return [dict(linha) for linha in linhas]
@app.post("/tarefas", status_code=201)
def criar(titulo: str, conexao: Conexao):
cursor = conexao.execute("INSERT INTO tarefas (titulo) VALUES (?)", (titulo,))
conexao.commit()
return {"id": cursor.lastrowid, "titulo": titulo}
cliente = TestClient(app)
print(cliente.post("/tarefas", params={"titulo": "Revisar o capítulo"}).json())
print(len(cliente.get("/tarefas").json()))
{'id': 3, 'titulo': 'Revisar o capítulo'}
3
SQL puro ou ORM
SQLite puro (sqlite3) | ORM (SQLAlchemy) | |
|---|---|---|
| Como se escreve | Strings de SQL | Classes e objetos Python |
| Proteção contra injeção | Só se você usar ? sempre | Embutida |
| Trocar de banco | Reescrever o SQL | Mudar a URL de conexão |
| Esforço inicial | Nenhum, vem com o Python | Instalar e aprender |
| Cresce bem? | Vira uma bagunça de strings | Sim, com modelos e relacionamentos |
Para um script ou uma aplicação minúscula, o sqlite3 basta. Para uma API que vai crescer, eu uso o ORM, e é o assunto dos próximos capítulos.
Exercício 1
Buscar com segurança
Escreva buscar_por_titulo(conexao, termo), que devolva os títulos que contêm o termo (com LIKE), usando parâmetro. Confira que a entrada maliciosa "' OR '1'='1" não devolve nada.