Blog
Librerías a instalar en este capítulo:
- Flask SQLAlchemy
- Flask Migrate
Una base de datos es un conjunto de datos pertenecientes a un mismo contexto y almacenados sistemáticamente para su posterior uso. En las aplicaciones web y en el área de la tecnología en general, son sumamente utilizadas. El inventario de una tienda, el sistema donde un colegio o universidad almacena las notas/nombres/grados de sus alumnos, entre otros, son ejemplos de bases de datos en la vida real. Naturalmente, al desarrollar aplicaciones web con Flask, se van a necesitar tarde o temprano. Como seguramente se esperaba, Flask no tiene soporte nativo de bases de datos (intencionalmente), pero esta practica es beneficiosa para los desarrolladores ya que permite que el desarrollador escoja la de su preferencia según las necesidades de la aplicación.
Existen muchas opciones de bases de datos en Python, muchas de ellas compatibles con Flask ya que favorecen una mejor integración con la aplicación. Como las bases de datos que siguen un modelo relacional tienen datos estructurados como listas de usuarios, publicaciones de los usuarios, etc, utilizaremos una de este tipo.
En la parte anterior introducimos el uso de extensión con Flask-WTF. En este introduciremos dos extensiones mas en nuestra aplicación. La primera será Flask-SQLAlchemy, la cual provee una interfaz bastante simple con el paquete SQLAlchemy.
Para instalar Flask-SQLAlchemy en nuestra app, teniendo el entorno virtual activo, ejecutamos el siguiente comando:
pip install flask-sqlalchemy
Migración de Bases de Datos
La migración o “actualización” de una base de datos, es un tema bastante delicado que rara vez es estudiado de forma adecuada. Esto puede traer problemas serios en la aplicación, ya que las bases de datos relacionales giran en torno a datos bien estructurados. Si las bases de datos no reciben las migraciones de forma correcta, tendremos una grave falla en la app.
La segunda extensión que presentaremos, cubrirá este tema, siendo dicha extensión Flask-Migrate, la cual es un framework para migrar las bases de datos de SQLAlchemy. Trabajar con las migraciones de las bases de datos, agregan un paso extra a la activación de las bases de datos, pero es un pequeño precio comparado con el gran problema que puede ahorrar este paso.
Para instalar pip install, ejecutamos el siguiente comando desde nuestro entorno virtual:
pip install flask-migrate
Configuración de Flask-SQLAlchemy
Durante el desarrollo, utilizaremos una base de datos SQLite. Estas bases de datos son las mas convenientes para desarrollar aplicaciones pequeñas y no tan pequeñas, ya que dichas bases son almacenadas en un solo archivo en el disco y no hace falta tener activo un servidor de bases de datos como MySQL o PostgreSQL.
En el archivo config.py debemos agregar nuevos datos:
import os BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) class Ajustes(object): SECRET_KEY = os.environ.get('SECRET_KEY') or 'flask-course' SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ 'sqlite:///' + os.path.join(BASE_DIR, 'app.db') SQLALCHEMY_TRACK_MODIFICATIONS = False
La extensión Flask-SQLAlchemy toma la ubicación de la base de datos de la aplicación desde“SQLALCHEMY_DATABASE_URI”. Nuevamente tomamos ajustes de variables de entorno y definimos un respaldo en caso de que la variable de entorno tenga alguna falla. Nuestra variable de entorno se llama “BASE_DIR”, y tendremos la base de datos “app.db” en el directorio principal de la aplicación que está guardado en la variable “base_dir”.
La opción “SQLALCHEMY_TRACK_MODIFICATIONS” está definida como False para desactivar una función de Flask-SQLAlchemy que no necesitamos, la cual envía un aviso a la aplicación cada vez que se haga un cambio en la base de datos.
Tendremos nuestra base de datos representada en la aplicación por una instancia de bases de datos. El motor de migración tendrá también una representación en la aplicación. Estos objetos deben ser creados luego de que la aplicación sea creada en el archivo __init__.py:
from flask import Flask from app.settings.config import Ajustes from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate app = Flask(__name__) app.config.from_object(Ajustes) bdd = SQLAlchemy(app) migrar = Migrate(app,bdd) from app import rutas, modelos
Los cambios hechos a __init__.py son:
- Importamos SQLAlchemy de Flask-SQLAlchemy y Migrate de Flask-Migrate a la aplicación.
- Creamos una instancia llamada bdd que representa a la base de datos y la definimos con SQLAlchemy.
- Agregamos la instancia migrar, la cual representa al motor de migración y la definimos con Migrate.
- Importamos modelos de app, para definir la estructura de nuestra base de datos.
La mayoría de las extensiones en Flask son iniciadas de esta forma en las aplicaciones, así que es bueno aprender este proceso. El archivo modelos.py aun no ha sido definido así que vamos a hacerlo.
Modelos de Bases de Datos
Los datos almacenados en la base de datos, serán representados por una colección de clases, llamadas generalmente modelos (por eso nuestro archivo se llamará modelos.py).
SQLAlchemy tiene una capa de Mapeo Relacional de Objetos (ORM por sus siglas en inglés) que se encarga de “traducir” los datos incompatibles entre Python-Flask y SQLite, creando así las filas necesarias en la tabla de la base de datos según los datos del modelo.
Para nuestro modelo de usuarios tendremos las siguientes filas:
- id: Será un numero único para cada usuario que servirá para identificarlo dentro del sistema.
- username: Será el nombre del usuario dentro del sistema. Será un string.
- email: La dirección de correo electrónico de cada usuario.
- hash_clave: Contendrá la contraseña de acceso del usuario encriptada.
El hecho de que tengamos la clave encriptada dentro del modelo de usuario, es una practica de seguridad utilizada ampliamente que asegura que, en caso de que algún hacker consiga acceso a la base de datos, no pueda acceder a la contraseña de los usuarios y por lo tanto no podrá acceder a la información del usuario dentro del sistema.
Como ya tenemos definido lo que queremos en nuestro modelo de usuarios, debemos entonces definir nuestros modelos en el archivo modelos.py, dentro de la carpeta app:
from app import bdd class Usuario(bdd.Model): id = bdd.Column(bdd.Integer, primary_key=True) username = bdd.Column(bdd.String(64), index=True, unique=True) email = bdd.Column(bdd.String(120), index=True, unique=True) hash_clave = bdd.Column(bdd.String(128)) def __repr__(self): return '<Usuario {}>'.format(self.username)
Cabe destacar la siguiente información:
- Los argumentos db.Integer y db.String definen el tipo de dato que debe contener cada fila.
- “primary_key=True” define id como el identificador primario de cada objeto de la clase.
- Los números que son pasados como argumento a db.String() definen el tamaño máximo de cada objeto.
- “unique=True” evita que dos usuarios tengan el mismo nombre de usuario o correo electrónico
- “__repr__” imprimirá un mensaje con el nombre del usuario cuando este sea creado.
La clase Usuario toma como base “bdd.model”, la cual es utilizada como base para todos los modelos en Flask-SQLAlchemy. Esta clase define varios campos como variables de clase. Los campos son creados como instancias de la clase “db.column”, la cual toma el tipo de campo como argumento, ademas de argumentos opcionales extra.
El método “__repr__” le indica a Python como debe imprimir la información de los objetos de esta clase. Este método será muy útil para el proceso de debugging.
Creación del Repositorio de Migración
El modelo creado define la estructura inicial de la base de datos de la aplicación. Pero, como las aplicaciones crecen continuamente, habrá necesidad de cambiar dicha estructura, por ejemplo agregando nuevos campos o eliminando elementos. Alembic (el framework de migración utilizado por Flask-Migrate) hace que los cambios estructurales de la base de datos no requieran que sea creada nuevamente desde el inicio.
Para completar esta tarea, Alembic mantiene un repositorio de migración en el que guarda los scripts de migración de datos. Cada vez que un cambio es hecho a la base de datos, un script de migración con los detalles del cambio es agregado al repositorio. Para aplicar las migraciones a la base de datos, los scripts son ejecutados en la secuencia en la que fueron creados.
Flask-Migrate muestra sus comandos a traves del comando flask. Ya hemos visto funcionando flask run, el cual es elemento de Flask. Otro elemento importante de Flask es flask db, el cual es agregado por Flask-Migrate, para manejar todo lo relacionado con la migración de bases de datos. Para crear el repositorio de migración, debemos ejecutar el siguiente comando:
flask db init
Luego de ver algo así:
(flaskc) D:\cursoflask\blog>flask db init Creating directory D:\cursoflask\blog\migrations ... done Creating directory D:\cursoflask\blog\migrations\versions ... done Generating D:\cursoflask\blog\migrations\alembic.ini ... done Generating D:\cursoflask\blog\migrations\env.py ... done Generating D:\cursoflask\blog\migrations\README ... done Generating D:\cursoflask\blog\migrations\script.py.mako ... done Please edit configuration/connection/logging settings in 'D:\\cursoflask\\blog\\migrations\\alembic.ini' before proceeding. (flaskc) D:\cursoflask\blog>
debería haber un directorio llamado migrations, con algunos archivos y un subdirectorio llamado versions. Todos estos archivos son parte de nuestro proyecto ahora.
Primera Migración de la Base de Datos
Una vez que tengamos nuestro repositorio de migraciones (migrations) en su lugar, es momento de ejecutar la primera migración, la cual incluirá la tabla de usuarios mapeada según el modelo Usuarios creado en el archivo modelos.py. Existen dos maneras de migrar la base de datos: manual o automáticamente. Para generar la migración automática, Alembic compara el esquema de la base de datos definido por los modelos con la base de datos actual. Luego ingresa en el script de migración toda la información de la migración con los cambios necesarios para que así la base de datos tenga la información de los modelos. En nuestro caso actual, como no hay base de datos existente, la migración automática agregará el modelo Usuarios al script de migración. El comando siguiente genera las migraciones automáticas:
(env) $ flask db migrate -m "modelo usuarios"
La salida del comando da una idea de lo que Alembic incluirá en la migración. El flag ‘-m “modelo usuarios”‘ es opcional y funciona para agregar información extra al archivo de la migración.
En el cmd te debería aparecer algo así:
(env) λ flask db migrate -m "modelo usuarios" INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.autogenerate.compare] Detected added table 'usuario' INFO [alembic.autogenerate.compare] Detected added index 'ix_usuario_email' on '['email']' INFO [alembic.autogenerate.compare] Detected added index 'ix_usuario_username' on '['username']' Generating C:\Users\MyUsuario\Desktop\curso-final\migrations\versions\5b0767814f4a_modelo_usuarios.py ... done
El script de migración generado ya forma parte del proyecto. Si revisamos el script de migración, conseguiremos que tiene una función llamada upgrade() (actualizar) y otra llamada downgrade() (“desactualizar”). La función upgrade() aplica la migración y la función downgrade() la elimina. Esto permite que Alembic migre la base de datos a cualquier punto anterior.
El comando flask db migrate no aplica ningún cambio a la base de datos, solamente genera el script de migración. Para aplicar el cambio realmente, se debe ejecutar el siguiente comando:
(env) $ flask db upgrade
En el cmd te debería aparecer algo así:
(env) λ flask db upgrade INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.runtime.migration] Running upgrade -> 5b0767814f4a, modelo usuarios
Como nuestra aplicación utiliza SQLite, el comando upgrade detectará que no existe ninguna base de datos aún, y procede a crearla (ahora tendremos un archivo llamado app.db luego de que se ejecute el comando. Esta será la base de datos SQLite). Cuando se trabaja con servidores de bases de datos como MySQL o PostgreSQL, se debe crear la base de datos antes de ejecutar el comando upgrade.
Upgrade y Downgrade de las Bases de Datos
Ya sabemos migrar y actualizar los modelos en la base de datos de nuestra aplicación. Esta operación permite implementar mejoras y nuevos modelos en nuestra aplicación.
Siempre que hagamos cambios en el archivo de modelos debemos generar el script de migración con “flask db migrate”, para luego utilizar el comando “flask db upgrade”, que implementa finalmente la migración a la base de datos.
Pero, ¿que pasa si el cambio que implementamos no es el que deseabamos o genera problemas en nuestra app?
Aquí es donde entra en juego el comando “flask db downgrade” que elimina el último cambio hecho a la base de datos. Este comando puede ser una herramienta sumamente útil y necesaria en el desarrollo de aplicaciones.
Una vez que hayamos ejecutado “flask db downgrade”, debemos eliminar el script de migración de la carpeta de migraciones, implementar en el archivo de modelos los cambios pertinentes y finalmente generar un script de migración nuevo.
Relaciones en la Base de Datos
Las bases de datos relacionales son muy buenas almacenando relaciones entre dos datos. Consideremos el caso en el que tenemos nuestro modelo de usuarios actual y un modelo de publicaciones del blog. El usuario tendrá su registro en el modelo de usuarios, y la publicación tendrá su registro en el modelo de publicaciones. La mejor forma de recordar quien escribió una publicación determinada, es establecer una relación entre las publicaciones y los usuarios.
Una vez que exista esta relación entre ambos datos, la base de datos puede responder consultas respecto esta relación. Un ejemplo trivial trivial es cuando tenemos una publicación y necesitamos saber quien la escribió. Un ejemplo más complejo puede ser el inverso de este. Si se tienen usuarios, seguramente necesitaremos saber que publicaciones hace cada uno. Flask-SQLAlchemy puede ayudar con ambos tipos de consulta.
Vamos a crear nuestro modelo de publicaciones con las siguientes filas:
- id: Será el identificador numérico único de cada publicación.
- cuerpo: Almacena el contenido de la publicación.
- timestamp: Este campo va a almacenar la hora y fecha de la publicación.
- id_usuario: Será el id del usuario que hace la publicación. Esta será la forma con la cual estableceremos la relación entre ambos modelos.
Tenemos en nuestro modelo de usuarios que el id es el identificador primario de cada usuario en la base de datos y que cada uno es único. La manera de relacionar la publicación con el usuario que la publicó es agregar este id en cada publicación y este es exactamente el que estará en la columna de id_usuario. Este id_usuario será el identificador externo de nuestras publicaciones en el blog y serán la manera como estarán relacionadas las publicaciones con los usuarios. Será una relación de uno a muchos, ya que un usuario puede tener muchas publicaciones.
Modifiquemos entonces nuestro archivo de modelos.py con lo que acabamos de presentar:
from app import bdd from datetime import datetime class Usuario(bdd.Model): id = bdd.Column(bdd.Integer, primary_key=True) username = bdd.Column(bdd.String(64), index=True, unique=True) email = bdd.Column(bdd.String(120), index=True, unique=True) hash_clave = bdd.Column(bdd.String(128)) pubs = bdd.relationship('Pubs', backref='autor', lazy='dynamic') def __repr__(self): return '<Usuario {}>'.format(self.username) class Pubs(bdd.Model): id = bdd.Column(bdd.Integer, primary_key=True) cuerpo = bdd.Column(bdd.String(256)) timestamp = bdd.Column(bdd.DateTime, index=True, default=datetime.now) id_usuario = bdd.Column(bdd.Integer, bdd.ForeignKey('usuario.id')) def __repr__(self): return '<Publicación {}>'.format(self.cuerpo)
La clase Pubs representará entonces a las publicaciones que hagan los usuarios. Tendremos el campo timestamp indexado, lo cual es útil si se quieren ver las publicaciones en orden cronológico y tenemos definido un valor por defecto (default), definido por la función datetime.now. Cuando definimos el valor por defecto con una función, SQLAlchemy le asignará el valor generado por llamar la función (como no se está agregando los paréntesis al final, se está pasando la función como valor y no el resultado de llamarla).
El campo id_usuario fue inicializado como identificador externo (foreign key) y su valor será usuario.id, lo que significa que buscara en el modelo de usuarios el valor de id.
Agregamos un campo nuevo para las publicaciones en el modelo de Usuarios, que es iniciado con la función bdd.relationship. Este no es un campo real de la base de datos, sino una representación de la relación entre los usuarios y las publicaciones. En las relaciones de uno a muchos, la representación de la relacion (el campo iniciado con bdd.relationship) está en el lado del “uno”, y es utilizado de forma conveniente para acceder a los “muchos”. El argumento backref define el nombre de un campo que será agregado a la clase de los “muchos”, que señala de nuevo al “uno”. Esto agrega la posibilidad de utilizar la expresión pub.autor, la cual devolverá el usuario según la publicación. El argumento lazy define como será hecha la consulta a la base de datos, lo cual explicaremos mas adelante juntos con ejemplos para aclarar posibles confusiones que se hayan presentado.
Modificamos nuestro archivo de modelos, entonces debemos hacer la migración de la base de datos:
flask db migrate -m "modelo pubs"
En el cmd te debería aparecer algo así:
(env) λ flask db migrate -m "modelo pubs" INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.autogenerate.compare] Detected added table 'pubs' INFO [alembic.autogenerate.compare] Detected added index 'ix_pubs_timestamp' on '['timestamp']' Generating C:\Users\MyUsuario\Desktop\curso-final\migrations\versions\30b167bd504a_modelo_pubs.py ... done
Ya generamos la migración, ahora la aplicamos:
flask db upgrade
En el cmd te debería aparecer algo así:
INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.runtime.migration] Running upgrade a871ae5670ea -> 30faa82190fc, modelo pubs
Activar y Probar
Ya luego de este proceso, tenemos definida nuestra base de datos, pero aun no hemos visto como funciona. Como la aplicación no tiene lógica de la base de datos aún, vamos a utilizar el intérprete de Python para familiarizarnos. Dentro del entorno virtual, vamos a iniciar una terminal de Python con el comando python. Una vez que la sesión del interprete esté iniciada, vamos a importar la base de datos y los modelos:
>>> from app import bdd >>> from app.modelos import Usuario, Pubs
Ahora, vamos a crear un usuario nuevo:
>>> u = Usuario(username='juan', email='juan@correo.com') >>> bdd.session.add(u) >>> bdd.session.commit()
Los cambios a la base de datos se harán dentro del contexto de una sesión (session), al cual accedemos con bdd.session. Se pueden acumular varios cambios en una sola sesión y, luego de que hayan sido registrados, los implementamos en la base de datos con un solo llamado a bdd.session.commit(), que escribe todos los cambios acumulados automáticamente. Si en alguna ocasión, mientras trabajamos en una sesión, cometemos algún error, se puede utilizar la función bdd.session.rollback(), la cual eliminará todos los cambios acumulados en la sesión. Lo importante de esto es recordar que los cambios son escritos en la base de datos solo cuando ejecutamos bdd.session.commit(). El concepto de sesión garantiza que la base de datos no será dejada nunca en un estado inconsistente.
Vamos a crear otro usuario:
>>> u = Usuario(username='maria',email='maria@correo.com') >>> bdd.session.add(u) >>> bdd.session.commit()
La base de datos puede responder cualquier consulta (query) que se le haga, en especifico, una que devuelva todos los usuarios:
>>> usuarios = Usuario.query.all() >>> usuarios [<Usuario juan>, <Usuario maria>] >>> for i in usuarios: ... print(i.id, i.username) ... 1 juan 2 maria
Todos los modelos tienen un atributo query, el cual se utiliza para hacer las consultas a la base de datos. La consulta mas básica es la que devuelve todos los elementos de una clase, all() (todos en ingles). Ten en cuenta que todos los campos de id fueron definidos automáticamente a 1 y 2 cuando los usuarios fueron creados.
Otra forma de hacer consultas puede ser con los id de los usuarios:
>>> u = Usuario.query.get(1) >>> u <Usuario juan>
Vamos a crear ahora una publicación:
>>> u = Usuario.query.get(1) >>> p = Pubs(cuerpo='primera publicación', autor=u) >>> bdd.session.add(p) >>> bdd.session.commit()
No se necesita definir el valor de timestamp porque toma el valor de la ejecución de la función, como definimos en el modelo. El campo id_usuario utiliza la relación (bdd.relationship) que está en la clase Usuario para tomar su valor. Agrega el atributo publicaciones y el atributo autor a la clase Pubs. El autor de la publicación es asignado por el uso del valor virtual de autor, en lugar de tener que involucrarse con los ids de los usuarios. SQLAlchemy sabe manejar muy bien esto, ya que provee abstracción de alto nivel sobre las relaciones y los identificadores externos.
>>> # Consultemos todas las publicaciones de juan >>> u = Usuario.query.get(1) >>> u <Usuario juan> >>> pubs = u.pubs.all() >>> pubs [<Publicación primera publicación>] >>> # Hagamos lo mismo pero con maria, que no tiene publicaciones >>> u = Usuario.query.get(2) >>> u <Usuario maria> >>> u.pubs.all() [] >>> # Vamos a imprimir todas las publicaciones con su autor >>> pubs = Pubs.query.all() >>> for p in pubs: ... print(p.id,p.autor.username,p.cuerpo) ... 1 juan primera publicacion >>> # Vamos a imprimir los usuarios en orden alfabético >>> Usuario.query.order_by(Usuario.username.desc()).all() [<Usuario maria>, <Usuario juan>]
La documentación de Flask-SQLAlchemy es el mejor lugar para aprender sobre las multiples opciones que están disponibles para consultar la base de datos.
Para completar esta sección, vamos a eliminar las publicaciones de prueba que agregamos, para que la base de datos esté limpia y lista para la siguiente parte:
>>> usuarios = Usuario.query.all() >>> for u in usuarios: ... bdd.session.delete(u) ... >>> pubs = Pubs.query.all() >>> pubs [<Publicación primera publicacion>] >>> for p in pubs: ... bdd.session.delete(p) ... >>> bdd.session.commit()
Flask Shell
Cuando estemos desarrollando nuestra aplicación, muchas veces necesitaremos entrar en el interprete de Python, y tendremos la necesidad de repetir los pasos de importar la base de datos y los modelos que tengamos definidos. El comando flask shell es una forma muy útil de iniciar la terminal y ahorrarnos la parte de importar las cosas nuevamente. Este comando abre un interprete de python con todo el contexto de la aplicación:
$ python >>> app Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'app' is not defined >>> exit() (env) $ flask shell >>> app <Flask 'app'> >>> exit()
Con una sesión normal del interprete, si no importamos la aplicación (app), no podremos trabajar con ella. Por otro lado, con flask shell, se importan todos los archivos de la aplicación. Lo mejor de esto, es que podemos definir el contexto del intérprete nosotros mismos con una lista de módulos que deben ser importados.
En el archivo blog.py, agregaremos la siguiente información que definirá un contexto que importa directamente la base de datos y los modelos directamente a la sesión del intérprete:
from app import app, bdd from app.modelos import Usuario, Pubs @app.shell_context_processor def make_shell_context(): return {'bdd':bdd,'Usuario':Usuario,'Pubs':Pubs}
El decorador @app.shell_context_processor
, registra la función como función de contexto del intérprete. Cuando ejecutamos flask shell, esta función es llamada y registra los elementos devueltos por ella en la sesión del intérprete. La razón por la que la función devuelve un diccionario y no una lista es que para cada item que se tiene, se debe suministrar un nombre con el cual se identificará en la sesión, que será la llave del diccionario.
Luego de que se agrega la función de contexto del interprete, se puede trabajar con las entidades de la base de datos sin importarla, ejecutamos flask shell nuevamente:
(env) λ flask shell Python 3.7.5 (tags/v3.7.5:5c02a39a0b, Oct 15 2019, 00:11:34) [MSC v.1916 64 bit (AMD64)] on win32 App: app [development] Instance: C:\Users\gamersnautas\Desktop\verificando-curso\instance >>>ce >>> bdd <SQLAlchemy engine=sqlite:///D:\cursoflask\dbejemplo\blog\app.db> >>> Usuario <class 'app.modelos.Usuario'> >>> Pubs <class 'app.modelos.Pubs'>
➡ Continúa aprendiendo con nuestro Curso de Flask – Python: