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 ___ ![Logo SQLAlchemy][sqlalchemy-logo] 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][sqlalchemy-django] et du [tutoriel disponible sur le site officiel][sqlalchemy-tutorial]. 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]: ... 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 "".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 : :::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]: [] # 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]: [] # 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_**][sqlalchemy-sessions]. [sqlalchemy-django]: http://lucumr.pocoo.org/2011/7/19/sqlachemy-and-you/ [sqlalchemy-tutorial]: https://docs.sqlalchemy.org/en/latest/orm/tutorial.html [sqlalchemy-logo]: /images/sqlalchemy_logo.png [sqlalchemy-sessions]: http://docs.sqlalchemy.org/en/latest/orm/contextual.html#using-thread-local-scope-with-web-applications