Pular para o conteúdo

    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:

    intermediario/cap15_sqlite.pylinhas 10 a 27
    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))
    
    Saída
    {'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:

    intermediario/cap15_sqlite.pylinhas 32 a 43
    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())
    
    Saída
    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:

    intermediario/cap15_sqlite.pylinhas 48 a 83
    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()))
    
    Saída
    {'id': 3, 'titulo': 'Revisar o capítulo'}
    3
    

    SQL puro ou ORM

    SQLite puro (sqlite3)ORM (SQLAlchemy)
    Como se escreveStrings de SQLClasses e objetos Python
    Proteção contra injeçãoSó se você usar ? sempreEmbutida
    Trocar de bancoReescrever o SQLMudar a URL de conexão
    Esforço inicialNenhum, vem com o PythonInstalar e aprender
    Cresce bem?Vira uma bagunça de stringsSim, 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.