Skip to content

📙 Clase 30 — SQLite I: crear, insertar, leer

Fase 6 · Especialización: Apps de escritorio 🎯 ⬅️ Volver al índice de clases

🎯 Qué aprendí

  • SQLite: una base de datos real, en un archivo, incluida con Python (import sqlite3).
  • CREATE TABLE, INSERT con placeholders y SELECT (fetchall/fetchone).
  • Por qué los placeholders ? no son opcionales (inyección SQL, demostrada).

📖 PARTE TEÓRICA

🗄️ 1. ¿Por qué SQLite (y no más JSON)?

JSON (Clase 20)SQLite
Buscar/filtrarcargas TODO y filtras en Pythonla BD filtra (WHERE)
Modificar 1 registroreescribes el archivo enteroUPDATE ... WHERE id=?
Miles de registroslento y frágilsu hábitat natural
Instalaciónninguna: viene con Python

SQLite guarda todo en un archivo (app.db) — sin servidor, sin configuración. Es la BD más desplegada del mundo (está en cada teléfono) y la estándar para apps de escritorio.

🏗️ 2. Conectar y crear la tabla

python
import sqlite3

con = sqlite3.connect("app.db")       # crea el archivo si no existe

con.execute("""
    CREATE TABLE IF NOT EXISTS contactos (
        id     INTEGER PRIMARY KEY AUTOINCREMENT,   -- ID automático (Clase 14!)
        nombre TEXT NOT NULL,                       -- obligatorio
        tel    TEXT,
        email  TEXT UNIQUE                          -- no admite repetidos
    )
""")
PiezaQué garantiza
IF NOT EXISTScorrer el CREATE en cada arranque sin error
PRIMARY KEY AUTOINCREMENTcada fila nace con ID único (tu Tarea.contador, pro)
NOT NULL / UNIQUEintegridad: la BD rechaza basura aunque tu código falle

Tipos de SQLite: TEXT, INTEGER, REAL (float), BLOB. No hay booleano: se usa 0/1.

➕ 3. INSERT — siempre con placeholders

python
with con:                             # transacción (Clase 28): commit automático
    con.execute(
        "INSERT INTO contactos (nombre, tel, email) VALUES (?, ?, ?)",
        ("Ana", "999-111", "ana@x.com"),          # ? posicionales (tupla)
    )
    cur = con.execute(
        "INSERT INTO contactos (nombre, tel, email) VALUES (:nombre, :tel, :email)",
        {"nombre": "Luis", "tel": "988-222", "email": "luis@x.com"},  # :nombre (dict!)
    )
    print(cur.lastrowid)              # 2  ← el ID que la BD le asignó

🗄️ Los placeholders :nombre casan con tu dict del formulario tal cual — el puente dict=fila de la Clase 04 en su forma final.

¿Por qué JAMÁS concatenar strings en SQL? Demostrado en tu venv:

python
maligno = "'; DROP TABLE contactos; --"     # entrada malintencionada del usuario

# ❌ f"SELECT ... WHERE nombre = '{maligno}'"  → ejecutaría el DROP TABLE 💀
# ✅ con placeholder es un TEXTO inofensivo:
con.execute("SELECT * FROM contactos WHERE nombre = ?", (maligno,)).fetchall()
# → []   (buscó literalmente ese nombre raro; la tabla sigue viva)

🧪 Tip de entrevista: "¿Qué es la inyección SQL y cómo se evita?" → Meter SQL en un campo de entrada para que se ejecute. Se evita con consultas parametrizadas (placeholders), nunca concatenando input del usuario en el SQL.

🔍 4. SELECT — leer

python
todas = con.execute("SELECT id, nombre FROM contactos").fetchall()
# [(1, 'Ana'), (2, 'Luis')]                 ← lista de TUPLAS (Clases 01/05)

fila = con.execute("SELECT * FROM contactos WHERE id = ?", (1,)).fetchone()
# (1, 'Ana', '999-111', 'ana@x.com')        ← UNA fila, o None si no existe

con.row_factory = sqlite3.Row               # modo dict-like
f = con.execute("SELECT * FROM contactos WHERE id = 1").fetchone()
print(f["nombre"])                          # Ana   ← por nombre de columna
print(dict(f))                              # {'id': 1, 'nombre': 'Ana', ...}
MétodoDevuelveÚsalo cuando
.fetchall()lista de filasquieres todo (pintar una lista)
.fetchone()una fila o Nonebuscas por ID / compruebas existencia
iterar el cursorfila por fila (perezoso)recorrido único de muchas filas (Clase 25)

