SQLAlchemy, c'est pas sorcier !
Posté le Sat 18 August 2018 dans Je tisse ma toile
Après plusieurs mois à me reposer sur l'ORM de Django, voici venu le temps de me frotter à quelquechose d'un peu plus conséquent : la Rolls de l'ORM Python, SQLAlchemy.
Initialisation
Voilà quelquechose dont je m'étais fait une montagne en survolant la documentation, et qui n'est pourtant pas si complexe que cela quand on décompose bien les étapes. Je m'inspire très largement et sans aucune honte de cet excellent article expliquant SQLAlchemy aux habitués de Django et du tutoriel disponible sur le site officiel.
SQLAlchemy est un peu le couteau suisse de l'ORM, mais beaucoup de sa puissance est cachée et ne sera invoquée directement que pendant l'initialisation. La première étape consiste donc en l'établissement de la connexion avec la base de données :
from sqlalchemy import create_engine
engine = create_engine('sqlite:///./db.sqlite3', echo=True)
Dès ce moment, toute l'abstraction par rapport au type de base de données manipulée est faite, et l'on a plus à s'en préoccuper. Visiblement on peut décider d'interagir directement avec cet objet Engine dans les cas très pointus, mais quand on utilise l'ORM, on peut s'en passer.
La deuxième étape consiste à décrire à SQLAlchemy les tables que nous allons manipuler et les classes Python qui leur seront associées (et qui nous permettront d'interagir avec la BDD sans écrire une ligne de SQL). Dans les versions modernes de SQLAlchemy, cela se fait en une seule fois, en utilisant une declarative base class fournie par la librairie et qui servira à déclarer tous les modèles de l'application.
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
Une fois que l'on dispose de cette base, on fait dériver tous nos modèles de cette classe, et la magie de SQLAlchemy opère en arrière plan. Le principe se rapproche beaucoup de celui utilisé dans l'ORM de Django, mais il y aura de base plus de choses à expliciter (le nom de la table, la clef primaire, etc.)
from sqlalchemy import Column, Integer, String
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String)
fullname = Column(String)
Toutes les informations apportées de cette manière sont agrégées dans la Base dont le champ "metadata" peut désormais être utilisé pour connecter tous les morceaux (la connexion à la base de données, et la déclaration du schéma).
Base.metadata.create_all(engine)
Cette expression va à la fois créer la base de données (si elle n'existe pas encore), les tables (si elles n'existent pas encore), et permettre d'utiliser l'ORM. Le fait d'hériter de Base permettait déjà de créer des instances des classes déclarées :
In [1]: user = User(name="way", fullname="theenglishway")
Out [1]: <User(name='way', fullname='theenglishway')>
... mais en tant que telles, elles ne peuvent être directement envoyées dans la
base de données (là où l'ORM de Django aurait déjà permis de faire
user.save()
). Il faudra pour cela en passer par un dernier type d'objet
qui est au coeur de SQLAlchemy, la session ; elle représente véritablement
la connexion à la base de données, et toute transaction devra se faire au
travers d'une session. Ces sessions seront crées à partir d'une factory là
aussi fournie par la librarie, à laquelle on passe entre autres comme arguments
l'engine déclaré au tout début.
from sqlalchemy.orm import sessionmaker
Session = sessionmaker(bind=engine)
Cette classe doit être gardée précieusement dans un coin de son code, car l'on
y fera appel dès lors que l'on voudra ouvrir une connexion, ce qui se fera
simplement via session = Session()
.
Tout est maintenant en place pour pouvoir échanger avec la base de données, y créer des entrées, effectuer des requêtes, etc. Il y a certes un peu plus à écrire que pour le même résultat dans Django, mais ça reste finalement assez simple une fois bien compris l'apport de chaque élément.
Voici à quoi ressemble le fichier complet :
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String
Base = declarative_base()
# Ceci pourrait être déplacé dans un fichier à part type 'models.py' ou
# n'importe quoi d'autre convenant à l'arborescence qu'on souhaite adopter
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String)
fullname = Column(String)
def __repr__(self):
return "<User(name='{}', fullname='{}')>".format(self.name, self.fullname)
engine = create_engine('sqlite:///./sqlite3.db', echo=True)
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
Utilisation basique
Maintenant que l'on a une belle base de données avec une table vide, il ne reste plus qu'à l'utiliser. Contrairement à Django qui faisait beaucoup pour nous dans notre dos (pour le meilleur et pour le pire), SQLAlchemy nous laisse un peu plus nous débrouiller.
La notion primordiale est celle de session. De nombreux articles et talks expliquent en détails en quoi ce système est supérieur à celui utilisé par des ORM plus basiques (comme celui de Django).
De ce que j'en comprends, au prix d'une utilisation peut-être un poil plus complexe, il permet :
- de limiter le nombre de requêtes vraiment relayées à la base de données
- de complètement annuler une transaction en cours, au besoin
- d'assurer la cohérence au sein d'une transaction donnée, avant même la moindre requëte
Un exemple vaut mille mots :
# On crée une première session
In [1]: session = Session()
# ... dans laquelle on ajoute un utilisateur
In [2]: session.add(User(name="way", fullname="theenglishway"))
# Une requëte faite dans cette session fait bien apparaître ce nouvel
# utilisateur
In [3]: session.query(User).all()
Out[3]: [<User(name='way', fullname='theenglishway')>]
# On ouvre une autre session
In [4]: session2 = Session()
# L'ajout n'y est pas encore visible
In [5]: session2.query(User).all()
Out[5]: []
# Mais dès que l'on commit l'autre session ...
In [6]: session.commit()
# L'utilisateur y apparaît
In [7]: session2.query(User).all()
Out[7]: [<User(name='way', fullname='theenglishway')>]
Le cas des frameworks web
Petite subtilité importante à noter à propos des sessions utilisées dans le cadre d'un framework web qui traite les requêtes en utilisant un thread donné : il faut alors utiliser des scoped sessions.
De manière générale, SQLAlchemy est encore plus puissant et subtil qu'il est possible de le concevoir au premier abord, et il est donc essentiel d'aller jeter un oeil à sa [documentation extrêmement complète] ... et évidemment, aux futurs articles que j'écrirai sur le sujet !