Guia de estudo

Aula 24 · Qua, 08/07

POO + Banco de dados (SQLite)

O gestor de ontem guardava tudo num .txt reescrito inteiro a cada mudança. Hoje os objetos vão pra um BANCO de verdade: tabelas, SQL e um repositório que esconde o banco da tela.

Última aula de conteúdo antes do projeto final. A persistência sobe mais um degrau: sai do arquivo de texto (aula 17, aula 23) e entra num banco relacional SQLite — o único ingrediente novo de hoje (um módulo que já vem com o Python, como o Tkinter). Tudo em volta é colheita: encapsulamento (04), listas de objetos (18), __str__ (03) e a MESMA GUI da 22/23. A gente aprende SQL básico, o placeholder ? seguro, mapeia classe ↔ tabela (ORM manual) e encapsula o CRUD numa classe RepositorioProduto.

SQLite & SQL CRUD & placeholder ? RepositorioProduto

roteiro da aula

O que vamos aprender hoje

01

Por que um banco

Os quatro furos do .txt da aula 23 (sem busca, reescreve tudo, split(";") frágil, sem relações) e o que um banco relacional resolve: tabela, linhas, colunas tipadas e chave primária.

02

sqlite3 & SQL

O módulo que já vem com o Python: connect(), execute(), commit(), close() e o with. E o SQL de hoje: CREATE, INSERT, SELECT, UPDATE, DELETE — com o placeholder ? seguro.

03

Classe ↔ tabela & Repositório

Mapear objeto em linha e linha em objeto (ORM manual: from_row/to_params, parente do from_dict/to_dict da aula 17), e encapsular todo o CRUD numa classe RepositorioProduto.

04

CRUD + a GUI sobre o banco

A janela da 22/23 consumindo o repositório: a tela não sabe SQL, só chama repo.salvar(), repo.listar(), repo.remover(). Trocamos a camada de dados sem mexer na interface.

ponte com a aula 23

De onde paramos — o gestor da aula 23

  • 🗂️ Ontem o gestor guardava os clientes num clientes.txt: carregar_todos() lia o arquivo virando lista de objetos, e salvar_todos() reescrevia o arquivo INTEIRO em modo "w" a cada mudança (aula 17).
  • 🔁 A ideia central da persistência continua a mesma da aula 17: objeto vira texto e volta — o from_dict/to_dict daquela aula, o split(";") desfazendo a linha. Hoje o mesmo objeto vira uma LINHA numa tabela.
  • 🐍 E, de novo, quase nada de Python novo. A única ferramenta nova é o módulo sqlite3 — que, igual ao Tkinter, já vem instalado com o Python. Tudo em volta é colheita: encapsulamento (04), listas (18), __str__ (03).
  • 🎯 O plano de hoje: pegar o mesmo par salvar/carregar que você já domina e trocar o arquivo de texto por um banco de dados de verdade. A GUI da 22/23 nem vai perceber — ela conversa com um repositório, não com o disco.

o problema de hoje

O furo do .txt → precisamos de um banco

  • 🔍 Sem busca. Achar "o cliente de e-mail X" no .txt obriga a ler o arquivo inteiro e comparar linha por linha na mão. Não dá pra pedir "me traga só os produtos acima de R$ 10" — o texto não sabe filtrar.
  • ✏️ Reescreve TUDO. Lembra do salvar_todos() em "w" da aula 23? Mudar UM registro obrigava a regravar o arquivo INTEIRO. Com dezenas, tudo bem; com milhares, é caro e arriscado (um erro no meio corrompe o arquivo todo).
  • 🧨 split(";") frágil. Toda a estrutura mora num caractere separador. Um cliente com ; no nome quebra a linha; um campo a mais ou a menos e o split estoura. O texto não tem TIPOS nem regras.
  • 🔗 Sem relações. E se um pedido apontasse pra um cliente e pra vários produtos? No .txt não há como LIGAR um arquivo a outro. Um banco relacional nasceu justamente pra guardar dados em tabelas que se relacionam.

conceito 1 · o banco

O que é um banco relacional

  • 🧱 Um banco relacional guarda dados em tabelas: uma grade de linhas e colunas, como uma planilha. A tabela produtos tem uma linha por produto e as colunas id, nome, preco.
  • 🏷️ Cada coluna tem um TIPO fixo: id é inteiro, nome é texto, preco é número real. O banco RECUSA um dado do tipo errado — o oposto do .txt, onde tudo era string e o split só torcia pra dar certo.
  • 🧩 O paralelo com a POO é direto: uma linha é um objeto, uma coluna é um atributo, e a tabela é a coleção (a list da aula 18). Guardar um objeto = inserir uma linha; carregar = ler linhas de volta.
  • 🔑 Toda tabela tem uma chave primária: uma coluna que identifica cada linha de forma única — o nosso id. O banco gera esse número sozinho a cada inserção; é por ele que a gente vai buscar, atualizar e remover uma linha específica.

conceito 1 · comparando

produtos.txt × tabela produtos

O mesmo produto guardado dos dois jeitos. O .txt é uma linha de texto solta; a tabela é uma estrutura tipada que o banco entende, busca e ordena.

produtos.txt — texto solto

  • Uma linha: Café;18.0 — tudo é string, inclusive o preço.
  • Pra achar um produto: ler o arquivo inteiro e comparar na mão.
  • Pra ordenar por preço: carregar tudo e sorted() na memória.
  • Um ; no meio do nome quebra o split(";") — sem TIPO, sem regra.

tabela produtos — estrutura tipada

  • Colunas id | nome | preco, cada uma com seu TIPO (INTEGER, TEXT, REAL).
  • Buscar: SELECT ... WHERE preco > 10 — o banco filtra pra você.
  • Ordenar: SELECT ... ORDER BY preco — sem carregar tudo na mão.
  • O banco recusa dado do tipo errado e gera o id sozinho (chave primária).
Mesma informação, estruturas opostas. O .txt serve pra dezenas de linhas simples; a tabela é o que sustenta um sistema que precisa buscar, filtrar, ordenar e relacionar dados — o que o projeto final vai pedir.

conceito 2 · a ferramenta

O módulo sqlite3

  • 🔋 O SQLite é um banco relacional inteiro dentro de um único arquivo (.db) — sem servidor, sem instalar nada. E o módulo sqlite3 que conversa com ele já vem com o Python, exatamente como o tkinter da aula 22. import sqlite3 e pronto.
  • 🔌 O ciclo é o mesmo abrir → usar → fechar do arquivo (aula 17): sqlite3.connect(caminho) abre (ou CRIA) o arquivo .db e devolve uma conexão; conexao.execute(sql) roda um comando SQL; conexao.close() fecha.
  • 💾 Um detalhe que pega todo mundo: mudanças (INSERT/UPDATE/DELETE) só ficam gravadas depois de conexao.commit(). Esqueceu o commit()? É como esquecer o salvar_todos() da aula 23 — rodou, pareceu funcionar, mas nada persistiu.
  • 📁 O arquivo .db fica ancorado ao lado do script com Path(__file__).parent — o mesmo padrão da aula 22 pro clientes.txt. Abrir a mesma conexão de novo reencontra os dados; é a persistência da aula 17 num formato profissional.

conceito 3 · CREATE TABLE

Primeiro contato: o ritual do banco

Abrir a conexão, rodar um CREATE TABLE, confirmar com commit() e fechar. É o mesmo abrir → usar → fechar do arquivo (aula 17) — só que agora o recurso é um banco.

🔌
sqlite3.connect(BANCO) abre (ou cria) o arquivo .db — o open() da aula 17, num banco. O BANCO = PASTA / "produtos.db" reusa o Path(__file__).parent da aula 22 pra ancorar o arquivo ao lado do script.
🧱
CREATE TABLE IF NOT EXISTS desenha a tabela: as colunas e seus TIPOS. O IF NOT EXISTS deixa rodar de novo sem quebrar. E o commit() grava — sem ele, a tabela não persiste (a armadilha do slide s15).
criar_banco.py
import sqlite3
from pathlib import Path

PASTA = Path(__file__).parent      # aula 22: ancora o .db ao lado do script
BANCO = PASTA / "produtos.db"      # aula 17: "pasta / arquivo", agora um .db

conexao = sqlite3.connect(BANCO)   # aula 24 (NOVO): abre (ou CRIA) o arquivo .db
conexao.execute("""
    CREATE TABLE IF NOT EXISTS produtos (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        nome TEXT NOT NULL,
        preco REAL NOT NULL
    )
""")                               # aula 24: SQL que cria a tabela (se ainda nao existe)
conexao.commit()                   # aula 24: confirma a mudanca no disco
conexao.close()                    # aula 17: fecha o recurso, como o arquivo

print("Banco pronto:", BANCO.name)
# Banco pronto: produtos.db
AUTOINCREMENT no id significa que o banco gera o número sozinho a cada INSERT — você nunca passa o id na mão. INTEGER PRIMARY KEY é a chave primária (única por linha); NOT NULL obriga nome e preço a existirem. Esse bloco SQL é escrito UMA vez; depois é só inserir, consultar e alterar linhas.
Os próximos exemplos de SQL usam este mesmo produtos.db — rode-os em ordem, começando por este.

conceito 3 · os comandos

O SQL de hoje

