Um banco relacional guarda dados em tabelas de linhas e colunas tipadas. Na POO: linha = objeto, coluna = atributo, tabela = coleção (aula 18), e cada linha tem uma chave primária única (id). O módulo sqlite3 já vem com o Python (como o tkinter) e guarda um banco inteiro num arquivo .db, sem servidor.
import sqlite3
from pathlib import Path
BANCO = Path(__file__).parent / "produtos.db" # aula 22
with sqlite3.connect(BANCO) as conexao: # commita sozinho ao sair (aula 17)
conexao.execute("""
CREATE TABLE IF NOT EXISTS produtos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nome TEXT NOT NULL,
preco REAL NOT NULL
)
""")
conexao.execute(
"INSERT INTO produtos (nome, preco) VALUES (?, ?)", # ? seguro
("Café", 18.0))
for linha in conexao.execute("SELECT id, nome, preco FROM produtos"):
print(linha) # cada linha e uma tupla (id, nome, preco) O grande erro de quem começa: esquecer o commit(). O INSERT roda, o programa termina sem erro, mas os dados somem ao reabrir. Ou use commit() explícito, ou o with que commita por você.
O banco fala em tuplas; o seu programa fala em objetos. Mapear um no outro é o ORM (mapeamento objeto-relacional). Hoje fazemos na mão, com dois métodos: from_row(linha) traz a tupla do SELECT pra objeto, e to_params() leva o objeto pra tupla dos ? do INSERT. É a mesma ideia do from_dict/to_dict da aula 17 — só muda o formato do dado (tupla/linha em vez de dict/JSON).
class Produto:
def __init__(self, nome, preco, id=None):
self.id = id # None ate salvar (o banco atribui)
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): # tupla do SELECT -> objeto
id, nome, preco = linha
return cls(nome, preco, id)
def to_params(self): # objeto -> tupla pros ? do INSERT
return (self.nome, self.preco) Objeto sem id = ainda não persistido; objeto com id = já está na tabela. O from_row/to_params é a alfândega entre os dois mundos — nada entra ou sai do banco sem passar por eles.
Espalhar connect, execute e SQL pelo programa é bagunça. O padrão Repositório junta todo o acesso ao banco numa classe (RepositorioProduto) e o expõe por métodos com nomes do domínio: salvar, listar, buscar_por_id, atualizar, remover. Quem usa nunca vê SQL — é o encapsulamento da aula 04. É a evolução do salvar_todos/carregar_todos da aula 23, de @classmethods soltos para um objeto dedicado à persistência.
class RepositorioProduto:
def __init__(self, caminho=BANCO):
self.caminho = caminho
self.criar_tabela() # garante a tabela na criacao
def salvar(self, produto): # CREATE
with sqlite3.connect(self.caminho) as conexao:
cursor = conexao.execute(
"INSERT INTO produtos (nome, preco) VALUES (?, ?)",
produto.to_params())
produto.id = cursor.lastrowid # 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()]
def remover(self, id): # DELETE
with sqlite3.connect(self.caminho) as conexao:
conexao.execute("DELETE FROM produtos WHERE id = ?", (id,)) Modelo e persistência separados: Produto guarda os dados e sabe se traduzir; RepositorioProduto sabe falar com o banco. É essa separação que deixa a mesma App(tk.Tk) rodar sobre um .txt (aula 23) ou um .db (hoje) sem mudar.
Ú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.
roteiro da aula
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.
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.
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.
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
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). from_dict/to_dict daquela aula, o split(";") desfazendo a linha. Hoje o mesmo objeto vira uma LINHA numa tabela. sqlite3 — que, igual ao Tkinter, já vem instalado com o Python. Tudo em volta é colheita: encapsulamento (04), listas (18), __str__ (03). o problema de hoje
.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. 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. .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
produtos tem uma linha por produto e as colunas id, nome, preco. 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. list da aula 18). Guardar um objeto = inserir uma linha; carregar = ler linhas de volta. 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
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.
.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
.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. sqlite3.connect(caminho) abre (ou CRIA) o arquivo .db e devolve uma conexão; conexao.execute(sql) roda um comando SQL; conexao.close() fecha. conexao.commit(). Esqueceu o commit()? É como esquecer o salvar_todos() da aula 23 — rodou, pareceu funcionar, mas nada persistiu. .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
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).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 conceito 3 · os comandos
| 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
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.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).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. conceito 3 · SELECT
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.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 conceito 3 · SELECT com filtro
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).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)] 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
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.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() conceito 2 · o with
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.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.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. armadilha 1
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.
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. 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(...) 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
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.
# 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). # 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. ? 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
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.
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.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. 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
(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). 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 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.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) conceito 5 · o mesmo padrão
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.
@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. @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. 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
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. salvar(produto), listar(), buscar_por_id(id), atualizar(produto), remover(id). Quem chama nem sabe que existe SQL por trás. 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. .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
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.
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.
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.
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.
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
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.
__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.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 resolução 1 · camada 2
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.# 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 resolução 1 · camada 3
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.# 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,)) resolução 1 · camada 4
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).
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.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.# 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 resolução 1 · o código completo
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.
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.produtos.db limpo pra saída bater byte a byte.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 resolução 1 · a prova da persistência
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.
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.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.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 conceito 6 · a tela sobre o banco
self.repo.salvar(...), self.repo.listar(), self.repo.remover(...). O repositório faz a ponte com o SQLite. execute, o repositório nunca vê um Button. 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. ao vivo 2 · vamos construir juntos
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.
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.
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.
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
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.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() resolução 2 · camada 2
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.
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.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.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) resolução 2 · camada 3
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.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 resolução 2 · o código completo
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.
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._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.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 resolução 2 · o resultado
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.
fechando o arco
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.
.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
| 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. |
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.
Criar a tabela e inserir/consultar com ?. Dois exercícios pra firmar o ritual connect → execute → commit → close e o placeholder seguro.
Persistir um Produto com from_row/to_params e escrever o RepositorioProduto com o CRUD completo, provando a persistência no terminal.
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.
exercícios · fundamentos do SQL
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 ?.
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.
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
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.
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.
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
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.
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.
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ê.