Title: SQLAlchemy, c'est pas sorcier ! Category: Le développement web pour les nuls Tags: développement, SQLAlchemy, Python, web Summary: Image: /images/sqlalchemy_logo.png Lang: fr Status: draft
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.
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 :
:::python
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.
:::python
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.)
:::python
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).
:::python
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 :
:::ipython3
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.
:::python
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 :
:::python
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)
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 :
Un exemple vaut mille mots :
:::ipython3
# 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 query 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')>]
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.