item detalhe
CREATE TABLE Desenha a tabela e os tipos das colunas. CREATE TABLE IF NOT EXISTS produtos (id INTEGER PRIMARY KEY AUTOINCREMENT, nome TEXT NOT NULL, preco REAL NOT NULL). Roda uma vez; o IF NOT EXISTS deixa reexecutar.
INSERT — o C do CRUD Cria uma linha (Create). INSERT INTO produtos (nome, preco) VALUES (?, ?). Os ? são placeholders; os valores vão numa tupla à parte. O banco atribui o id.
SELECT — o R do CRUD Lê linhas (Read). SELECT id, nome, preco FROM produtos [WHERE id = ?] [ORDER BY id]. Devolve tuplas via fetchall() (lista) ou fetchone() (uma ou None).
UPDATE — o U do CRUD Muda linhas (Update). UPDATE produtos SET nome = ?, preco = ? WHERE id = ?. O WHERE diz QUAL linha — sem ele, muda a tabela INTEIRA.
DELETE — o D do CRUD Apaga linhas (Delete). DELETE FROM produtos WHERE id = ?. De novo: sem WHERE, apaga TUDO. O WHERE id = ? é o cinto de segurança.
WHERE — o filtro do banco A cláusula que escolhe QUAIS linhas o SELECT/UPDATE/DELETE afeta. A condição pode mirar uma linha só (id = ?) ou um conjunto (por faixa de preço, por trecho do nome...). Regra de ouro: UPDATE e DELETE quase sempre pedem um WHERE — sem ele, a operação atinge a tabela INTEIRA.

conceito 4 · INSERT

INSERT com ?: guardar uma linha

O ? é um placeholder: você escreve o SQL com ? no lugar dos valores e passa os valores numa TUPLA à parte. O banco encaixa com segurança — nunca cole valores no meio do texto do SQL (o porquê está no slide s16).

🧩
execute(sql, valores) recebe DOIS argumentos: o SQL com ? e uma tupla com os valores na ordem dos ?. Note a vírgula em ("Café", 18.0) — é uma tupla (aula 18), não parênteses soltos.
💾
O commit() é o "salvar" — sem ele, o INSERT some quando o programa fecha, como esquecer o salvar_todos() da aula 23. O id não aparece aqui: o banco gera sozinho (a gente pega ele com lastrowid no s24).
inserir.py
import sqlite3
from pathlib import Path

BANCO = Path(__file__).parent / "produtos.db"   # aula 24: o mesmo .db do slide anterior

conexao = sqlite3.connect(BANCO)                # aula 24: abre o banco
conexao.execute(
    "INSERT INTO produtos (nome, preco) VALUES (?, ?)",   # aula 24 (NOVO): ? = placeholder seguro
    ("Café", 18.0),                             # aula 24: os valores vao numa TUPLA a parte
)
conexao.commit()                                # aula 24: sem commit, nada persiste
conexao.close()

print("1 produto inserido.")
# 1 produto inserido.
Por que dois argumentos e não uma f-string? Porque o ? deixa o banco tratar o valor com segurança — mesmo que o nome tenha aspas, ponto-e-vírgula ou coisa pior. É a versão-banco do split(";") frágil da aula 23: nunca confie em juntar texto na mão. O slide s16 mostra o desastre de usar f-string aqui.
Este exemplo assume que o s8 já criou a tabela. O connect cria o arquivo .db; a tabela nasce no CREATE TABLE — sem ela, o INSERT dá "no such table".

conceito 3 · SELECT

SELECT: consultar linhas

O SELECT lê linhas. fetchall() devolve uma LISTA de tuplas (aula 18); fetchone() devolve UMA tupla (ou None se não achar). Por enquanto são tuplas cruas — no próximo bloco cada uma vira um objeto Produto.

📋
fetchall() devolve uma lista de tuplas — a mesma list da aula 18, só que cada item é uma tupla (id, nome, preco). Percorrer com for é idêntico ao que você já faz com qualquer lista.
🎯
fetchone() traz UMA linha — a primeira do resultado — ou None se o WHERE não achou nada. Esse None é o mesmo do buscar que não encontra (aula 18): sempre cheque antes de usar.
consultar.py
import sqlite3
from pathlib import Path

BANCO = Path(__file__).parent / "produtos.db"

conexao = sqlite3.connect(BANCO)

cursor = conexao.execute("SELECT id, nome, preco FROM produtos")   # aula 24: consulta as linhas
linhas = cursor.fetchall()          # aula 18: fetchall() -> LISTA de tuplas
print("Todas:", linhas)
# Todas: [(1, 'Café', 18.0)]

primeira = conexao.execute(
    "SELECT id, nome, preco FROM produtos WHERE id = ?", (1,)).fetchone()   # aula 24: fetchone() -> UMA tupla ou None
print("Primeira:", primeira)
# Primeira: (1, 'Café', 18.0)

conexao.close()
# cada tupla dessas vira um objeto Produto no proximo bloco
Repare que o SELECT devolve TUPLAS, não objetos Produto. O banco não sabe da sua classe — ele só entrega linhas cruas. Transformar (1, 'Café', 18.0) num Produto é trabalho seu: é exatamente o from_row que a gente escreve no s19. Por ora, guarde: SELECT -> tuplas.

conceito 3 · SELECT com filtro

WHERE que filtra: condição e LIKE

Até aqui o WHERE só apareceu como id = ? — um cadeado que acha UMA linha. Mas o WHERE aceita uma condição e escolhe VÁRIAS linhas: é a promessa do s6 ("o banco filtra pra você") e a resposta ao furo "sem busca" do s4. E pra buscar texto por pedaço, entra o LIKE com o curinga %.

🔎
WHERE preco > ? não olha o id: é uma condição que o banco testa em CADA linha e devolve todas as que passam — o "me traga os produtos acima de R$ 10" que o .txt não sabia fazer (furo do s4). O valor continua indo por ? (aula 24, s10), nunca colado no SQL.
🔤
LIKE compara texto por pedaço, e o curinga % quer dizer "qualquer coisa aqui": '%lei%' acha Leite no meio, 'lei%' só o que COMEÇA com lei. Regra de ouro: o % vai dentro do valor ((f"%{termo}%",)), o SQL fica com ? — nunca concatene o termo no texto do SQL (o perigo do s16).
filtrar.py
import sqlite3
from pathlib import Path

BANCO = Path(__file__).parent / "produtos.db"   # aula 24: a tabela ja tem Café, Açúcar e Leite

with sqlite3.connect(BANCO) as conexao:
    # WHERE com CONDICAO: escolhe VARIAS linhas (nao so id = ?)
    caros = conexao.execute(
        "SELECT id, nome, preco FROM produtos WHERE preco > ?", (10,)).fetchall()   # aula 24 (NOVO): filtro por condicao
    print("Acima de R$ 10:", caros)
    # Acima de R$ 10: [(1, 'Café', 18.0)]

    # LIKE + curinga %: busca PARCIAL de texto
    achados = conexao.execute(
        "SELECT id, nome, preco FROM produtos WHERE nome LIKE ?", ("%lei%",)).fetchall()   # aula 24 (NOVO): LIKE acha "Leite"
    print("Nome com 'lei':", achados)
    # Nome com 'lei': [(3, 'Leite', 4.2)]
Esse mesmo WHERE ... LIKE ? é o motor do buscar_por_nome(termo) que o repositório ganha na EV1 (s24): busca parcial por nome, devolvendo uma lista de Produto. Detalhe honesto: o LIKE ignora maiúsculas/minúsculas para letras ASCII ('ca' acha 'Café'), mas acentos contam — o que explica uma busca "que não achou" um nome acentuado.

conceito 3 · UPDATE / DELETE

UPDATE e DELETE com WHERE id = ?

Mudar e apagar linhas por id. O WHERE id = ? diz QUAL linha — e é o cinto de segurança: sem WHERE, o comando atinge a tabela inteira (o eco do "w" que apagava tudo na aula 17).

✏️
UPDATE produtos SET preco = ? WHERE id = ? muda só a linha do id passado. Os valores continuam vindo numa tupla (19.5, 1), na ordem dos ? — placeholder de novo (aula 24, s10).
🗑️
DELETE FROM produtos WHERE id = ? apaga a linha do id. É o del da lista (aula 23), agora no banco. O WHERE é obrigatório na prática — sem ele, some a tabela toda.
alterar.py
import sqlite3
from pathlib import Path

BANCO = Path(__file__).parent / "produtos.db"

conexao = sqlite3.connect(BANCO)

conexao.execute(
    "UPDATE produtos SET preco = ? WHERE id = ?", (19.5, 1))   # aula 24: muda UMA linha pelo id
conexao.execute(
    "DELETE FROM produtos WHERE id = ?", (1,))                 # aula 24: apaga UMA linha pelo id
conexao.commit()                    # aula 24: confirma as duas mudancas

print("Atualizado e removido.")
# Atualizado e removido.
conexao.close()
Grave a regra: UPDATE e DELETE sem WHERE valem pra TODAS as linhas. UPDATE produtos SET preco = 0 zera o preço de todo mundo; DELETE FROM produtos esvazia a tabela. É o mesmo susto do modo "w" da aula 17 (que trunca o arquivo). O WHERE id = ? é o que limita o estrago a uma linha só.

conceito 2 · o with

