Explorar o código

Write a beginner's article on SQLAlchemy

theenglishway (time) %!s(int64=7) %!d(string=hai) anos
pai
achega
7d5f306619
Modificáronse 3 ficheiros con 190 adicións e 1 borrados
  1. BIN=BIN
      content/images/sqlalchemy_logo.png
  2. 189 0
      content/posts/sqlalchemy-intro.md
  3. 1 1
      pelicanconf.py

BIN=BIN
content/images/sqlalchemy_logo.png


+ 189 - 0
content/posts/sqlalchemy-intro.md

@@ -0,0 +1,189 @@
+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]: <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)
+
+# 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]: [<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_**][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

+ 1 - 1
pelicanconf.py

@@ -6,7 +6,7 @@ AUTHOR = u'theenglishway'
 SITENAME = u'theenglishway'
 SITEURL = u'http://localhost:8000'
 
-SITETITLE = 'The English way'
+SITETITLE = 'theenglishway'
 SITESUBTITLE = "Thought I'd've something more to say"
 SITELOGO = '/images/theenglishway_profile.jpg'
 FAVICON = '/images/favicon.ico'