SQLModel en la onda del python moderno
Hace poco estaba revisando el ecosistema que rodea FastAPI debido a un nuevo cambio de documentación que me encontré que por cierto esta demasiado cool donde se explica el funcionamiento de un TLS proxy ver Figura 1, por cierto en mi artículo anterior también hable un poco del tema usando nginx, en ese research me encontré con una nueva tool, que podría considerarse una mejora o un wrapper del ORM SQLAlchemy pero pensando para ser utilizado con los conceptos del python moderno
Python moderno
python ha venido agregando nuevas features muy interesantes y ha atendido algunas solicitudes que por mucho tiempo su comunidad había solicitado, entre los cambios que más impacto han tenido es el soporte a type hints, y el asincronismo recuerdo en un charla que pude ver que ahorita no recuerdo quién ni cómo ni dónde la vi, el author desconocido sugeria que las nuevas librerías de python deberían nacer asíncronas con soporto síncrono, cuando se menciona python moderno hace referencia a todo el ecosistema que se ha ido desarrollando sobre estos conceptos, esta librería SQLModel es una librería que se construye sobre SQLAlchemy que es uno de los orms más utilizados en python, esta librería implementa los type hints y también facilita el asincronismo una mejora que recientemente anunció SQLAlchemy
SQLModel
Es una librería par interactuar con la base de datos por medio de objetos python está construida sobre SQLAlchemy y pydantic, esta librería fue creada por el mismo autor de FastAPI lo que convierte a SQLModel en una librería muy prometedora ver Figura 2 , sin embargo SQLModel al ser una librería tan nueva su documentacion esta en construcción todavia por lo que trataré de mostrar algunos ejemplos basados en su documentación y complementando con la experiencia de haber usado SQLAlchemy
Ejemplos
lo primero que hare sera instalar las librerias necesarias que serán sqlmodel y mysqlclient en este caso lo haré usando poetry
poetry add sqlmodel
poetry add mysqlclient
luego de ello creare un archivo llamado models.py que contendra los modelos que utilizare en la demostración
from typing import List, Optional
from sqlalchemy.util.langhelpers import set_creation_order
from sqlmodel import SQLModel, Field, Relationship
class University(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str
addres: str
students:List["Student"] = Relationship(back_populates="university")
class Student(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str
second_name: str
age: Optional[int] = None
university_id: int = Field(foreign_key="university.id")
university: University = Relationship(back_populates="students")
si han tenido la oportunidad de trabajar SQLAlchemy recordarán que cuando se creaba una tabla era algo parecido a lo siguiente
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import String, Column, Integer
Base = declarative_base()
class User(Base):
__tablename__ = "user"
id = Column(Integer,primary_key=True, index=True)
name = Column(String(255))
como se aprecia se tienen atributos propios del ORM lo cual no son datos nativos de python más sin embargo se podrían transformar usando pydantic para pasarlos a objetos propios en python, con SQLModel como se aprecio en el ejemplo se tienen atributos nativos de python por lo que no se debe utilizar ningún proceso de transformación adicional. En el ejemplo de SQLModel se esta creando una clase que hereda de la clase SQLModel y se pone un atributo table=True para que se cree un tabla con el mismo nombre de la clase pero en minúsculas, esto es opcional por si en algún momento se desea utilizar una clase y no crear necesariamente una tabla, luego de ello tenemos los atributos de esa clase que equivalen a columnas en una tabla como se ve a continuación
class Student(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str
second_name: str
age: Optional[int] = None
university_id: int = Field(foreign_key="university.id")
university: University = Relationship(back_populates="students")
los atributos son de tipo nativo de python es decir str, bool y int SQLModel se encarga de hacer su conversión respectiva al tipo de dato en cada base de datos, el atributo Field viene de SQLAlchemy este atributo no se agrega a la columna de la tabla si no que funciona en el ORM para indicarle que cuando a un objeto de tipo Student se acceda al atributo university traerá la información de esa tabla relacionada utilizando el Foreing Key, ahora bien para ejecutar la creación de las tablas en la base de datos creare un modulo llamado main.py con el siguiente codigo
from sqlmodel import create_engine, Session
from sqlmodel.main import SQLModel
from models import Student, University
DATABASE_URL = "mysql://root:toor@localhost/sqlmodel"
engine = create_engine(DATABASE_URL, encoding="latin1", echo=True)
SQLModel.metadata.create_all(engine)
Ejecuto el codigo main.py y obtengo la siguiente salida en consola
en el codigo main.py importamos los modelos Student y University (es importante importarlos para crear las tablas) y después de ello se pone la url de conexión, como mencione al principio al estar basado en SQLAlchemy podemos guiarnos también por su documentación, luego de ello se crea un objeto engine el cual mantendrá el acceso compartido de la aplicación a la base de datos y finalmente se llama a SQLModel.metadata.create_all(engine) esta función lo que hace es ejecutar la creación de todas los modelos que se han puesto en su clase table=True por eso la importancia de importarlos ya que si no se importa no se realizará la creación
Creando y Consultando
Para crear información utilizare un concepto llamado session este concepto viene de SQLalchemy , en la doc de SQLModel lo explican muy bien pero básicamente yo lo usare para hacer operaciones o grupos de operaciones, esté session ya no es general para toda la aplicación si no que se crea por ejemplo en aplicaciones web por cada request, el ejemplo de creación y listar se aprecia a continuación
from typing import List
from sqlmodel import create_engine, Session, select
from sqlmodel.main import SQLModel
from models import Student, University
SQLALCHEMY_DATABASE_URL = "mysql://root:toor@localhost/sqlmodel"
engine = create_engine(SQLALCHEMY_DATABASE_URL, encoding="latin1", echo=True)
def create_tables():
SQLModel.metadata.create_all(engine)
def create_university():
university = University(addres="4626 Briarwood Drive NORTHPORT", name="University of example")
with Session(engine) as session:
session.add(university)
session.commit()
def get_all_universities():
with Session(engine) as session:
statement = select(University)
universities:List[University] = session.exec(statement=statement).all()
for university in universities:
print(f"{university.name} - {university.addres}")
create_university()
get_all_universities()
ejecutamos el código para crear universidad y tenemos el siguiente resultado
2021-10-02 17:05:30,964 INFO sqlalchemy.engine.Engine INSERT INTO university (name, addres) VALUES (%s, %s)
2021-10-02 17:05:30,965 INFO sqlalchemy.engine.Engine [generated in 0.00026s] ('University of example', '4626 Briarwood Drive NORTHPORT')
2021-10-02 17:05:30,968 INFO sqlalchemy.engine.Engine COMMIT
y ejecutamos el código de listar universidades y se obtiene el siguiente resultado
en el código anterior utilize un variable de session para ejecutar un conjunto de consultas en este caso de creación utilizando un with para que automáticamente la session se cierre cuando termine, luego utilizó el método .add() que cargue la información en memoria lista para que se envíe (muy similar al utilizar git add) y finalmente se llama al método .commit() que envía la información cargada en memoria a la base de datos.
Para el listar se utiliza un funcion select() esta función está modificada por SQLModel pero es hereda de SQLAlchemy utilizando el session de nuevo se ejecuta el select pero aquí se tiene un método adicional llamado .all() el cual permite traer directamente dentro de la consulta objetos python generando así de la misma forma un autocompletado como se aprecia a continuación
para finalizar dejo un código de la creación de un estudiante con llave foránea de universidad
from typing import List
from sqlmodel import create_engine, Session, select
from sqlmodel.main import SQLModel
from models import Student, University
SQLALCHEMY_DATABASE_URL = "mysql://root:toor@localhost/sqlmodel"
engine = create_engine(SQLALCHEMY_DATABASE_URL, encoding="latin1", echo=True)
def create_tables():
SQLModel.metadata.create_all(engine)
def get_all_universities():
with Session(engine) as session:
statement = select(University)
universities:List[University] = session.exec(statement=statement).all()
print("INFO OF UNVIVERSITIES")
for university in universities:
print("--------------------------------------------------------")
print(f"{university.name} - {university.addres}")
print("--------------------------------------------------------")
def create_student():
with Session(engine) as session:
stament = select(University).where(University.name=="University of example")
result = session.exec(stament)
university = result.one()
student = Student(name="Daniel", second_name="Osborne", age=25,university_id=university.id)
session.add(student)
session.commit()
session.refresh(student)
print("--------------------------------------------------------------------")
print("STUDENT CREATED")
print(f"Student : {student}")
print()
create_student()
get_all_universities()
Conclusión
Para quienes han podido trabajar con SQLAlchemy se les hará muy familiar el uso de esta herramienta ya que muchas de las funciones son heredadas y algunas se les ha agregado mejoras para mantener un soporte adicional relacionado con el tipado de datos, una característica a destacar es la capacidad que brinda esta herramienta de poder seguir manteniendo el soporte por si se requieren realizar alguna que otra operación que requiera el uso de SQLAlchemy puro pero aun manteniendo los modelos que ya se han creado, mi objetivo era llegar a mostrar mas, sin embargo para no alargar el post lo dejare para otro articulo