with sqlite3.connect(...): o commit automático

O mesmo with que fechava o arquivo sozinho (aula 17) serve pro banco — com um bônus: ao SAIR do with, o commit() acontece automaticamente (e um rollback desfaz tudo se der erro no meio). Daqui pra frente, todo acesso vai dentro de um with.

🎁
with sqlite3.connect(...) as conexao: é o mesmo with open(...) da aula 17, aplicado a um recurso novo. Ao sair do bloco, ele COMMITA sozinho — some a chance de esquecer o commit() do slide s15.
🛟
Bônus de segurança: se uma exceção estourar DENTRO do with, ele faz rollback — desfaz as mudanças daquele bloco. É a mesma proteção do with de arquivo (aula 17), agora garantindo que o banco não fique num estado quebrado.
com_with.py
import sqlite3
from pathlib import Path

BANCO = Path(__file__).parent / "produtos.db"

with sqlite3.connect(BANCO) as conexao:    # aula 24 (NOVO): o with da aula 17, agora num banco
    conexao.execute(
        "INSERT INTO produtos (nome, preco) VALUES (?, ?)", ("Leite", 4.2))
# ao SAIR do with, o COMMIT ja aconteceu sozinho (rollback se der erro)

print("Inserido com commit automatico.")
# Inserido com commit automatico.
Detalhe honesto: o with de sqlite3 COMMITA (ou faz rollback) ao sair, mas — diferente do with de arquivo — NÃO fecha a conexão. Nesse nível tudo bem: o Python fecha a conexão sozinho quando ela é descartada. O ganho que importa é o commit automático — por isso, daqui pra frente, todo acesso ao banco vai dentro de um with.

armadilha 1

Armadilha: esqueceu o commit()

O erro nº 1 de quem começa com banco: rodar o INSERT, ver o programa terminar sem erro nenhum — e os dados NÃO estarem lá quando reabre. Faltou confirmar a escrita.

Sem commit — os dados somem

sem_commit.py
conexao = sqlite3.connect(BANCO)
conexao.execute(
    "INSERT INTO produtos (nome, preco)"
    " VALUES (?, ?)", ("Café", 18.0))
conexao.close()          # fechou SEM commit

# rode de novo e faca um SELECT:
# o Café NAO esta la. A escrita
# foi descartada ao fechar.

Com commit (ou o with) — persiste

com_commit.py
conexao = sqlite3.connect(BANCO)
conexao.execute(
    "INSERT INTO produtos (nome, preco)"
    " VALUES (?, ?)", ("Café", 18.0))
conexao.commit()         # confirma a escrita
conexao.close()

# ou, melhor, deixe o with commitar:
# with sqlite3.connect(BANCO) as conexao:
#     conexao.execute(...)
É o mesmo susto de esquecer o salvar_todos() na aula 23: a lista mudou na memória, mas o disco não soube. Regra: toda escrita (INSERT/UPDATE/DELETE) precisa de commit() — ou use o with do slide s14, que confirma sozinho.

armadilha 2

Armadilha: f-string no SQL × placeholder ?

Tentador colar os valores direto no SQL com uma f-string. É a versão-banco do split(";") frágil da aula 23: um caractere especial quebra tudo — e abre a porta pra um ataque de verdade.

f-string no SQL — perigoso

perigo.py
# NUNCA faca isso:
sql = f"INSERT INTO produtos" \
      f" (nome, preco)" \
      f" VALUES ('{nome}', {preco})"
conexao.execute(sql)

# nome com ' quebra o SQL. E pior:
# nome = "'); DROP TABLE produtos; --"
# apaga a tabela inteira (injection).

placeholder ? — seguro

seguro.py
# sempre assim:
conexao.execute(
    "INSERT INTO produtos (nome, preco)"
    " VALUES (?, ?)",
    (nome, preco),   # valores numa tupla
)

# o banco trata o valor com seguranca,
# tenha ele aspas, ; ou o que for.
O ? não é firula: ele separa o COMANDO (o texto do SQL) dos DADOS (a tupla), e o banco nunca confunde os dois. É a mesma lição da aula 23 — não confie em juntar texto na mão. Valores de usuário sempre entram por ?, nunca por f-string.

armadilha 3 · erros do banco

Quando o banco falha: try/except no SQL

Uma operação de banco pode falhar: um dado fere uma regra da tabela (um NOT NULL), o SQL está torto, a tabela não existe. Sem proteção, o erro derruba o programa. A colheita da aula 14 resolve: o mesmo try/except, agora com as exceções do sqlite3.

🧱
É o try/except da aula 14, agora protegendo o banco. sqlite3.IntegrityError é o erro específico de ferir uma regra da tabela (um NOT NULL vazio, um valor duplicado numa coluna única) — pegue-o primeiro, com uma mensagem amigável.
🔌
sqlite3.Error é o "pai" de todos os erros de banco (o IntegrityError é uma subclasse dele) — a rede de segurança pra qualquer outra falha (SQL torto, tabela inexistente). A ordem importa: o específico vem antes do geral, como na hierarquia de exceções da aula 14.
erros_banco.py
import sqlite3
from pathlib import Path

BANCO = Path(__file__).parent / "produtos.db"   # aula 24: o mesmo .db (a tabela ja existe)

nome, preco = None, 18.0                              # nome None fere o NOT NULL da tabela

try:                                                  # aula 14: o mesmo try/except, agora no banco
    with sqlite3.connect(BANCO) as conexao:
        conexao.execute(
            "INSERT INTO produtos (nome, preco) VALUES (?, ?)", (nome, preco))
except sqlite3.IntegrityError:                        # feriu uma regra da tabela (ex.: NOT NULL)
    print("Produto inválido: fere uma regra da tabela.")
except sqlite3.Error as erro:                         # o "pai" de todos os erros de banco
    print(f"Erro no banco: {erro}")
# Produto inválido: fere uma regra da tabela.
Na prática, você embrulha assim as chamadas do repositório: um try/except em volta de repo.salvar(...) na GUI transforma um erro de banco num messagebox.showerror em vez de derrubar a janela. A lição da aula 14 vale igual pro banco: o específico (IntegrityError) antes do geral (sqlite3.Error).

conceito 5 · ORM manual

Mapear classe ↔ tabela: ORM manual

  • 🔀 O banco fala em tuplas ((1, 'Café', 18.0)); o seu programa fala em objetos (Produto). Alguém precisa TRADUZIR entre os dois — pôr o objeto na tabela e trazer a linha de volta como objeto. Essa ponte se chama ORM (mapeamento objeto-relacional).
  • 🖐️ Existem bibliotecas de ORM prontas (SQLAlchemy, Django ORM), mas hoje a gente faz na mãoORM manual — pra entender o que acontece por baixo. São só dois métodos: um pra ir e um pra voltar.
  • 📥 from_row(linha): recebe a tupla do SELECT e devolve um objeto Produto. É um @classmethod (aula 17), o irmão do from_dict daquela aula — só que a origem agora é uma linha SQL, não um dicionário.
  • 📤 to_params(): recebe o objeto e devolve a tupla de valores pros ? do INSERT. É o parente do to_dict da aula 17. Mesma ideia de sempre — objeto vira dado e volta — só muda o formato do dado: tupla/linha em vez de dict/JSON.

conceito 5 · a classe

A classe Produto + from_row + to_params

A classe do modelo com os dois tradutores. O id nasce None — o produto ainda não foi salvo, então o banco não deu um número pra ele. from_row traz a linha pra objeto; to_params leva o objeto pros ?.

📥
from_row é @classmethod (aula 17): desempacota a tupla (id, nome, preco) (aula 18) e chama cls(...) pra montar o objeto. Repare que from_row passa o id que veio do banco — o objeto lido JÁ tem id.
📤
to_params devolve só (nome, preco) — SEM o id, porque o banco gera o id sozinho no INSERT (o AUTOINCREMENT, aula 24). O __str__ com .replace(".", ",") mostra o preço no formato brasileiro: 18,00.
produto.py
class Produto:
    def __init__(self, nome, preco, id=None):
        self.id = id                 # aula 24: a chave primaria vem do banco (None ate salvar)
        self.nome = nome
        self.preco = preco

    def __str__(self):               # aula 03: representacao do objeto
        return f"#{self.id} {self.nome} — R$ {self.preco:.2f}".replace(".", ",")

    @classmethod
    def from_row(cls, linha):        # aula 24: linha (tupla do SELECT) -> objeto  (parente do from_dict, aula 17)
        id, nome, preco = linha      # aula 18: desempacota a tupla
        return cls(nome, preco, id)

    def to_params(self):             # aula 24: objeto -> tupla pros ? do INSERT  (parente do to_dict, aula 17)
        return (self.nome, self.preco)


p = Produto.from_row((1, "Café", 18.0))   # aula 24: uma tupla do banco vira objeto
print(p)                                  # aula 03: usa o __str__
# #1 Café — R$ 18,00
print(p.to_params())                      # aula 24: e o objeto vira tupla pros ?
# ('Café', 18.0)
Por que id=None por padrão? Porque um Produto recém-criado no seu código ainda não existe no banco — não tem chave primária. Só depois do salvar() o banco atribui um id (via lastrowid, s24) e a gente preenche produto.id. Objeto sem id = ainda não persistido; com id = já está na tabela.

conceito 5 · o mesmo padrão

