Usando SQLAlchemy con Flask y PostgreSQL

Hoy en día, los mapeadores relacionales de objetos como SQLAlchemy se utilizan como un puente entre las aplicaciones y las bases de datos SQL y facilitan el trabajo con ellos mediante programación.

Introducción

Las bases de datos son una parte crucial de las aplicaciones modernas, ya que almacenan los datos que se utilizan para alimentarlas. Generalmente, usamos el lenguaje de consulta estructurado (SQL) para realizar consultas en la base de datos y manipular los datos dentro de ella. Aunque inicialmente se hizo a través de herramientas de SQL dedicadas, rápidamente pasamos a usar SQL desde dentro de las aplicaciones para realizar consultas.

Naturalmente, con el paso del tiempo, surgieron Object Relational Mappers (ORM), que nos permiten conectarnos de manera segura, fácil y conveniente a nuestra base de datos mediante programación sin necesidad de ejecutar consultas para manipular los datos.

Uno de estos ORM es SQLAlquimia. En esta publicación, profundizaremos en los ORM y específicamente en SQLAlchemy, luego lo usaremos para crear una aplicación web basada en una base de datos utilizando el marco [Matraz] (http://flask.palletsprojects.com/en/1.1.x/).

¿Qué es un ORM y por qué usarlo?

Mapeo objeto-relacional, como sugiere el nombre, mapea objetos a entidades relacionales. En los lenguajes de programación orientados a objetos, los objetos no son tan diferentes de las entidades relacionales: tienen ciertos campos/atributos que se pueden asignar de manera intercambiable.

Dicho esto, como es bastante fácil asignar un objeto a una base de datos, lo contrario también es muy simple. Esto facilita el proceso de desarrollo de software y reduce las posibilidades de cometer errores manuales al escribir código SQL simple.

Otra ventaja de usar ORM es que nos ayudan a escribir código que se adhiere a los principios DRY (Don’t Repeat Yourself) al permitirnos usar nuestros modelos para manipular datos en lugar de escribir código SQL cada vez que necesitamos acceder a la base de datos.

Los ORM extraen las bases de datos de nuestra aplicación, lo que nos permite usar múltiples bases de datos o cambiarlas con facilidad. Digamos, si usamos SQL en nuestra aplicación para conectarnos a una base de datos MySQL, necesitaríamos modificar nuestro código si tuviéramos que cambiar a una base de datos MSSQL ya que difieren en la sintaxis.

Si nuestro SQL se integró en varios puntos de nuestra aplicación, esto será bastante complicado. A través de un ORM, los cambios que necesitaríamos hacer se limitarían a cambiar solo un par de parámetros de configuración.

Aunque los ORM nos hacen la vida más fácil al abstraer las operaciones de la base de datos, debemos tener cuidado de no olvidar lo que sucede debajo del capó, ya que esto también guiará la forma en que usamos los ORM. También debemos estar familiarizados con los ORM y aprenderlos para usarlos de manera más eficiente y esto introduce una pequeña curva de aprendizaje.

SQLAlchemy ORM

SQLAlchemy es un ORM escrito en Python para brindarles a los desarrolladores el poder y la flexibilidad de SQL, sin la molestia de usarlo realmente.

SQLAlchemy envuelve la API de base de datos de Python (DBAPI de Python) que se envía con Python y fue creada para facilitar la interacción entre Módulos y bases de datos de Python.

El DBAPI se creó para establecer la coherencia y la portabilidad en lo que respecta a la administración de bases de datos, aunque no necesitaremos interactuar con él directamente, ya que SQLAlchemy será nuestro punto de contacto.

También es importante tener en cuenta que SQLAlchemy ORM está construido sobre Núcleo de SQLAlchemy - que maneja la integración DBAPI e implementa SQL. En otras palabras, SQLAlchemy Core proporciona los medios para generar consultas SQL.

Si bien SQLAlchemy ORM hace que nuestras aplicaciones sean independientes de la base de datos, es importante tener en cuenta que las bases de datos específicas requerirán controladores específicos para conectarse a ellas. Un buen ejemplo es Piscopg, que es una implementación de PostgreSQL de DBAPI que, cuando se usa junto con SQLAlchemy, nos permite interactuar con las bases de datos de Postgres.

Para las bases de datos MySQL, la biblioteca PyMySQL ofrece la implementación DBAPI necesaria para interactuar con ellas.

SQLAlchemy también se puede utilizar con Oracle y Microsoft SQL Server. Algunos nombres importantes en la industria que confían en SQLAlchemy incluyen Reddit, Yelp, DropBox y Survey Monkey.

Habiendo presentado el ORM, construyamos una API de Flask simple que interactúe con una base de datos de Postgres.

Matraz con SQLAlchemy

Flask es un micromarco ligero que se utiliza para crear aplicaciones web mínimas y, a través de bibliotecas de terceros, podemos aprovechar su flexibilidad para crear aplicaciones web robustas y ricas en funciones.

En nuestro caso, construiremos una API RESTful simple y usaremos la extensión Flask-SQLAlchemy para conectar nuestra API a una base de datos de Postgres.

Requisitos previos

Usaremos postgresql (también conocido como Postgres) para almacenar nuestros datos que serán manejados y manipulados por nuestra API.

Para interactuar con nuestra base de datos de Postgres, podemos usar la línea de comandos o clientes que vienen equipados con interfaces gráficas de usuario que los hacen más fáciles de usar y mucho más rápidos para navegar.

Para Mac OS, recomiendo usar Postico que es bastante simple e intuitivo y proporciona una interfaz de usuario limpia.

Administrador de página es otro excelente cliente que soporta todos los principales sistemas operativos e incluso proporciona una versión dockerizado.

Usaremos estos clientes para crear la base de datos y también ver los datos durante el desarrollo y ejecución de nuestra aplicación.

Con las instalaciones fuera del camino, creemos nuestro entorno e instalemos las dependencias que necesitaremos para nuestra aplicación:

1
2
3
4
5
$ virtualenv --python=python3 env --no-site-packages
$ source env/bin/activate
$ pip install psycopg2-binary
$ pip install flask-sqlalchemy
$ pip install Flask-Migrate

Los comandos anteriores crearán y activarán un entorno virtual, instalarán el controlador Psycopg2, instalarán Flask-sqlalchemy e instalarán Flask-Migrate para manejar las migraciones de bases de datos.

Flask-Migrate usa Alambique, que es una herramienta ligera de migración de base de datos que nos ayuda a interactuar con nuestra base de datos de una manera mucho más clara al ayudarnos a crear y recrear bases de datos, mover datos dentro y entre bases de datos e identificar el estado de nuestra base de datos.

En nuestro caso, no tendremos que volver a crear la base de datos o las tablas cada vez que se inicie nuestra aplicación y lo hará automáticamente por nosotros en caso de que no exista.

Implementación

Construiremos una API simple para manejar y manipular información sobre automóviles. Los datos serán almacenados en una base de datos PostgreSQL ya través de la API realizaremos operaciones CRUD.

Primero, tenemos que crear la base de datos cars_api utilizando nuestro cliente PostgreSQL de elección:

sqlalchemy_create_db

Con la base de datos en su lugar, conectémonos a ella. Comenzaremos arrancando nuestra Flask API en el archivo apps.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return {"hello": "world"}

if __name__ == '__main__':
    app.run(debug=True)

Comenzamos creando una aplicación Flask y un punto final único que devuelve un objeto JSON.

Para nuestra demostración, usaremos Flask-SQLAlchemy, que es una extensión diseñada específicamente para agregar la funcionalidad de SQLAlchemy a las aplicaciones de Flask.

Ahora integremos Flask-SQLAlchemy y Flask-Migrate en nuestro app.py y creemos un modelo que definirá los datos sobre nuestros autos que almacenaremos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Previous imports remain...
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = "postgresql://postgres:[correo electrónico protegido]:5432/cars_api"
db = SQLAlchemy(app)
migrate = Migrate(app, db)

class CarsModel(db.Model):
    __tablename__ = 'cars'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String())
    model = db.Column(db.String())
    doors = db.Column(db.Integer())

    def __init__(self, name, model, doors):
        self.name = name
        self.model = model
        self.doors = doors

    def __repr__(self):
        return f"<Car {self.name}>"

Después de importar flask_sqlalchemy, comenzamos agregando el URI de la base de datos a la configuración de nuestra aplicación. Este URI contiene nuestras credenciales, la dirección del servidor y la base de datos que usaremos para nuestra aplicación.

Luego creamos una instancia de Flask-SQLAlchemy llamada db y la usamos para todas nuestras interacciones con la base de datos. La instancia de Flask-Migrate, llamada migrate, se crea después de eso y se usará para manejar las migraciones de nuestro proyecto.

CarsModel es la clase de modelo que se usará para definir y manipular nuestros datos. Los atributos de la clase representan los campos que queremos almacenar en la base de datos.

Definimos el nombre de la tabla usando __tablename__ junto a las columnas que contienen nuestros datos.

Flask se envía con una interfaz de línea de comandos y comandos dedicados. Por ejemplo, para iniciar nuestra aplicación, usamos el comando flask run. Para aprovechar este script, solo necesitamos definir una variable de entorno que especifique el script que aloja nuestra aplicación Flask:

1
2
3
4
5
6
7
8
9
$ export FLASK_APP=app.py
$ flask run
 * Serving Flask app "app.py" (lazy loading)
 * Environment: development
 * Debug mode: on
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 172-503-577

Con nuestro modelo en su lugar y Flask-Migrate integrado, usémoslo para crear la tabla cars en nuestra base de datos:

1
2
3
$ flask db init
$ flask db migrate
$ flask db upgrade

Comenzamos inicializando la base de datos y habilitando las migraciones. Las migraciones generadas son solo scripts que definen las operaciones a realizar sobre nuestra base de datos. Dado que esta es la primera vez, el script solo generará la tabla cars con columnas como se especifica en nuestro modelo.

El comando flask db upgrade ejecuta la migración y crea nuestra tabla:

sqlalchemy_db_upgrade

En caso de que agreguemos, eliminemos o cambiemos alguna columna, siempre podemos ejecutar los comandos migrar y actualizar para reflejar estos cambios también en nuestra base de datos.

Creación y lectura de entidades

Con la base de datos en su lugar y conectada a nuestra aplicación, todo lo que queda es implementar las operaciones CRUD. Comencemos con la creación de un coche, así como la recuperación de todos los existentes actualmente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Imports and CarsModel truncated

@app.route('/cars', methods=['POST', 'GET'])
def handle_cars():
    if request.method == 'POST':
        if request.is_json:
            data = request.get_json()
            new_car = CarsModel(name=data['name'], model=data['model'], doors=data['doors'])
            db.session.add(new_car)
            db.session.commit()
            return {"message": f"car {new_car.name} has been created successfully."}
        else:
            return {"error": "The request payload is not in JSON format"}

    elif request.method == 'GET':
        cars = CarsModel.query.all()
        results = [
            {
                "name": car.name,
                "model": car.model,
                "doors": car.doors
            } for car in cars]

        return {"count": len(results), "cars": results}

Comenzamos definiendo una ruta /cars que acepta solicitudes GET y POST. La solicitud GET devolverá una lista de todos los autos almacenados en nuestra base de datos, mientras que el método POST recibirá los datos de un auto en formato JSON y llenará nuestra base de datos con la información proporcionada.

Para crear un auto nuevo, usamos la clase CarsModel y proporcionamos la información requerida para completar las columnas de nuestra tabla cars. Después de crear un objeto CarsModel, creamos una sesión de base de datos y le agregamos nuestro car.

Para guardar nuestro automóvil en la base de datos, confirmamos la sesión a través de db.session.commit(), que cierra la transacción de la base de datos y guarda nuestro automóvil.

Intentemos agregar un auto usando una herramienta como Cartero:

sqlalchemy_postman

El mensaje de respuesta nos notifica que nuestro coche ha sido creado y guardado en la base de datos:

sqlalchemy_db

Puede ver que ahora hay un registro del automóvil en nuestra base de datos.

Con los autos guardados en nuestra base de datos, la solicitud ‘GET’ nos ayudará a obtener todos los registros. Consultamos todos los autos almacenados en nuestra base de datos usando la función CarsModel.query.all(), que proporciona Flask-SQLAlchemy.

Esto devuelve una lista de objetos CarsModel, que luego formateamos y agregamos a una lista usando una comprensión de lista y la pasamos a la respuesta junto con la cantidad de autos en nuestra base de datos. Cuando solicitamos la lista de autos a través de la API en Postman:

sqlalchemy_postman_2

El método GET en el extremo /cars devuelve la lista de coches tal como aparecen en nuestra base de datos, así como el recuento total.

Nota: Observe cómo no hay una consulta SQL única presente en el código. SQLAlchemy se encarga de eso por nosotros.

Actualización y eliminación de entidades

Hasta ahora, podemos crear un solo automóvil y obtener una lista de todos los automóviles almacenados en la base de datos. Para completar el conjunto de operaciones CRUD en autos en nuestra API, necesitamos agregar funcionalidad para devolver los detalles, modificar y eliminar un solo auto.

Los métodos/verbos HTTP que usaremos para lograr esto serán GET, PUT y DELETE, que se reunirán en un único método llamado handle_car():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Imports, Car Model, handle_cars() method all truncated

@app.route('/cars/<car_id>', methods=['GET', 'PUT', 'DELETE'])
def handle_car(car_id):
    car = CarsModel.query.get_or_404(car_id)

    if request.method == 'GET':
        response = {
            "name": car.name,
            "model": car.model,
            "doors": car.doors
        }
        return {"message": "success", "car": response}

    elif request.method == 'PUT':
        data = request.get_json()
        car.name = data['name']
        car.model = data['model']
        car.doors = data['doors']
        db.session.add(car)
        db.session.commit()
        return {"message": f"car {car.name} successfully updated"}

    elif request.method == 'DELETE':
        db.session.delete(car)
        db.session.commit()
        return {"message": f"Car {car.name} successfully deleted."}

Nuestro método handle_car() recibe el car_id de la URL y obtiene el objeto del coche tal como está almacenado en nuestra base de datos. Si el método de solicitud es GET, los detalles del automóvil simplemente se devolverán:

sqlalchemy_postman_3

Para actualizar los detalles de nuestro coche, usamos el método PUT y no PATCH. Ambos métodos pueden usarse para actualizar los detalles, sin embargo, el método PUT acepta una versión actualizada de nuestro recurso y reemplaza la que tenemos almacenada en la base de datos.

El método PATCH simplemente modifica el que tenemos en nuestra base de datos sin reemplazarlo. Por lo tanto, para actualizar un registro CarsModel en nuestra base de datos, debemos proporcionar todos los atributos de nuestro automóvil, incluidos los que se actualizarán.

Usamos los detalles para modificar nuestro objeto de automóvil y confirmar estos cambios usando db.session.commit() y luego devolvemos una respuesta al usuario:

sqlalchemy_postman_4

Nuestro coche ha sido actualizado con éxito.

Por último, para eliminar un automóvil, enviamos una solicitud DELETE al mismo punto final. Con el objeto CarsModel ya consultado, todo lo que tendremos que hacer es usar la sesión actual para eliminarlo ejecutando db.session.delete(car) y confirmando nuestra transacción para reflejar nuestros cambios en la base de datos:

sqlalchemy_postman_5

Conclusión

Las aplicaciones de la vida real no son tan simples como las nuestras y, por lo general, manejan datos relacionados y distribuidos en varias tablas.

SQLAlchemy nos permite definir relaciones y manipular datos relacionados también. Puede encontrar más información sobre el manejo de relaciones en la documentación oficial de Flask-SQLAlchemy.

Nuestra aplicación se puede ampliar fácilmente para acomodar relaciones e incluso más tablas. También podemos conectarnos a múltiples bases de datos usando Binds. Puede encontrar más información sobre Binds en la Vincula la página de documentación.

En esta publicación, presentamos los ORM y, específicamente, el ORM de SQLAlchemy. Usando Flask y Flask-SQLAlchemy, hemos creado una API simple que expone y maneja datos sobre automóviles almacenados en una base de datos PostgreSQL local.

El código fuente del proyecto de esta publicación se puede encontrar en GitHub.