🖥️ EN TU APP DE ESCRITORIO

La capa datos.py de la Clase 29, ahora con SQLite de verdad:

python
# datos.py — lo ÚNICO que sabe SQL en toda la app
import sqlite3
from pathlib import Path

RUTA_BD = Path.home() / ".mi_agenda" / "app.db"     # pathlib (Clase 22)

def conectar():
    RUTA_BD.parent.mkdir(exist_ok=True)
    con = sqlite3.connect(RUTA_BD)
    con.row_factory = sqlite3.Row
    con.execute("""CREATE TABLE IF NOT EXISTS contactos (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        nombre TEXT NOT NULL, tel TEXT, email TEXT UNIQUE)""")
    return con

def insertar_contacto(con, contacto: dict) -> int:
    with con:
        cur = con.execute(
            "INSERT INTO contactos (nombre, tel, email) VALUES (:nombre, :tel, :email)",
            contacto)
    return cur.lastrowid

def obtener_contactos(con) -> list:
    return con.execute("SELECT * FROM contactos ORDER BY nombre").fetchall()

Y la vista ni se entera de que hay SQL:

python
def _guardar(self):
    contacto = {"nombre": self.e_nombre.get(), "tel": self.e_tel.get(),
                "email": self.e_email.get()}
    try:
        datos.insertar_contacto(self.con, contacto)
    except sqlite3.IntegrityError:                   # el email UNIQUE (verificado)
        self.status.configure(text="⚠️ Ese email ya está registrado")
        return
    self._pintar()

🏋️ EJERCICIOS CON SOLUCIÓN

Ejercicio 1 — Tu primera tabla

Crea notas.db con tabla notas(id AUTOINCREMENT, titulo NOT NULL, texto) e inserta dos notas.

Ver solución
python
import sqlite3

con = sqlite3.connect("notas.db")
con.execute("""CREATE TABLE IF NOT EXISTS notas (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    titulo TEXT NOT NULL,
    texto TEXT)""")
with con:
    con.execute("INSERT INTO notas (titulo, texto) VALUES (?, ?)", ("Compra", "pan"))
    con.execute("INSERT INTO notas (titulo, texto) VALUES (?, ?)", ("Idea", "app gastos"))
print(con.execute("SELECT * FROM notas").fetchall())
# [(1, 'Compra', 'pan'), (2, 'Idea', 'app gastos')]

Ejercicio 2 — Buscar por ID (sin morir)

Escribe buscar_nota(con, id) que devuelva la fila o None (y no explote nunca).

Ver solución
python
def buscar_nota(con, id):
    return con.execute("SELECT * FROM notas WHERE id = ?", (id,)).fetchone()

print(buscar_nota(con, 1))     # (1, 'Compra', 'pan')
print(buscar_nota(con, 99))    # None   ← fetchone devuelve None, no explota

Ejercicio 3 — Del formulario a la fila

Simula un formulario: nota = {"titulo": "Curso", "texto": "repasar SQLite"} e insértala con placeholders :con_nombre.

Ver solución
python
nota = {"titulo": "Curso", "texto": "repasar SQLite"}
with con:
    cur = con.execute("INSERT INTO notas (titulo, texto) VALUES (:titulo, :texto)", nota)
print("ID asignado:", cur.lastrowid)    # 3

❓ Preguntas y respuestas (autoevaluación)

1. ¿Qué necesitas instalar para usar SQLite en Python?

Nada: sqlite3 viene en la librería estándar; la BD es un archivo.

2. ¿Por qué placeholders (? / :nombre) y nunca f-strings en SQL?

Por la inyección SQL: el input del usuario podría ejecutar SQL propio. El placeholder lo trata siempre como dato.

3. ¿Qué devuelve fetchone() si no hay resultados?

None (perfecto para if fila is None: ...).

4. ¿Qué hace row_factory = sqlite3.Row?

Las filas se acceden por nombre de columna (fila["nombre"]) y se convierten con dict(fila) — de vuelta a tu formato favorito.


📎 Apuntes relacionados

➡️ Siguiente

Clase 31 · SQLite II — UPDATE, DELETE, búsqueda: el CRUD completo en tu GUI.