from_dict/to_dict (17) × from_row/to_params (hoje)

Você já fez exatamente isso na aula 17 — só que o "dado" era um dicionário/JSON. A ideia é idêntica; muda só o CONTAINER do outro lado da ponte.

Aula 17 — objeto ↔ dict/JSON

aula17.py
@classmethod
def from_dict(cls, d):
    return cls(d["nome"], d["preco"])

def to_dict(self):
    return {"nome": self.nome,
            "preco": self.preco}

# o dado e um dicionario (vira JSON).
# origem/destino: um .json no disco.

Aula 24 — objeto ↔ tupla/linha SQL

aula24.py
@classmethod
def from_row(cls, linha):
    id, nome, preco = linha
    return cls(nome, preco, id)

def to_params(self):
    return (self.nome, self.preco)

# o dado e uma tupla (vira linha SQL).
# origem/destino: uma tabela no banco.
Mesmo padrão, container diferente: dict/JSON na aula 17, tupla/linha SQL hoje. Se você entendeu o from_dict/to_dict, já entendeu o from_row/to_params — é a mesma ponte objeto ↔ dado, com outro formato do lado do dado.

conceito 6 · o repositório

A classe RepositorioProduto

  • 🎁 Espalhar connect, execute, SQL e commit pelo programa todo é bagunça — igual às funções soltas que viraram App(tk.Tk) na aula 23. A solução é juntar TODO o acesso ao banco numa classe: o RepositorioProduto.
  • 🔒 Ele encapsula o banco (aula 04): esconde o SQL atrás de métodos com nomes do domínio — salvar(produto), listar(), buscar_por_id(id), atualizar(produto), remover(id). Quem chama nem sabe que existe SQL por trás.
  • 🧬 É a evolução natural do salvar_todos/carregar_todos da aula 23: aqueles eram @classmethod soltos na classe do modelo; agora viram um objeto repositório dedicado só à persistência — o modelo (Produto) fica limpo, só com os dados.
  • 🖥️ E o grande ganho pro projeto final: a GUI vai conversar com o repositório, não com o banco. Trocar .txt por SQLite (ou SQLite por outro banco) vira mexer só numa classe — a tela nem percebe. É o padrão Repositório, uma boa prática de verdade.

ao vivo 1 · vamos construir juntos

Exercício ao Vivo 1: construir o repositório do zero

Vamos escrever o RepositorioProduto inteiro, método por método, e provar que os dados SOBREVIVEM ao fechar o programa. Em camadas: cada uma entra, roda, e só então vem a próxima.

1

Camada 1 — __init__ + criar_tabela

O PASTA/BANCO ancorados com Path(__file__).parent (aula 22). O __init__ guarda o caminho e chama criar_tabela(), que roda o CREATE TABLE IF NOT EXISTS dentro de um with — a tabela existe já na criação do objeto.

2

Camada 2 — salvar + listar (C e R)

salvar(produto): INSERT com ? e produto.to_params(), pegando o id gerado com cursor.lastrowid. listar(): SELECT + [Produto.from_row(linha) for linha in ...] — o banco devolve tuplas, a list comprehension (aula 18) devolve objetos. E o buscar_por_nome(termo): o mesmo SELECT do listar, agora com WHERE nome LIKE ? — busca parcial devolvendo uma lista de objetos.

3

Camada 3 — buscar_por_id + atualizar + remover

buscar_por_id(id): fetchone() → objeto ou None. atualizar(produto): UPDATE ... WHERE id = ?. remover(id): DELETE ... WHERE id = ?. Fecha o CRUD completo dentro da classe.

4

Provar a persistência (fecha e reabre)

Um programa que cria o repo, salva 3 produtos, lista, atualiza um e remove outro — conferindo a saída. Depois, um segundo programa que só ABRE e LISTA (sem inserir): os dados continuam lá. É o payoff da aula 17, num banco de verdade.

resolução 1 · camada 1

EV1 camada 1: __init__ + criar_tabela

O repositório nasce garantindo a tabela. O __init__ guarda o caminho e chama criar_tabela(), que roda o CREATE TABLE IF NOT EXISTS dentro de um with — commit automático, sem quebrar na segunda execução.

🏗️
O __init__ chama self.criar_tabela() — assim, criar o objeto já deixa o banco pronto. O caminho vira atributo (aula 02) com valor padrão BANCO, ancorado por Path(__file__).parent (aula 22).
🛡️
criar_tabela roda o CREATE dentro de um with sqlite3.connect(...) (aula 24, s14): commita sozinho. O IF NOT EXISTS é o que deixa criar o repo toda vez sem erro.
repositorio_produto.py
import sqlite3
from pathlib import Path

PASTA = Path(__file__).parent      # aula 22: ancora o .db ao lado do script
BANCO = PASTA / "produtos.db"      # aula 17: "pasta / arquivo", agora um .db

# Camada 1: o repositorio nasce garantindo a tabela
class RepositorioProduto:
    def __init__(self, caminho=BANCO):
        self.caminho = caminho                           # aula 02: o caminho vira atributo
        self.criar_tabela()                              # garante a tabela ja na criacao

    def criar_tabela(self):
        with sqlite3.connect(self.caminho) as conexao:   # aula 24: o with commita sozinho
            conexao.execute("""
                CREATE TABLE IF NOT EXISTS produtos (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    nome TEXT NOT NULL,
                    preco REAL NOT NULL
                )
            """)                                         # IF NOT EXISTS: nao quebra na 2a execucao
Sem o IF NOT EXISTS, a segunda vez que você criasse o repo daria OperationalError: table produtos already exists — o banco recusa recriar uma tabela que já existe. Com IF NOT EXISTS, o CREATE vira "crie se ainda não houver", e criar_tabela() pode rodar em toda inicialização sem medo.

resolução 1 · camada 2

EV1 camada 2: salvar + listar + buscar_por_nome

O C e o R do CRUD. salvar insere e captura o id que o banco gerou (lastrowid); listar consulta e transforma cada tupla num objeto com from_row. São métodos da mesma classe RepositorioProduto. E o buscar_por_nome é o listar com um filtro LIKE: mesmo fetchall, mesma lista de objetos, só muda o SQL.

🆔
cursor.lastrowid devolve o id que o banco acabou de gerar no INSERT — a gente grava de volta em produto.id, então o objeto sai do salvar JÁ com sua chave primária. O valor foi passado por to_params() (aula 24, s19).
📋
listar é o SELECT virando objetos: [Produto.from_row(linha) for linha in cursor.fetchall()] — a list comprehension da aula 18 aplica o from_row (s19) em cada tupla. Sai uma lista de Produto, não de tuplas.
🔎
buscar_por_nome é gêmeo do listar: mesma list comprehension com from_row (aula 18), só que o SELECT ganha WHERE nome LIKE ? (aula 24, s12). O curinga % entra pelo VALOR — (f"%{termo}%",) — nunca colado no SQL.
repositorio_produto.py
# Camada 2: salvar (CREATE) e listar (READ) -- metodos de RepositorioProduto
class RepositorioProduto:
    # ... __init__ e criar_tabela da Camada 1 ...

    def salvar(self, produto):                        # CREATE
        with sqlite3.connect(self.caminho) as conexao:
            cursor = conexao.execute(
                "INSERT INTO produtos (nome, preco) VALUES (?, ?)",   # aula 24: ? seguro
                produto.to_params(),                  # aula 24: o objeto vira tupla
            )
            produto.id = cursor.lastrowid             # aula 24 (NOVO): o banco gerou o id
        return produto

    def listar(self):                                 # READ (todos)
        with sqlite3.connect(self.caminho) as conexao:
            cursor = conexao.execute(
                "SELECT id, nome, preco FROM produtos ORDER BY id")
            return [Produto.from_row(linha) for linha in cursor.fetchall()]   # aula 18: SELECT -> lista de objetos

    def buscar_por_nome(self, termo):                 # READ com filtro (aula 24)
        with sqlite3.connect(self.caminho) as conexao:
            cursor = conexao.execute(
                "SELECT id, nome, preco FROM produtos WHERE nome LIKE ?",   # aula 24 (NOVO): LIKE + curinga %
                (f"%{termo}%",))                       # o "%" vai no VALOR, nunca concatenado no SQL
            return [Produto.from_row(linha) for linha in cursor.fetchall()]   # aula 18: lista de objetos
O ORDER BY id garante que listar() sempre devolve na mesma ordem — importante quando a GUI (EV2) casa o índice do Listbox com o índice dessa lista (a armadilha da aula 23). O lastrowid só faz sentido logo após um INSERT: é o id da linha recém-criada.

resolução 1 · camada 3

EV1 camada 3: buscar_por_id + atualizar + remover

O resto do CRUD: buscar um (fetchone → objeto ou None), atualizar (UPDATE ... WHERE id = ?) e remover (DELETE ... WHERE id = ?). Todos por id, todos dentro de um with.

