Apariencia
📙 Clase 31 — SQLite II: el CRUD completo
Fase 6 · Especialización: Apps de escritorio 🎯 ⬅️ Volver al índice de clases
🎯 Qué aprendí
- Update y Delete (con
WHEREyrowcount) — el CRUD queda completo. - Búsqueda con
LIKE, orden y paginación (ORDER BY,LIMIT). - El patrón GUI+BD definitivo: la BD es la fuente de la verdad, la GUI solo refleja.
📖 PARTE TEÓRICA
🔄 1. CRUD: lo que toda app hace
| Letra | Operación | SQL | Ya lo tienes |
|---|---|---|---|
| Create | crear | INSERT | ✅ Clase 30 |
| Read | leer | SELECT | ✅ Clase 30 |
| Update | modificar | UPDATE | 👇 hoy |
| Delete | borrar | DELETE | 👇 hoy |
✏️ 2. UPDATE
python
with con: # transacción siempre (Clase 28)
cur = con.execute(
"UPDATE contactos SET tel = ? WHERE id = ?",
("111-000", 1),
)
print(cur.rowcount) # 1 ← cuántas filas cambió (0 = ese id no existía)⚠️ EL error catastrófico del CRUD: olvidar el
WHERE.UPDATE contactos SET tel = '111'(sin WHERE) cambia TODAS las filas. IgualDELETE FROM contactosvacía la tabla. Regla: escribe elWHEREantes que elSET.
🗑️ 3. DELETE
python
with con:
cur = con.execute("DELETE FROM contactos WHERE id = ?", (2,))
print(cur.rowcount) # 1 ← borrada; 0 significaría "no existía"rowcount te deja avisar honesto en la GUI: "Contacto eliminado" vs "No se encontró".
🔎 4. Búsqueda con LIKE, orden y paginación
python
# LIKE con comodines: % = "lo que sea" (verificado: encuentra Mariana y Marcos)
texto = "mar"
filas = con.execute(
"SELECT nombre FROM contactos WHERE nombre LIKE ?",
(f"%{texto}%",), # el comodín va EN el parámetro
).fetchall()
# [('Mariana',), ('Marcos',)] ← case-insensitive en ASCII
# Contar, ordenar, paginar:
total = con.execute("SELECT COUNT(*) FROM contactos").fetchone()[0]
pagina = con.execute(
"SELECT nombre FROM contactos ORDER BY nombre LIMIT ? OFFSET ?",
(10, 0), # 10 por página, página 1
).fetchall()🗄️ ¿Recuerdas el slicing para paginar (Clase 06)?
LIMIT/OFFSETes lo mismo pero la BD solo trae lo que se muestra — la versión que escala.
🧪 Tip de entrevista: "¿Qué hace
LIKE '%ana%'?" → Coincidencia parcial: contiene "ana" en cualquier posición.'ana%'= empieza con;'%ana'= termina con.
🖥️ EN TU APP DE ESCRITORIO — el patrón definitivo
La BD es la fuente de la verdad. La GUI nunca "recuerda" datos: cada cambio va a la BD y se repinta desde la BD. Es tu patrón "redibujar desde la lista", con la lista viviendo en el disco:
python
# datos.py — completa el CRUD
def actualizar_contacto(con, id: int, campos: dict) -> bool:
with con:
cur = con.execute(
"UPDATE contactos SET nombre=:nombre, tel=:tel WHERE id=:id",
{**campos, "id": id}) # dict del formulario + id (Clase 09!)
return cur.rowcount > 0
def eliminar_contacto(con, id: int) -> bool:
with con:
return con.execute("DELETE FROM contactos WHERE id=?", (id,)).rowcount > 0
def buscar_contactos(con, texto: str = "") -> list:
return con.execute(
"SELECT * FROM contactos WHERE nombre LIKE ? ORDER BY nombre",
(f"%{texto}%",)).fetchall()python
# ui — búsqueda EN VIVO: cada tecla re-consulta y repinta
class App(ctk.CTk):
def _crear_widgets(self):
self.buscador = ctk.CTkEntry(self, placeholder_text="🔎 Buscar…")
self.buscador.pack(fill="x", padx=10)
self.buscador.bind("<KeyRelease>", lambda e: self._pintar()) # cada tecla
def _pintar(self):
for w in self.lista.winfo_children():
w.destroy()
filas = datos.buscar_contactos(self.con, self.buscador.get()) # ← LA BD filtra
for fila in filas:
self._fila_widget(fila)
def _fila_widget(self, fila):
marco = ctk.CTkFrame(self.lista)
ctk.CTkLabel(marco, text=f'{fila["nombre"]} — {fila["tel"]}').pack(side="left")
ctk.CTkButton(marco, text="🗑", width=30,
command=lambda id=fila["id"]: self._eliminar(id)).pack(side="right")
marco.pack(fill="x", pady=2)
def _eliminar(self, id):
if datos.eliminar_contacto(self.con, id):
self._pintar() # repintar DESDE la BD escribe "mar" ──▶ KeyRelease ──▶ SELECT ... LIKE '%mar%' ──▶ repintar
clic 🗑 (id=3) ──▶ DELETE WHERE id=3 ──▶ repintar desde la BD💡 Nota el
lambda id=fila["id"]:— tu viejo amigo de la Clase 10. Ahora captura el ID de la BD, la forma correcta de identificar registros (no el índice de la lista, que cambia al filtrar).
🏋️ EJERCICIOS CON SOLUCIÓN
Ejercicio 1 — Marcar como hecha
En una tabla tareas(id, texto, hecha), escribe marcar_hecha(con, id) que ponga hecha=1 y devuelva si existió.
Ver solución
python
def marcar_hecha(con, id):
with con:
cur = con.execute("UPDATE tareas SET hecha = 1 WHERE id = ?", (id,))
return cur.rowcount > 0Ejercicio 2 — Limpieza selectiva
Escribe borrar_completadas(con) que elimine todas las tareas hechas y devuelva cuántas borró.
Ver solución
python
def borrar_completadas(con):
with con:
cur = con.execute("DELETE FROM tareas WHERE hecha = 1")
return cur.rowcount # aquí el WHERE filtra un GRUPO a propósitoEjercicio 3 — Detecta el desastre
¿Qué hace este código mal y cómo lo arreglas?
python
def actualizar_tel(con, nombre, tel):
con.execute(f"UPDATE contactos SET tel = '{tel}' WHERE nombre = '{nombre}'")Ver solución
python
# DOS pecados: ① f-string en SQL → inyección SQL (Clase 30);
# ② sin transacción ni commit → el cambio puede perderse.
def actualizar_tel(con, nombre, tel):
with con:
con.execute("UPDATE contactos SET tel = ? WHERE nombre = ?", (tel, nombre))❓ Preguntas y respuestas (autoevaluación)
1. ¿Qué pasa si ejecutas un UPDATE sin WHERE?
Modifica todas las filas de la tabla. (Y
DELETEsin WHERE la vacía.)
2. ¿Para qué sirve cur.rowcount?
Dice cuántas filas afectó el UPDATE/DELETE — 0 permite avisar "no se encontró".
3. ¿Cómo buscas "contiene el texto" en SQL?
WHERE campo LIKE ?con el parámetrof"%{texto}%"(comodines dentro del parámetro).
4. ¿Por qué la GUI debe repintar "desde la BD" tras cada cambio?
La BD es la fuente de la verdad: evita que pantalla y datos se desincronicen (y el filtro/orden siempre sale correcto).
📎 Apuntes relacionados
- INSERT/SELECT y placeholders → Clase 30
- El lambda captura-ID → Clase 10
- Paginar en memoria vs en BD → Clase 06 · Slicing
➡️ Siguiente
Clase 32 · Ventanas y diálogos — apps con más de una ventana.