Skip to content

📙 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 WHERE y rowcount) — 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

LetraOperaciónSQLYa lo tienes
CreatecrearINSERTClase 30
ReadleerSELECT✅ Clase 30
UpdatemodificarUPDATE👇 hoy
DeleteborrarDELETE👇 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. Igual DELETE FROM contactos vacía la tabla. Regla: escribe el WHERE antes que el SET.

🗑️ 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/OFFSET es 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 > 0

Ejercicio 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ósito

Ejercicio 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 DELETE sin 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ámetro f"%{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

➡️ Siguiente

Clase 32 · Ventanas y diálogos — apps con más de una ventana.