🎯
buscar_por_id usa fetchone() e checa if linha is None — sem achar, devolve None (o mesmo contrato do buscar da aula 18). Achou: from_row transforma a tupla no objeto Produto.
🔧
atualizar e remover mexem por id com WHERE id = ? (aula 24, s13) — o cinto de segurança contra alterar a tabela inteira. Note que atualizar recebe o objeto e usa produto.id pra achar a linha.
repositorio_produto.py
# Camada 3: buscar_por_id (READ um), atualizar (UPDATE) e remover (DELETE)
class RepositorioProduto:
    # ... __init__, criar_tabela, salvar e listar (Camadas 1–2) ...

    def buscar_por_id(self, id):                      # READ (um)
        with sqlite3.connect(self.caminho) as conexao:
            linha = conexao.execute(
                "SELECT id, nome, preco FROM produtos WHERE id = ?", (id,)).fetchone()
        if linha is None:                             # aula 18: como o buscar que devolve None
            return None
        return Produto.from_row(linha)                # aula 24: a tupla vira objeto

    def atualizar(self, produto):                     # UPDATE
        with sqlite3.connect(self.caminho) as conexao:
            conexao.execute(
                "UPDATE produtos SET nome = ?, preco = ? WHERE id = ?",
                (produto.nome, produto.preco, produto.id),
            )

    def remover(self, id):                            # DELETE
        with sqlite3.connect(self.caminho) as conexao:
            conexao.execute("DELETE FROM produtos WHERE id = ?", (id,))
Com essas três, o RepositorioProduto tem o CRUD completo: salvar (C), listar/buscar_por_id (R), atualizar (U), remover (D). Repare o padrão que se repete em todo método: abre um with, roda o SQL com ? e deixa o with commitar. É esse padrão único que o encapsulamento (aula 04) esconde de quem usa o repo.

resolução 1 · camada 4

EV1 camada 4: o programa que exercita

Agora usamos o repositório como um cliente qualquer usaria: cria, salva 3, lista, atualiza um e remove outro — sem escrever uma linha de SQL. A saída no terminal prova cada passo (execução com o produtos.db limpo).

🧼
Nenhum connect, execute ou SQL neste programa — só repo.salvar(), repo.listar(), repo.buscar_por_id(), repo.atualizar(), repo.remover(). É o encapsulamento (aula 04) pagando: o banco ficou escondido atrás de métodos com nome de negócio.
🖨️
A saída sai do print(p) usando o __str__ (aula 03): #1 Café — R$ 18,00. Depois do atualizar(cafe) o Café vira 19,50 e o remover(2) tira o Açúcar — a lista final tem só Café e Leite.
repositorio_produto.py
# Camada 4: o programa que exercita o repositorio (nenhum SQL aqui!)
repo = RepositorioProduto()          # cria a tabela se nao existir

repo.salvar(Produto("Café", 18.0))   # o banco atribui id 1
repo.salvar(Produto("Açúcar", 6.5))  # id 2
repo.salvar(Produto("Leite", 4.2))   # id 3

print("--- Produtos ---")
for p in repo.listar():              # aula 18: o SELECT devolve uma lista de objetos
    print(p)                         # aula 03: __str__ por linha
# --- Produtos ---
# #1 Café — R$ 18,00
# #2 Açúcar — R$ 6,50
# #3 Leite — R$ 4,20

cafe = repo.buscar_por_id(1)         # aula 24: READ de um -> objeto
cafe.preco = 19.5                    # muda o objeto na memoria
repo.atualizar(cafe)                 # aula 24: UPDATE ... WHERE id = 1
repo.remover(2)                      # aula 24: DELETE ... WHERE id = 2 (Açúcar)

print("--- Depois de atualizar e remover ---")
for p in repo.listar():
    print(p)
# --- Depois de atualizar e remover ---
# #1 Café — R$ 19,50
# #3 Leite — R$ 4,20

print('--- Busca por "ca" ---')
for p in repo.buscar_por_nome("ca"):   # aula 24 (NOVO): READ com filtro LIKE
    print(p)                           # aula 03: __str__ por linha
# --- Busca por "ca" ---
# #1 Café — R$ 19,50
A saída acima vale pra 1ª execução com o produtos.db LIMPO (apague o .db se rodar de novo). Como o INSERT sempre INSERE, reexecutar este programa inteiro acumularia linhas (Café, Açúcar, Leite de novo, com ids 4, 5, 6...). Isso é esperado — o s28 mostra o outro lado: abrir sem inserir e ver que os dados continuam lá.

resolução 1 · o código completo

Tudo junto: repositorio_produto.py

As quatro camadas no arquivo final: a classe Produto (modelo + tradutores), a classe RepositorioProduto (CRUD encapsulado) e o programa que exercita. Um mapa do que você acabou de construir.

🗺️
Leia como um mapa: a classe Produto com os tradutores from_row/to_params (aula 24, s19), a classe RepositorioProduto com o CRUD dentro de with (aula 24) e a demo que usa tudo sem tocar em SQL.
🔁
Digite este arquivo junto, não copie — os dedos aprendem o que o olho só reconhece. E rode com o produtos.db limpo pra saída bater byte a byte.
repositorio_produto.py
import sqlite3
from pathlib import Path

PASTA = Path(__file__).parent      # aula 22: ancora o .db ao lado do script
BANCO = PASTA / "produtos.db"      # aula 17: "pasta / arquivo", agora um .db

class Produto:                                    # o modelo + os tradutores (ORM manual)
    def __init__(self, nome, preco, id=None):
        self.id = id                              # aula 24: id vem do banco (None ate salvar)
        self.nome = nome
        self.preco = preco

    def __str__(self):                            # aula 03: representacao do objeto
        return f"#{self.id} {self.nome} — R$ {self.preco:.2f}".replace(".", ",")

    @classmethod
    def from_row(cls, linha):                     # aula 24: linha -> objeto (parente do from_dict, aula 17)
        id, nome, preco = linha
        return cls(nome, preco, id)

    def to_params(self):                          # aula 24: objeto -> tupla pros ? (parente do to_dict, aula 17)
        return (self.nome, self.preco)

class RepositorioProduto:                         # o CRUD encapsulado (aula 04)
    def __init__(self, caminho=BANCO):
        self.caminho = caminho
        self.criar_tabela()

    def criar_tabela(self):
        with sqlite3.connect(self.caminho) as conexao:   # aula 24: with commita sozinho
            conexao.execute("""
                CREATE TABLE IF NOT EXISTS produtos (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    nome TEXT NOT NULL,
                    preco REAL NOT NULL
                )
            """)

    def salvar(self, produto):                    # CREATE
        with sqlite3.connect(self.caminho) as conexao:
            cursor = conexao.execute(
                "INSERT INTO produtos (nome, preco) VALUES (?, ?)",   # aula 24: ? seguro
                produto.to_params(),
            )
            produto.id = cursor.lastrowid         # aula 24: o banco gerou o id
        return produto

    def listar(self):                             # READ (todos)
        with sqlite3.connect(self.caminho) as conexao:
            cursor = conexao.execute(
                "SELECT id, nome, preco FROM produtos ORDER BY id")
            return [Produto.from_row(linha) for linha in cursor.fetchall()]   # aula 18: -> lista de objetos

    def buscar_por_nome(self, termo):                 # READ com filtro (aula 24)
        with sqlite3.connect(self.caminho) as conexao:
            cursor = conexao.execute(
                "SELECT id, nome, preco FROM produtos WHERE nome LIKE ?",   # aula 24 (NOVO): LIKE + curinga %
                (f"%{termo}%",))                       # o "%" vai no VALOR, nunca concatenado no SQL
            return [Produto.from_row(linha) for linha in cursor.fetchall()]   # aula 18: lista de objetos

    def buscar_por_id(self, id):                  # READ (um)
        with sqlite3.connect(self.caminho) as conexao:
            linha = conexao.execute(
                "SELECT id, nome, preco FROM produtos WHERE id = ?", (id,)).fetchone()
        if linha is None:                         # aula 18: como o buscar que devolve None
            return None
        return Produto.from_row(linha)

    def atualizar(self, produto):                 # UPDATE
        with sqlite3.connect(self.caminho) as conexao:
            conexao.execute(
                "UPDATE produtos SET nome = ?, preco = ? WHERE id = ?",
                (produto.nome, produto.preco, produto.id),
            )

    def remover(self, id):                        # DELETE
        with sqlite3.connect(self.caminho) as conexao:
            conexao.execute("DELETE FROM produtos WHERE id = ?", (id,))

# --- demonstracao (rode com o produtos.db limpo) ---
repo = RepositorioProduto()

repo.salvar(Produto("Café", 18.0))   # id 1
repo.salvar(Produto("Açúcar", 6.5))  # id 2
repo.salvar(Produto("Leite", 4.2))   # id 3

print("--- Produtos ---")
for p in repo.listar():
    print(p)
# --- Produtos ---
# #1 Café — R$ 18,00
# #2 Açúcar — R$ 6,50
# #3 Leite — R$ 4,20

cafe = repo.buscar_por_id(1)
cafe.preco = 19.5
repo.atualizar(cafe)                 # UPDATE ... WHERE id = 1
repo.remover(2)                      # DELETE ... WHERE id = 2 (Açúcar)

print("--- Depois de atualizar e remover ---")
for p in repo.listar():
    print(p)
# --- Depois de atualizar e remover ---
# #1 Café — R$ 19,50
# #3 Leite — R$ 4,20

print('--- Busca por "ca" ---')
for p in repo.buscar_por_nome("ca"):   # aula 24 (NOVO): READ com filtro LIKE
    print(p)                           # aula 03: __str__ por linha
