Ya sabés escribir SQL. Ya sabés programar con objetos. Lo que falta es unir las dos cosas: que tu programa Python consulte, guarde y modifique datos en MySQL — y que todo ese trabajo viva ordenado en una sola clase en vez de desparramado por todo el proyecto.
El resto del programa no tiene por qué saber
cómo se habla con la base de datos.
Crear bases y tablas. SELECT, INSERT, UPDATE, DELETE, WHERE. Eso no cambia acá: el SQL sigue siendo el mismo SQL.
Clases, atributos, métodos, encapsulamiento. Vamos a usarlo para ordenar el acceso a datos.
Instalar librerías con pip e importarlas. Hoy instalamos una más.
Dos mundos que todavía no se hablan
Sabemos escribir SQL. Sabemos programar en Python.
¿Cómo hacemos para que
Python ejecute una consulta SQL sobre nuestra base?
Hasta hoy, esos dos mundos vivieron separados. El SQL lo escribiste en Workbench, en phpMyAdmin o en la consola de MySQL: escribías la consulta a mano y mirabas el resultado en pantalla. Python, mientras tanto, guardaba todo en listas y diccionarios que desaparecían al cerrar el programa.
Python MySQL
listas tablas
objetos filas
variables columnas
? ← no se conocen → ?
Ctrl + C.Un puente: algo que lleve una consulta escrita en Python hasta MySQL, y traiga el resultado de vuelta convertido en datos que Python entienda.
Esto se llama persistencia: que los datos sigan existiendo después de que el programa termina. Es la diferencia entre un ejercicio y una aplicación de verdad.
La librería que hace de puente
Un conector (o driver) es una librería que sabe hablar el idioma de un motor de base de datos. Traduce: recibe de Python un texto con una consulta, se lo manda a MySQL por la red, espera la respuesta y te la devuelve como listas y tuplas de Python.
pip install mysql-connector-python
pip3 o python -m pip install mysql-connector-python.import mysql.connector from mysql.connector import Error
mysql-connector-python
Así se llama el paquete en PyPI, el repositorio de donde pip lo baja.
import mysql.connector
Así se llama el módulo adentro de Python. El punto indica que connector
vive dentro de mysql.
No tienen por qué coincidir, y muchas veces no coinciden. Es normal, y es una de las primeras cosas que se buscan en la documentación de cualquier librería nueva.
| Librería | ¿Se instala? | ¿Qué te da? |
|---|---|---|
| random | No, viene con Python | Números al azar. No sale de tu programa. |
| tkinter | No, viene con Python | Ventanas. Habla con tu sistema operativo. |
| pygame | Sí, con pip | Un videojuego. Habla con pantalla, teclado y sonido. |
| mysql.connector | Sí, con pip | Habla con otro programa: el servidor MySQL. |
Existen otros conectores para MySQL (PyMySQL, mysqlclient) y conectores
para otros motores (psycopg2 para PostgreSQL, sqlite3 que viene incluido en
Python). Buscá uno y contestá: ¿el código se parece al nuestro? Vas a descubrir que sí, y eso
no es casualidad.
Necesitás MySQL corriendo en tu computadora (XAMPP, MAMP, Workbench o el servidor solo) y
una base creada. Si pip install salió bien pero después nada conecta, el problema casi
nunca es Python: es que el servidor está apagado.
Cuatro datos y un apretón de manos
Todavía no vamos a crear una clase. Primero, la forma más simple que existe: un archivo, siete líneas, y ver si el servidor nos atiende.
CREATE DATABASE tienda; USE tienda; CREATE TABLE productos ( id INT AUTO_INCREMENT PRIMARY KEY, nombre VARCHAR(100) NOT NULL, precio DECIMAL(10,2) NOT NULL ); INSERT INTO productos (nombre, precio) VALUES ('Remera negra', 25000), ('Buzo gris', 48000), ('Gorra', 12000);
import mysql.connector conexion = mysql.connector.connect( host="localhost", port=3306, user="root", password="", database="tienda" ) print(conexion) conexion.close()
<mysql.connector.connection_cext.CMySQLConnection object at 0x...>.
Feo, pero significa «estás adentro».localhost = esta misma. Mañana puede ser una IP o un dominio.3306 por defecto; si no lo ponés, se asume ese.root; en un servidor real, nunca.database por una base que no existe. Leé el error completo.3307. ¿Qué dice ahora?¿Y qué pasa si la conexión falla?
Con el código de arriba, el programa explota: aparece un
Traceback rojo y todo se corta ahí. Para un ejercicio da igual; para una aplicación
que alguien va a usar, no. Queremos enterarnos del problema sin morir en el intento.
import mysql.connector from mysql.connector import Error try: conexion = mysql.connector.connect( host="localhost", user="root", password="", database="tienda" ) print("Conexión exitosa") conexion.close() except Error as e: print(f"Error de conexión: {e}")
try = «intentá esto». except Error as e = «si MySQL se queja,
guardá la queja en e y seguí». El programa no se cae: avisa.Access denied for user 'root'@'localhost' → usuario o contraseña mal.
Can't connect to MySQL server → el servidor está apagado o el puerto no es ese.
Unknown database 'tienda' → conectaste bien, pero esa base no existe todavía.
Leer el error es la mitad del trabajo: cada mensaje te dice exactamente dónde mirar.
Volvé a romper la contraseña, pero ahora con try. ¿Ves la diferencia? Antes se
cortaba el programa; ahora imprime un mensaje y sigue vivo.
Conectarse no alcanza: hay que preguntar algo
Estar conectado es como tener la llamada establecida: todavía nadie dijo nada. Para hablar necesitamos un cursor.
El cursor es el objeto que lleva y trae. Vos le pasás la consulta, él la manda a MySQL, y después se queda con el resultado hasta que se lo pedís. Pensalo como el mozo: la conexión es estar sentado en el restaurante; el cursor es quien lleva tu pedido a la cocina y vuelve con el plato.
cursor = conexion.cursor() cursor.execute("SELECT * FROM productos") resultados = cursor.fetchall() print(resultados) cursor.close() conexion.close()
[(1, 'Remera negra', Decimal('25000.00')), (2, 'Buzo gris', Decimal('48000.00')), (3, 'Gorra', Decimal('12000.00'))]
for como
cualquier lista.for fila in resultados: print(fila[1], "-> $", fila[2])
[(1, 'Remera'), (2, 'Buzo')]
Trae todas las filas, siempre en una lista. Si no hay nada, la lista viene vacía:
[].
(1, 'Remera')
Trae una sola fila, como tupla suelta. Si no hay nada, devuelve None.
Ideal para buscar por id.
SELECT * por SELECT nombre, precio. ¿Cambia la forma de las tuplas?fetchone() en lugar de fetchall(). ¿Qué imprime?execute() o en fetchall()?type(resultados) y len(resultados).SELECT *?SELECT * o nombrar las columnas?Cuando la consulta depende de lo que escribe el usuario
¿Y si quiero buscar un producto en particular, el que el usuario escriba?
La consulta ya no es fija: una parte la pone el programa y otra parte la pone alguien
desde afuera. La tentación es armar el texto con un +. No lo hagas.
query = "SELECT * FROM productos WHERE nombre = '" + nombre + "'" cursor.execute(query)
Vos estás escribiendo SQL con las manos del usuario.
query = "SELECT * FROM productos WHERE nombre = %s" cursor.execute(query, (nombre,))
Vos escribís la consulta y el conector acomoda el dato adentro.
%s es un hueco. No es «poné acá un texto»: es «acá va un dato, y lo
vas a tratar como dato, pase lo que pase». La tupla del segundo argumento trae los
valores que llenan esos huecos, en orden.
Supongamos que el usuario escribe esto en tu buscador:
Nombre a buscar: ' OR '1'='1
+, la consulta que llega a MySQL termina siendo
WHERE nombre = '' OR '1'='1', que es siempre verdadera: devuelve la tabla
entera. Y hay textos peores que ese.Se llama inyección SQL, y es una de las fallas de seguridad más viejas y más comunes que
existen. Lo vas a ver a fondo en otra materia. Por ahora alcanza con la regla:
ningún dato que venga de afuera se pega a la consulta con +. Nunca. Va con
%s.
| Si el dato es... | Con + | Con %s |
|---|---|---|
| Remera | Funciona | Funciona |
| Camisa D'Orsay | Rompe: el apóstrofo cierra la comilla | Funciona |
| Un número | Rompe: hay que convertirlo a texto a mano | Funciona |
| Una fecha | Hay que formatearla vos | Funciona |
(nombre) no es una tupla: es simplemente nombre entre paréntesis.
La tupla de un solo elemento se escribe (nombre,), con coma final. Si te olvidás, el
error que aparece no menciona la coma por ningún lado — acordate de este cuadro.
"Gorra" con %s y fetchone().WHERE precio > %s AND precio < %s con (10000, 30000).El SQL de siempre, ahora disparado desde Python
Las consultas ya las sabés.
Lo nuevo es solamente cómo ejecutarlas desde Python.
cursor.execute("SELECT id, nombre, precio FROM productos") productos = cursor.fetchall()
commit().cursor.execute( "INSERT INTO productos (nombre, precio) VALUES (%s, %s)", (nombre, precio) ) conexion.commit()
commit().cursor.execute( "UPDATE productos SET precio = %s WHERE id = %s", (precio, id_producto) ) conexion.commit()
%s: primero el precio nuevo,
después el id. Si los das vuelta, MySQL no se queja — hace otra cosa.cursor.execute( "DELETE FROM productos WHERE id = %s", (id_producto,) ) conexion.commit()
WHERE: sin él, borrás
toda la tabla.commit() significa «confirmo estos cambios». Hasta que no lo
llamás, MySQL tiene el cambio anotado en borrador pero no lo guarda de verdad. Si cerrás la
conexión sin confirmar, se pierde todo lo que hiciste — y esto no da error, que
es lo traicionero: tu programa dice «producto agregado» y en la tabla no hay nada.
INSERT sin commit(). Mirá la tabla en Workbench: no está.commit() y repetí. Ahora sí.UPDATE, imprimí cursor.rowcount: te dice cuántas filas tocó.UPDATE con un WHERE que no matchee nada. ¿Da error o da 0?| Operación | ¿Devuelve filas? | ¿fetch? | ¿commit? |
|---|---|---|---|
| SELECT | Sí | Sí | No |
| INSERT | No | No | Sí |
| UPDATE | No | No | Sí |
| DELETE | No | No | Sí |
Guardá mentalmente esta tabla: en la sección 8 se va a transformar, casi tal cual, en los parámetros de un método.
Donde aparece la necesidad, antes que la solución
Ya sabés hacer las cuatro operaciones. Escribamos entonces el programa completo, con tres funciones nada más:
def ver_productos(): conexion = mysql.connector.connect(host="localhost", user="root", password="", database="tienda") cursor = conexion.cursor() cursor.execute("SELECT * FROM productos") datos = cursor.fetchall() cursor.close() conexion.close() return datos def agregar_producto(nombre, precio): conexion = mysql.connector.connect(host="localhost", user="root", password="", database="tienda") cursor = conexion.cursor() cursor.execute("INSERT INTO productos (nombre, precio) VALUES (%s, %s)", (nombre, precio)) conexion.commit() cursor.close() conexion.close() def borrar_producto(id_producto): conexion = mysql.connector.connect(host="localhost", user="root", password="", database="tienda") cursor = conexion.cursor() cursor.execute("DELETE FROM productos WHERE id = %s", (id_producto,)) conexion.commit() cursor.close() conexion.close()
¿Y si el programa tuviera 30 consultas en vez de 3?
30 veces connect() con los mismos cinco datos. 30 cursores. 30 close().
Un commit() que falta en la función 17. Un close() que nunca se llamó. Nadie se da cuenta hasta que falla.
Te mudás de computadora y cambia la contraseña: hay que tocar 30 lugares y no equivocarse en ninguno.
Personaje nació igual.)¿No sería mejor tener un solo lugar encargado de hablar con la base de datos?
Un lugar que sepa conectarse y sepa ejecutar una consulta.
Todo el resto del programa le pide cosas a ese lugar y no vuelve a ver un connect()
en su vida.
Ese lugar va a ser una clase, y la vamos a llamar
BasededatosControlador.
Fijate lo que acaba de pasar: primero apareció el problema, después la solución. No empezamos con una clase porque «hay que usar clases». Llegamos a la clase porque el código repetido nos empujó hasta ahí. Así nace casi todo lo que vas a aprender de ahora en adelante.
Construido de a un pedazo, sin copiar y pegar
Vamos a armarlo en cuatro pasos. Después de cada paso, el archivo tiene que
ejecutarse sin errores. Guardalo como base_de_datos.py.
class BasededatosControlador: pass
import mysql.connector from mysql.connector import Error class BasededatosControlador: def connect(): try: return mysql.connector.connect( host="localhost", port=3306, database="tienda", user="root", password="" ) except Error as e: print(f"Error de conexión: {e}") return None
Este método siempre contesta algo: si pudo conectarse, te da el objeto conexión; si no pudo,
avisa por pantalla y devuelve None. Quien lo llame va a poder preguntar
«¿me diste una conexión o no?» antes de seguir. Eso es lo que hace el paso 3.
No hay, y no es un olvido. Hasta ahora, los métodos trabajaban sobre un objeto:
goku.atacar() necesitaba saber quién es goku. Acá no hay ningún objeto
que guardar: no vamos a crear «varios controladores». Hay uno solo y se usa directamente
desde la clase, sin instanciar:
conexion = BasededatosControlador.connect()
En Python, la forma prolija de decir «este método no necesita objeto» es ponerle
@staticmethod arriba. Funciona sin él, pero con él queda declarado.
Poneselo a todos los métodos de esta clase.
@staticmethod def connect(): ...
Acá está el truco de toda la guía. Volvé a mirar la tabla del final de la sección 6: las cuatro operaciones se diferencian en tres cosas nada más. Esas tres cosas van a ser los parámetros del método.
| Parámetro | Qué pregunta | Para qué sirve |
|---|---|---|
| query | ¿Qué consulta ejecuto? | El SQL, con sus %s si hace falta. |
| params | ¿Con qué datos? | La tupla que llena los huecos. Por defecto, ninguno. |
| fetchone | ¿Traigo una fila o todas? | True = una sola. Por defecto, todas. |
| commit | ¿Esto modifica la base? | True en INSERT, UPDATE y DELETE. |
@staticmethod def ejecutar_query(query, params=None, fetchone=False, commit=False): connection = BasededatosControlador.connect() if connection: try: with connection.cursor() as cursor: cursor.execute(query, params) if commit: connection.commit() return True return cursor.fetchone() if fetchone else cursor.fetchall() except Error as e: print(f"Error en la consulta: {e}") finally: connection.close() return None if fetchone else []
connect() devolvió None, no se intenta nada.with que usás para abrir archivos.params es None, no pasa nada: la consulta no tenía huecos.True para avisar «salió bien».if escrito en una línea.return en el medio. Esto es lo que más se olvida a mano.None o lista vacía, según lo que esperaban. Nunca devuelve sorpresas.Quien llama a ejecutar_query quiere recorrer el resultado con un for.
Si ante un error devolviéramos None, ese for explotaría. Devolver
[] significa «no hubo datos» y el for simplemente no da vueltas.
Un buen método falla sin arruinarle el día a quien lo usó.
Al final del archivo, agregá estas dos líneas y ejecutalo:
productos = BasededatosControlador.ejecutar_query("SELECT * FROM productos") print(productos)
Una línea para pedir. Sin connect, sin cursor, sin close. Ese es
todo el punto.
Apagá el servidor de MySQL y volvé a ejecutar. ¿Qué imprime? ¿Se cae el programa o avisa?
¿En cuál de los dos except entró — el de connect o el de
ejecutar_query?
Del SQL suelto al vocabulario de tu aplicación
Ya podemos ejecutar cualquier consulta desde un solo lugar. Pero todavía hay algo incómodo: el SQL sigue apareciendo desparramado por el programa, cada vez que alguien quiere algo. Démosle nombre a esas cosas que el programa quiere.
@staticmethod def traer_productos(): query = "SELECT id, nombre, precio FROM productos" return BasededatosControlador.ejecutar_query(query)
@staticmethod def buscar_producto(nombre): query = """ SELECT id, nombre, precio FROM productos WHERE nombre = %s """ return BasededatosControlador.ejecutar_query( query, (nombre,), fetchone=True )
@staticmethod def agregar_producto(nombre, precio): query = "INSERT INTO productos (nombre, precio) VALUES (%s, %s)" return BasededatosControlador.ejecutar_query( query, (nombre, precio), commit=True )
commit=True en vez de
fetchone=True. El patrón ya es siempre el mismo.El resto del programa ya no necesita saber cómo se habla con MySQL. No sabe
qué es un cursor, no sabe qué es commit, no conoce la contraseña.
Solo pide lo que necesita, con palabras de tu aplicación:
productos = BasededatosControlador.traer_productos() uno = BasededatosControlador.buscar_producto("Gorra") BasededatosControlador.agregar_producto("Campera", 90000)
| Capa | Sabe | No sabe |
|---|---|---|
| traer_productos() | Qué consulta hace falta | Cómo se ejecuta |
| ejecutar_query() | Cómo se ejecuta cualquier consulta | Qué consulta le van a pasar |
| connect() | Dónde está la base y con qué usuario entrar | Qué se va a hacer adentro |
Cada una hace una cosa sola y confía en la de abajo. Si mañana cambia la
contraseña se toca connect(); si cambia el nombre de una columna se toca
traer_productos(). Un cambio, un lugar.
traer_productos, buscar_producto, agregar_producto.
No se llaman select1, query_a ni funcion2. Los nombres
cuentan qué quiere hacer tu aplicación, no cómo lo hace por dentro. Leer la lista de
métodos de esta clase debería ser como leer el índice de lo que tu programa puede hacer.
buscar_producto?Un paso atrás para mirar lo que construimos
No lo dijimos en el camino, pero lo que acabás de hacer es exactamente lo que venís practicando desde la clase 1. Contestá estas preguntas sobre tu propia clase:
Representa una cosa: tiene vida, fuerza, nombre. Creás muchos: goku,
freezer. Cada uno guarda sus datos.
Representa una responsabilidad: hablar con la base. No hay muchos, hay uno. No guarda datos propios: guarda un saber hacer.
Las dos son clases perfectamente válidas. Una clase no sirve solamente para modelar sustantivos del mundo real: también sirve para juntar en un lugar todo lo que tiene que ver con un mismo asunto.
Encapsular también significa esconder detalles
que el resto del programa no necesita conocer.
Cuando aprendiste encapsulamiento, se trataba de proteger atributos: que nadie le
ponga vida negativa a un personaje. Es la misma idea, un escalón más arriba. Acá lo que está
escondido es todo un procedimiento: la contraseña, el cursor, el commit, el cierre. Quien
usa la clase no necesita saber nada de eso, igual que vos no necesitás saber cómo funciona
print() por dentro.
A una clase así se le suele decir servicio o controlador: no representa una cosa, presta un servicio al resto del programa. Es el primer nombre nuevo de una familia que vas a seguir conociendo más adelante en la carrera. Por ahora, con la idea alcanza: un lugar, una responsabilidad.
¿Y si mañana la aplicación tuviera que guardar en un archivo de texto en vez de MySQL? Fijate
que podrías cambiar todo el interior de ejecutar_query y
connect, y el resto del programa seguiría llamando igual a
traer_productos() sin enterarse de nada. Eso es lo que hace tan valiosa esta
separación.
Sacar la contraseña del código
¿Qué tiene de malo esto?
password="123456"
Subís el proyecto a GitHub para entregarlo y tu contraseña queda a la vista de todos. Y queda en el historial aunque después la borres.
Tu compañero tiene otra contraseña de MySQL. Cada uno tendría que editar el archivo, y alguno lo va a subir cambiado.
En tu compu la base está en localhost. En el servidor, no. Mismo código, distintos datos.
Es un dato que no vive en tu programa sino en el ambiente donde el programa corre: tu computadora, la de tu compañero, el servidor. El código pregunta «¿cuál es la contraseña acá?» y cada lugar responde la suya.
import os usuario = os.getenv("DB_USER") password = os.getenv("DB_PASSWORD")
os.getenv("DB_USER") = «traeme el valor que el ambiente guardó con ese
nombre». Si no existe, devuelve None.host = os.getenv("DB_HOST", "localhost") port = int(os.getenv("DB_PORT", "3306"))
DB_HOST,
usá localhost». Así el proyecto funciona recién clonado, sin configurar nada.int(): las variables de entorno siempre son texto, y el puerto
tiene que ser número.La forma más cómoda de definirlas es un archivo llamado .env (con
punto adelante) al lado de tu programa:
DB_HOST=localhost DB_PORT=3306 DB_NAME=tienda DB_USER=root DB_PASSWORD=mi_clave_secreta
=, una variable por línea.pip install python-dotenv — la librería que sabe leer ese archivo.from dotenv import load_dotenv y después load_dotenv().os.getenv("DB_USER") encuentra lo que pusiste en el .env..env a tu archivo .gitignore. El .env nunca se sube..env.example con los nombres y sin los valores, para que el que clone el proyecto sepa qué tiene que completar.import os import mysql.connector from mysql.connector import Error from dotenv import load_dotenv load_dotenv() class BasededatosControlador: @staticmethod def connect(): try: return mysql.connector.connect( host=os.getenv('DB_HOST', 'localhost'), port=int(os.getenv('DB_PORT', '3306')), database=os.getenv('DB_NAME', 'tienda'), user=os.getenv('DB_USER', 'root'), password=os.getenv('DB_PASSWORD', '') ) except Error as e: print(f"Error de conexión: {e}") return None
.env, instalá python-dotenv y confirmá que sigue conectando.DB_NAME en el .env por una base que no existe. ¿Avisa?.env entero. ¿Sigue funcionando? ¿Por qué? (Pista: los valores por defecto.)os.getenv("DB_PASSWORD") para ver que lo está leyendo de verdad.Cuando tu aplicación deje tu computadora y se vaya a un servidor, nada del código va a cambiar: solo van a cambiar las variables de entorno de ese lugar. Es la razón por la que se hace así en todos lados. Cómo se sube una aplicación a un servidor es tema de otra guía.
Una aplicación de consola que recuerda
Una aplicación con un menú de consola que permita ver, buscar, agregar, modificar y
eliminar productos. Toda la comunicación con MySQL tiene que pasar por
BasededatosControlador: si en main.py aparece la palabra
cursor, algo salió mal.
La interfaz es de consola, a propósito. Hoy lo que estamos aprendiendo es la comunicación Python ↔ MySQL, y una ventana de por medio solo agregaría ruido. La ventana viene en la sección 14 — y vas a ver que casi no hay que tocar nada.
La clase BasededatosControlador. Sabe de MySQL. No sabe que
existe un menú.
El menú, los input() y los print(). Sabe hablar con el
usuario. No sabe nada de MySQL.
===== PRODUCTOS ===== 1. Ver productos 2. Buscar producto 3. Agregar producto 4. Modificar producto 5. Eliminar producto 6. Salir Opción: _
from base_de_datos import BasededatosControlador def mostrar_menu(): print("\n===== PRODUCTOS =====") print("1. Ver productos") print("2. Buscar producto") print("3. Agregar producto") print("4. Modificar producto") print("5. Eliminar producto") print("6. Salir") def ver_productos(): productos = BasededatosControlador.traer_productos() if not productos: print("No hay productos cargados.") return for id_p, nombre, precio in productos: print(f"[{id_p}] {nombre} — ${precio}") def agregar_producto(): nombre = input("Nombre: ") precio = input("Precio: ") if BasededatosControlador.agregar_producto(nombre, precio): print("Producto agregado.") else: print("No se pudo agregar.") # TODO: buscar_producto(), modificar_producto(), eliminar_producto() while True: mostrar_menu() opcion = input("\nOpción: ") if opcion == "1": ver_productos() elif opcion == "3": agregar_producto() elif opcion == "6": print("Hasta luego.") break else: print("Opción inválida.")
main.py no importa
mysql.connector por ningún lado.for id_p, nombre, precio in productos: desarma cada tupla en tres variables con
nombre. Es mucho más legible que fila[0], fila[1],
fila[2] — y te obliga a saber qué columnas pediste.
connect() ni un cursor fuera del controlador.%s.commit=True..env, no en el código.Se prueba así: cargá tres productos desde tu programa, cerralo, abrí Workbench y mirá la tabla. Volvé a abrir tu programa. Tienen que seguir ahí. Ese es el momento en que tu aplicación dejó de ser un ejercicio.
De a uno, y probando cada uno antes de seguir
Todos se resuelven agregando un método a BasededatosControlador y
llamándolo desde el menú. Ninguno necesita tocar ejecutar_query.
Devolver todos los productos. Ya está resuelto en la sección 9: escribilo de memoria, sin mirar, y conectalo con la opción 1 del menú.
Pista: no lleva params, no lleva fetchone, no
lleva commit.
Recibe un nombre y devuelve un solo producto. Opción 2 del menú.
LIKE para que «reme» encuentre «Remera».
Investigá cómo se arma el % del LIKE junto con el %s
del parámetro — no es tan obvio como parece.Inserta un producto nuevo. Opción 3 del menú.
commit=True."caro" en el precio? Probalo y decidí qué
hacer.Modifica un producto existente. Opción 4 del menú.
id elegir.%s del SET van
antes que el del WHERE.id no existe, no da error pero no cambia nada. ¿Cómo te enterás?
Investigá cursor.rowcount.Borra un producto. Opción 5 del menú.
WHERE esté. Probalo con la base de prueba, nunca
con datos que te importen.Agregá una tabla clientes (id, nombre,
email) y hacé que el programa maneje las dos.
traer_clientes(), agregar_cliente(), etcétera.connect()? ¿Tocaste ejecutar_query()?Para pensar, sin resolverlo todavía: si los métodos de productos y los de clientes empiezan a ser muchos, ¿te seguiría cerrando tenerlos en la misma clase? ¿Qué harías? Guardate la respuesta: es la pregunta con la que arranca la próxima etapa.
ORDER BY:
en Python o en el SQL? ¿Por qué conviene que sea en el SQL?cliente_id? ¿Podrías traer el nombre del cliente
junto con el producto en una sola consulta?executemany.El puente hacia la aplicación completa
¿Qué pasaría si mañana le ponemos a esto la interfaz que aprendimos con PyQt?
Buscar producto
Nombre: [ gorra________ ]
[ BUSCAR ]
↓
Gorra
$12.000
Donde la consola hacía print(), la ventana va a hacer
etiqueta.setText(). Donde había input(), va a haber
campo.text(). La línea del medio es exactamente la misma:
producto = BasededatosControlador.buscar_producto(nombre)
nombre = input("Nombre: ") p = BasededatosControlador.buscar_producto(nombre) print(p)
nombre = campo.text() p = BasededatosControlador.buscar_producto(nombre) etiqueta.setText(str(p))
La interfaz no tiene por qué saber
cómo funciona MySQL. Solo pide.
Y funciona en los dos sentidos: la base de datos tampoco sabe que existe una ventana. Por eso el mismo controlador sirve para una app de consola, para una de escritorio y para lo que venga después. Escribiste el controlador una vez; la interfaz podés cambiarla tres veces sin tocarlo.
Agarrá el mini proyecto de la sección 12 y reemplazá solamente main.py por
una ventana PyQt con un campo, un botón y una etiqueta. No toques base_de_datos.py.
Si lo lográs sin abrir ese archivo ni una vez, acabás de comprobar vos mismo de qué sirve
separar responsabilidades.
Las piezas ya están: falta armarlas juntas
| Capa | Cuándo la aprendiste | Qué resuelve |
|---|---|---|
| Interfaz | Clase 3 y guía de PyQt | Que una persona pueda usar tu programa. |
| Lógica | Clases 1 y 2, Arena de Combate | Las reglas: qué puede y qué no puede pasar. |
| Servicios | Esta guía | Traer y guardar datos sin ensuciar el resto. |
| Datos | Bases de Datos | Que la información sobreviva al programa. |
Hasta ahora aprendimos las piezas por separado. Cada una funcionaba sola y se entendía sola. El próximo paso es aprender a unirlas: una sola aplicación que tenga interfaz, lógica y datos, y que no se convierta en un archivo de 800 líneas imposible de tocar.
(1, 'Gorra', 12000) vuelva a ser un objeto
Producto con atributos y métodos?Cada una de esas preguntas es una clase futura. Tenerlas en la cabeza antes de que te las contesten es exactamente lo que hace que después se entiendan.
Lo que te tenés que llevar
ANTES Python
AHORA
Python
↕
MySQL
DESPUÉS
Interfaz
↓
Python
↓
Servicios
↓
MySQL
%s, nunca pegados con
+.commit(). Modificar, sí — y si falta, no hay error:
simplemente no se guardó nada.SELECT y recorrer el resultado.fetchone() y fetchall().%s y puedo explicar por qué.commit().BasededatosControlador.try, except y finally ahí adentro.Si algún casillero quedó vacío, no sigas para adelante: volvé a esa sección. Todo lo que viene después se apoya en esto, y se apoya fuerte.
Ya no hacemos solamente programas que funcionan.
Empezamos a construir
programas que pueden crecer.
Un programa que anda es el primer objetivo. Un programa que se puede modificar seis meses después, sin miedo y sin romper nada, es el oficio.