# --- Busca por "ca" ---
# #1 Café — R$ 19,50
Este arquivo compartilha o produtos.db com o gestor da EV2 (s34): salve produtos aqui, veja-os aparecer lá. Repare a divisão de papéis — Produto guarda os DADOS e sabe se traduzir; RepositorioProduto sabe FALAR com o banco. Modelo e persistência separados: é o que o padrão Repositório organiza.

resolução 1 · a prova da persistência

A prova: feche e reabra

A prova da persistência: rode ESTE programa DEPOIS do repositorio_produto.py. É outro arquivo, que só reabre o mesmo produtos.db e lê. Não insere nada. Os dados continuam lá, gravados em disco — provando que a persistência vive no arquivo, não no objeto.

💾
Nenhum salvar aqui — só um SELECT. E mesmo assim os produtos aparecem, com o Café já em 19,50 e sem o Açúcar (removido no s26). O banco guardou tudo em disco entre uma execução e outra: os dados sobrevivem ao fechar o programa, num banco de verdade.
🔓
Repare que este arquivo não conhece a classe RepositorioProduto — abre o .db direto com sqlite3 (s14) e lê (s11). Qualquer programa que aponte para o mesmo arquivo reencontra os dados: eles vivem no produtos.db, não na memória de quem os salvou.
consulta_produtos.py
import sqlite3
from pathlib import Path

BANCO = Path(__file__).parent / "produtos.db"   # aula 22: o mesmo .db, ao lado do script

# prova de persistencia: outro programa, NAO insere nada; so reabre o banco
with sqlite3.connect(BANCO) as conexao:         # aula 24 (s14): reabre o mesmo arquivo
    linhas = conexao.execute(
        "SELECT id, nome, preco FROM produtos ORDER BY id").fetchall()   # aula 24 (s11)

print("--- Produtos salvos ---")
for id, nome, preco in linhas:                  # percorre as linhas devolvidas
    print(f"#{id} {nome} — R$ {preco:.2f}".replace(".", ","))   # aula 03: a formatacao do __str__
# --- Produtos salvos ---
# #1 Café — R$ 19,50
# #3 Leite — R$ 4,20
É o payoff de sempre: o produtos.db sobrevive ao fechar o programa — e num formato que busca, filtra e ordena. Esta prova só LÊ (nenhum INSERT), então é segura de rodar quantas vezes quiser; já reexecutar o demo COMPLETO do s26 acumula linhas, porque o INSERT sempre insere.

conceito 6 · a tela sobre o banco

A GUI como consumidora do repositório

  • 🖥️ Agora o encaixe final: a mesma janela da aula 22/23, mas guardando no banco. A tela não sabe SQL — ela só chama self.repo.salvar(...), self.repo.listar(), self.repo.remover(...). O repositório faz a ponte com o SQLite.
  • 🧅 Pense em camadas: a tela (Tkinter) fala com o repositório (RepositorioProduto), que fala com o banco (SQLite). Cada camada só conhece a de baixo — a GUI nunca vê um execute, o repositório nunca vê um Button.
  • 🔗 É exatamente o gestor de clientes da aula 23, com uma troca cirúrgica: onde antes ela chamava Cliente.salvar_todos()/carregar_todos() (arquivo), agora chama os métodos do repositório (banco). O resto da tela — Listbox, botões, formulário — é idêntico.
  • 🎁 E o bônus que importa pro projeto final: como a tela fala com o repositório e não com o banco, trocar a camada de dados não mexe na interface. É o desacoplamento que o encapsulamento (aula 04) e o padrão Repositório (hoje) entregam.

ao vivo 2 · vamos construir juntos

Exercício ao Vivo 2: o estoque na tela

Vamos pôr o RepositorioProduto atrás de uma janela: uma app App(tk.Tk) (aula 23) que lista, adiciona e exclui produtos — cada operação persistindo no banco. Em camadas, como sempre.

1

Camada 1 — App(tk.Tk) + repo + Listbox (READ)

A app HERDA de tk.Tk (aula 23): super().__init__(), self.repo = RepositorioProduto() e um Listbox como atributo. atualizar_lista() chama self.repo.listar() e redesenha — a leitura vem do banco.

2

Camada 2 — adicionar() (CREATE)

Lê os campos, valida o preço com float(...) num try/except (aula 14) e chama self.repo.salvar(Produto(...)). Depois redesenha a lista e limpa os campos. Um clique = um INSERT persistido.

3

Camada 3 — excluir() (DELETE) + askyesno

Pega a seleção com curselection(), acha o produto por self.produtos[i] (o índice do Listbox = índice da lista, aula 23), confirma com messagebox.askyesno e chama self.repo.remover(produto.id) — DELETE pelo id do banco.

resolução 2 · camada 1

EV2 camada 1: App(tk.Tk) + repo + Listbox

A app é uma classe que herda de tk.Tk (aula 23). Ela guarda o repositório como atributo e uma lista self.produtos que espelha o Listbox. atualizar_lista() é a fonte única da verdade na tela: pergunta ao banco e redesenha.

🏛️
class App(tk.Tk) + super().__init__() (aula 23): self É a janela. O self.repo = RepositorioProduto() é o único ponto de contato com o banco — a tela nunca vê um connect ou execute.
🔄
atualizar_lista() faz self.produtos = self.repo.listar() (aula 24) e redesenha o Listbox (limpa + reinsere, aula 23). Guardar em self.produtos é o que deixa o índice do Listbox casar com o objeto na hora de excluir.
gestor_produtos.py
import tkinter as tk
from tkinter import messagebox
# (Produto e RepositorioProduto vêm do mesmo arquivo -- ver s34)

class App(tk.Tk):
    def __init__(self):
        super().__init__()                       # aula 23: a app herda de tk.Tk (vem PRIMEIRO)
        self.title("Estoque")
        self.geometry("380x460")
        self.repo = RepositorioProduto()         # aula 24: a GUI fala com o repo, nao com o SQL
        self.produtos = []                       # aula 18: a lista espelha o Listbox
        self.total_texto = tk.StringVar()        # aula 23: StringVar do total
        self._montar_widgets()
        self.atualizar_lista()                   # ja abre mostrando o que ha no banco

    def atualizar_lista(self):                   # READ
        self.produtos = self.repo.listar()       # aula 24: o SELECT devolve a lista de objetos
        self.lista.delete(0, tk.END)             # aula 23: limpa o Listbox
        for p in self.produtos:                  # aula 18: percorre os objetos
            self.lista.insert(tk.END, str(p))    # aula 03: __str__ por linha
        self.total_texto.set(f"Total: {len(self.produtos)} produto(s)")   # aula 18: len()
É o mesmo esqueleto do gestor de clientes da aula 23 — App(tk.Tk), Listbox, StringVar — com UMA troca: onde antes vinha Cliente.carregar_todos() (arquivo), agora vem self.repo.listar() (banco). O padrão Repositório permitiu trocar a fonte dos dados sem mudar a estrutura da tela.

resolução 2 · camada 2

EV2 camada 2: adicionar() (CREATE)

O observer do botão Adicionar. Valida antes de agir (aula 14): campos vazios ou preço não-numérico viram um messagebox.showerror e um return. Só com dados bons ele chama self.repo.salvar(...), redesenha e limpa os campos.

🛡️
A validação vem primeiro (aula 14): campos vazios → showerror + return; float(...) num try/except ValueError pega o preço não-numérico. O replace(",", ".") aceita o aluno digitando 18,50 no padrão brasileiro.
Só depois de validar: self.repo.salvar(Produto(nome, preco)) (aula 24) faz o INSERT no banco. Em seguida atualizar_lista() relê do banco e redesenha — a tela sempre mostra o que ESTÁ no banco, não um palpite.
gestor_produtos.py
class App(tk.Tk):
    # ... __init__ e atualizar_lista da Camada 1 ...

    def adicionar(self):                         # CREATE
        nome = self.campo_nome.get()             # aula 22: le o Entry
        preco_texto = self.campo_preco.get()
        if nome == "" or preco_texto == "":      # aula 14: valida na entrada
            messagebox.showerror("Campos vazios", "Preencha nome e preço.")
            return                               # aula 14: sai antes de tocar no banco
        try:
            preco = float(preco_texto.replace(",", "."))   # aula 14: texto -> numero pode falhar
        except ValueError:
            messagebox.showerror("Preço inválido", "O preço deve ser um número.")
            return
        self.repo.salvar(Produto(nome, preco))   # aula 24: o repositorio faz o INSERT
        self.atualizar_lista()                   # aula 24: relê do banco e redesenha
        self.campo_nome.delete(0, tk.END)        # aula 22: limpa os campos
        self.campo_preco.delete(0, tk.END)
Repare que adicionar() não mexe no Listbox diretamente: ele salva no banco e chama atualizar_lista(), que relê. Essa disciplina — mudou o banco? releia — é o que mantém tela e dados em sincronia, o mesmo princípio do preencher_lista() da aula 23, agora com o banco como fonte da verdade.

resolução 2 · camada 3

EV2 camada 3: excluir() (DELETE)

O observer do botão Excluir. Pega a seleção, acha o produto pela lista self.produtos (o índice do Listbox É o índice da lista, aula 23), confirma com askyesno e só então chama self.repo.remover(produto.id) — DELETE pelo id do banco.

🎯
self.produtos[selecao[0]] acha o objeto: o índice do Listbox É o índice da lista, porque atualizar_lista() povoou os dois na mesma ordem (aula 23). Daí sai o produto.id — a chave primária que o remover precisa.
messagebox.askyesno(...) (aula 23) barra a ação destrutiva: só com o "Sim" o self.repo.remover(produto.id) (aula 24) roda o DELETE. Clicou "Não" → return, nada muda. Depois, atualizar_lista() relê o banco.
gestor_produtos.py
class App(tk.Tk):
    # ... __init__, atualizar_lista e adicionar (Camadas 1–2) ...

    def excluir(self):                           # DELETE
        selecao = self.lista.curselection()      # aula 23: indices marcados
        if not selecao:                          # aula 01: nada selecionado?
            messagebox.showerror("Sem seleção", "Selecione um produto na lista.")
            return                               # aula 14: sai antes de mexer no banco
        produto = self.produtos[selecao[0]]      # aula 23: indice do Listbox == indice da lista
        if not messagebox.askyesno("Excluir", f"Excluir {produto.nome}?"):   # aula 23: confirma
            return                               # o usuario clicou Nao
        self.repo.remover(produto.id)            # aula 24: DELETE pelo id do banco
        self.atualizar_lista()                   # aula 24: relê e redesenha
O remover recebe produto.id (a chave do banco), não o índice do Listbox — são coisas diferentes. O índice serve pra ACHAR o objeto na lista; o id serve pra achar a LINHA no banco. Confundir os dois é a armadilha clássica: sempre delete pelo id, nunca pela posição na tela.

resolução 2 · o código completo

Tudo junto: gestor_produtos.py

O arquivo final: o mesmo Produto e RepositorioProduto da EV1, mais a App(tk.Tk) com o CRUD visual. A GUI da aula 23 sobre o banco de hoje — sem uma linha de SQL na camada da tela.

🧅
Três camadas num arquivo: Produto (dados), RepositorioProduto (banco) e App(tk.Tk) (tela). A tela só chama métodos do repo — salvar, listar, remover. Nenhum execute vive dentro da App: encapsulamento (aula 04) na prática.
🧩
O _montar_widgets segue a regra da aula 22: um gerente por container — o form usa grid, a janela usa pack. Como cada gerente está num container diferente, não há o TclError de misturar layout.
gestor_produtos.py
import tkinter as tk
from tkinter import messagebox
import sqlite3
from pathlib import Path

PASTA = Path(__file__).parent      # aula 22: ancora o .db ao lado do script
BANCO = PASTA / "produtos.db"      # aula 17: "pasta / arquivo", agora um .db

class Produto:                                    # modelo + tradutores (aula 24)
    def __init__(self, nome, preco, id=None):
        self.id = id
        self.nome = nome
        self.preco = preco

    def __str__(self):                            # aula 03
        return f"#{self.id} {self.nome} — R$ {self.preco:.2f}".replace(".", ",")

    @classmethod
    def from_row(cls, linha):
        id, nome, preco = linha
        return cls(nome, preco, id)

    def to_params(self):
        return (self.nome, self.preco)

class RepositorioProduto:                         # CRUD encapsulado (aula 04)
    def __init__(self, caminho=BANCO):
        self.caminho = caminho
        self.criar_tabela()

    def criar_tabela(self):
        with sqlite3.connect(self.caminho) as conexao:
            conexao.execute("""
                CREATE TABLE IF NOT EXISTS produtos (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    nome TEXT NOT NULL,
                    preco REAL NOT NULL
                )
            """)

    def salvar(self, produto):
        with sqlite3.connect(self.caminho) as conexao:
            cursor = conexao.execute(
                "INSERT INTO produtos (nome, preco) VALUES (?, ?)", produto.to_params())
            produto.id = cursor.lastrowid
        return produto

    def listar(self):
        with sqlite3.connect(self.caminho) as conexao:
            cursor = conexao.execute("SELECT id, nome, preco FROM produtos ORDER BY id")
            return [Produto.from_row(linha) for linha in cursor.fetchall()]

    def remover(self, id):
        with sqlite3.connect(self.caminho) as conexao:
            conexao.execute("DELETE FROM produtos WHERE id = ?", (id,))

class App(tk.Tk):                                 # a app inteira herda de tk.Tk (aula 23)
    def __init__(self):
        super().__init__()
        self.title("Estoque")
        self.geometry("380x460")
        self.repo = RepositorioProduto()          # aula 24: a GUI fala com o repo
        self.produtos = []
        self.total_texto = tk.StringVar()         # aula 23: StringVar do total
        self._montar_widgets()
        self.atualizar_lista()

    def _montar_widgets(self):
        # formulario: um Frame com grid (aula 22: um gerente por container)
        form = tk.Frame(self)
        form.pack(padx=10, pady=10)
        tk.Label(form, text="Nome:").grid(row=0, column=0, sticky="e")
        self.campo_nome = tk.Entry(form, width=22)
        self.campo_nome.grid(row=0, column=1, padx=4, pady=2)
        tk.Label(form, text="Preço:").grid(row=1, column=0, sticky="e")
        self.campo_preco = tk.Entry(form, width=22)
        self.campo_preco.grid(row=1, column=1, padx=4, pady=2)
        tk.Button(form, text="Adicionar", command=self.adicionar).grid(
            row=2, column=0, columnspan=2, pady=6)
        # a lista e o total: na janela, com pack
        self.lista = tk.Listbox(self, width=44, height=12)   # aula 23: o Listbox
        self.lista.pack(padx=10, pady=5)
        tk.Label(self, textvariable=self.total_texto).pack()   # aula 23: rotulo ligado ao StringVar
        tk.Button(self, text="Excluir selecionado", command=self.excluir).pack(pady=8)

    def atualizar_lista(self):                    # READ
        self.produtos = self.repo.listar()        # aula 24: SELECT -> lista de objetos
        self.lista.delete(0, tk.END)
        for p in self.produtos:
            self.lista.insert(tk.END, str(p))     # aula 03: __str__ por linha
        self.total_texto.set(f"Total: {len(self.produtos)} produto(s)")

    def adicionar(self):                          # CREATE
        nome = self.campo_nome.get()
        preco_texto = self.campo_preco.get()
        if nome == "" or preco_texto == "":       # aula 14: valida na entrada
            messagebox.showerror("Campos vazios", "Preencha nome e preço.")
            return
        try:
            preco = float(preco_texto.replace(",", "."))   # aula 14: texto -> numero
        except ValueError:
            messagebox.showerror("Preço inválido", "O preço deve ser um número.")
            return
        self.repo.salvar(Produto(nome, preco))    # aula 24: INSERT via repo
        self.atualizar_lista()
        self.campo_nome.delete(0, tk.END)
        self.campo_preco.delete(0, tk.END)

    def excluir(self):                            # DELETE
        selecao = self.lista.curselection()
        if not selecao:                           # aula 01: nada selecionado?
            messagebox.showerror("Sem seleção", "Selecione um produto na lista.")
            return
        produto = self.produtos[selecao[0]]       # aula 23: indice do Listbox == indice da lista
        if not messagebox.askyesno("Excluir", f"Excluir {produto.nome}?"):
            return
        self.repo.remover(produto.id)             # aula 24: DELETE pelo id do banco
        self.atualizar_lista()

app = App()                                       # aula 23: cria a app
app.mainloop()                                    # aula 22: loop de eventos
# cadastre pelo formulario; selecione uma linha e Excluir (com confirmacao):
# tudo persiste no produtos.db -- feche e reabra, os dados continuam la
Este gestor compartilha o produtos.db com o repositorio_produto.py da EV1: rode a demo lá, abra este e os produtos aparecem na tela. Deixei o atualizar/editar de fora da App de propósito — é o exercício-desafio (s42). O coração (listar, adicionar, excluir persistindo no banco) está todo aqui.

resolução 2 · o resultado

A janela do gestor de produtos

O formulário Nome/Preço em cima, o Listbox com os produtos no formato #id nome — R$ preço, o total embaixo e o botão Excluir selecionado. Cada linha veio do banco; cada ação volta pra ele.

Screenshot de uma janela Tkinter com o título 'Estoque': no topo um formulário com os campos Nome e Preço e um botão Adicionar; no meio uma lista (Listbox) com produtos cadastrados no formato '#1 Café — R$ 19,50'; abaixo o total de produtos; e na base o botão 'Excluir selecionado'.
Ao lado do gestor_produtos.py fica o produtos.db — o arquivo do banco, criado na primeira execução. Diferente do clientes.txt (que você abre e lê no editor), o .db é binário: você não lê a olho nu, mas o SQLite (e o repositório) leem por você. Feche a janela e reabra: os produtos continuam lá.

fechando o arco

O desafio: trocar a persistência

A grande lição de hoje em uma imagem: a MESMA GUI, duas camadas de dados diferentes. Trocar uma pela outra não toca na tela — porque a tela fala com uma abstração (arquivo ou repositório), não com o disco.

Aula 23 — persistência em .txt

  • 🗂️ Cliente.salvar_todos() / carregar_todos() liam e regravavam o clientes.txt.
  • 🗂️ Cada mudança reescrevia o arquivo INTEIRO em modo "w".
  • 🗂️ Sem busca, sem tipos, split(";") frágil, sem relações.
  • 🗂️ Bom pra dezenas de registros simples.

Aula 24 — persistência em SQLite

  • 🗄️ RepositorioProduto faz o CRUD com SQL: só a linha que mudou é tocada.
  • 🗄️ Colunas tipadas, busca (WHERE), ordenação (ORDER BY), chave primária.
  • 🗄️ Placeholder ? seguro; dados sobrevivem e escalam.
  • 🗄️ A MESMA App(tk.Tk) — trocou só a classe de persistência.
Esse é o desafio de casa (s42): pegar o gestor de clientes da aula 23 e trocar o .txt por um RepositorioCliente SQLite, sem mudar a interface. Se as camadas estão bem separadas, é uma troca cirúrgica — e é assim que sistemas de verdade evoluem.

recapitulando

Glossário rápido de hoje

item detalhe
banco relacional / tabela Guarda dados em tabelas de linhas e colunas tipadas. Paralelo POO: linha = objeto, coluna = atributo, tabela = coleção (aula 18). Cada linha tem uma chave primária (id) única.
sqlite3 Módulo que já vem com o Python (como o tkinter). Um banco inteiro num arquivo .db, sem servidor. import sqlite3 e pronto.
connect() / cursor / execute() sqlite3.connect(caminho) abre (ou cria) o .db e devolve a conexão; conexao.execute(sql, valores) roda um comando SQL e devolve um cursor com os resultados.
commit() e close() commit() confirma as escritas no disco (sem ele, nada persiste); close() fecha a conexão. É o mesmo abrir → usar → fechar do arquivo (aula 17).
with sqlite3.connect(...) O with da aula 17 no banco: COMMITA sozinho ao sair (e faz rollback se der erro). Não fecha a conexão (o Python fecha ao descartar), mas elimina o esquecimento do commit().
CREATE TABLE (IF NOT EXISTS) Desenha a tabela e os tipos das colunas. O IF NOT EXISTS deixa rodar de novo sem o OperationalError: table produtos already exists.
INSERT / SELECT / UPDATE / DELETE O CRUD em SQL: Create (INSERT), Read (SELECT), Update (UPDATE), Delete (DELETE). UPDATE e DELETE quase sempre pedem WHERE — sem ele, atingem a tabela inteira.
placeholder ? Marca a posição de um valor no SQL; os valores vão numa tupla à parte no execute(sql, valores). Seguro contra aspas, ; e injection — nunca use f-string pra valores.
fetchall() / fetchone() Leem o resultado de um SELECT: fetchall() devolve uma LISTA de tuplas (aula 18); fetchone() devolve UMA tupla, ou None se não achar.
INTEGER PRIMARY KEY AUTOINCREMENT / lastrowid A coluna id que o banco preenche sozinho a cada INSERT. cursor.lastrowid devolve o id recém-gerado — a gente grava em produto.id logo após salvar.
ORM manual (from_row / to_params) A ponte objeto ↔ tabela feita na mão. from_row(linha) transforma a tupla do SELECT em objeto (parente do from_dict, aula 17); to_params() transforma o objeto na tupla dos ? (parente do to_dict).
RepositorioProduto (CRUD encapsulado) Classe que junta todo o acesso ao banco atrás de métodos do domínio (salvar, listar, buscar_por_id, atualizar, remover). Esconde o SQL (encapsulamento, aula 04); a GUI fala com ela, não com o banco.
WHERE A cláusula que escolhe QUAL linha do SELECT/UPDATE/DELETE. WHERE id = ? é o cinto de segurança: sem WHERE, a operação vale pra tabela toda.

O que dominamos hoje

  • Sei por que um banco supera o .txt: busca, tipos, alterar só a linha que mudou e relações — os quatro furos do arquivo da aula 23 resolvidos.
  • Uso o sqlite3 (que já vem com o Python): connect() abre/cria o .db, execute() roda o SQL, commit() grava, close() fecha — e o with commita sozinho.
  • Escrevo o SQL como string: CREATE TABLE pra desenhar a tabela, e INSERT/SELECT/UPDATE/DELETE pro CRUD, sempre com WHERE id = ? no que altera uma linha.
  • Passo valores por placeholder ? (numa tupla), NUNCA por f-string — o jeito seguro, a versão-banco do split(";") frágil.
  • Mapeio classe ↔ tabela na mão (ORM manual): from_row transforma a tupla em objeto e to_params transforma o objeto na tupla dos ? — parentes do from_dict/to_dict da aula 17.
  • Sei que o SELECT devolve uma lista de objetos (via from_row, aula 18) e que buscar_por_id devolve um objeto ou None (o contrato do buscar que não acha).
  • Encapsulo todo o CRUD numa classe RepositorioProduto (aula 04): a GUI chama repo.salvar()/listar()/remover() e nunca vê SQL — trocar a camada de dados não toca na tela.
  • Lembro que toda escrita precisa de commit() (ou do with): sem ele, o INSERT roda, o programa termina sem erro, mas os dados somem ao reabrir.

exercícios

Mãos à obra: os exercícios

Cinco exercícios em três níveis, todos INDIVIDUAIS e entregues em arquivo .py (o desafio pede também um PNG da SUA janela). Comece em sala; termine em casa. Mire pelo menos um de cada nível — e teste cada operação (criar, listar, remover) antes de entregar.

01

Fácil · fundamentos do SQL

Criar a tabela e inserir/consultar com ?. Dois exercícios pra firmar o ritual connect → execute → commit → close e o placeholder seguro.

Fácil
02

Médio · objeto ↔ tabela

Persistir um Produto com from_row/to_params e escrever o RepositorioProduto com o CRUD completo, provando a persistência no terminal.

Médio
03

Desafio · trocar o .txt por SQLite

Pegar o gestor de clientes da aula 23 e trocar a persistência em arquivo por um RepositorioCliente SQLite — mantendo a GUI idêntica. Entregar .py + PNG.

Desafio

exercícios · fundamentos do SQL

Nível fácil

Sem POO ainda: só o ritual do banco e o SQL cru. Objetivo é a mão pegar o connect → execute → commit → close e o placeholder ?.

01

Criar a tabela

Num arquivo agenda.py, abra agenda.db com sqlite3.connect e rode CREATE TABLE IF NOT EXISTS contatos (id INTEGER PRIMARY KEY AUTOINCREMENT, nome TEXT, telefone TEXT); commit() e close(). Rode DUAS vezes e confirme que não quebra (o IF NOT EXISTS segura). Entregue o .py. Foco: o ritual do banco + CREATE TABLE.

02

INSERT + SELECT com ?

No mesmo agenda.db, insira 3 contatos com execute("INSERT INTO contatos (nome, telefone) VALUES (?, ?)", (nome, tel)) e commit(). Depois faça um SELECT, fetchall() e imprima cada linha num for. Entregue o .py. Foco: INSERT/SELECT com placeholder ? e fetchall().

exercícios · objeto ↔ tabela

Nível médio

Agora com POO: o objeto virando linha e a linha virando objeto, e o CRUD inteiro dentro de uma classe. É a EV1 refeita pela sua mão.

03

Persistir Produto no SQLite

Escreva a classe Produto com from_row e to_params (aula 24). Crie a tabela, salve 2 produtos com INSERT (usando to_params()) e depois SELECT + [Produto.from_row(linha) for linha in ...], imprimindo cada objeto com print(p) (o __str__). Entregue o .py. Foco: ORM manual — objeto ↔ linha.

04

Classe RepositorioProduto

Escreva o RepositorioProduto completo — criar_tabela, salvar, listar, buscar_por_nome, buscar_por_id, atualizar, remover — cada método num with sqlite3.connect(...). Teste no terminal: salve, liste, busque por parte de um nome (com WHERE nome LIKE ?), atualize um e remova outro. Prove a persistência: rode um segundo script que só abre e lista. Entregue o(s) .py. Foco: CRUD encapsulado + persistência.

exercícios · trocar a persistência

Desafio

O exercício que amarra as aulas 23 e 24: a mesma janela, outra camada de dados. Se as camadas estão bem separadas, é uma troca cirúrgica.

05

Trocar o .txt por SQLite no app anterior

Pegue o gestor_clientes.py da aula 23 (o CRUD visual que salvava em clientes.txt) e troque a persistência por SQLite: escreva um RepositorioCliente (com criar_tabela, salvar, listar, remover) e faça a GUI chamar o repositório no lugar de salvar_todos()/carregar_todos(). A interface deve ficar IDÊNTICA — muda só a camada de dados. Confira que os clientes sobrevivem ao fechar (agora num .db). Entregue o .py E um PNG da SUA janela com clientes cadastrados. Foco: desacoplar tela e persistência — o padrão Repositório na prática, o coração do projeto final.

próxima aula

Aula 25 · 09/07

Projeto final — briefing, planejamento e UML

Acabou o conteúdo novo — agora é juntar tudo. Na próxima aula começa o PROJETO FINAL: apresentação dos requisitos, formação dos grupos e escolha do tema. E antes de escrever a primeira classe, a gente modela em UML (aula 19) — o diagrama vem ANTES do código. Classes, encapsulamento, herança, polimorfismo, exceções, Tkinter e o SQLite de hoje: todas as ferramentas do curso, num sistema só, feito por você.

Projeto final Briefing & grupos UML antes do código