Dans la partie 1 de cette série, vous avez utilisé Flask et Connexion pour créer une API REST fournissant des opérations CRUD à une simple structure en mémoire appelée PEOPLE
. Cela a fonctionné pour démontrer comment le module Connexion vous aide à créer une belle API REST avec une documentation interactive.
Comme certains l'ont noté dans les commentaires de la partie 1, les PEOPLE
structure est réinitialisée à chaque redémarrage de l'application. Dans cet article, vous apprendrez à stocker les PEOPLE
structure et les actions fournies par l'API à une base de données utilisant SQLAlchemy et Marshmallow.
SQLAlchemy fournit un modèle relationnel objet (ORM), qui stocke les objets Python dans une représentation de base de données des données de l'objet. Cela peut vous aider à continuer à penser de manière Pythonique et à ne pas vous soucier de la manière dont les données d'objet seront représentées dans une base de données.
Marshmallow fournit des fonctionnalités pour sérialiser et désérialiser les objets Python lorsqu'ils sortent et entrent dans notre API REST basée sur JSON. Marshmallow convertit les instances de classe Python en objets pouvant être convertis en JSON.
Vous pouvez trouver le code Python de cet article ici.
Bonus gratuit : Cliquez ici pour télécharger une copie du guide "Exemples d'API REST" et obtenir une introduction pratique aux principes de l'API Python + REST avec des exemples exploitables.
À qui s'adresse cet article
Si vous avez apprécié la partie 1 de cette série, cet article élargit encore plus votre ceinture à outils. Vous utiliserez SQLAlchemy pour accéder à une base de données d'une manière plus pythonique que SQL simple. Vous utiliserez également Marshmallow pour sérialiser et désérialiser les données gérées par l'API REST. Pour ce faire, vous utiliserez les fonctionnalités de programmation orientée objet de base disponibles en Python.
Vous utiliserez également SQLAlchemy pour créer une base de données et interagir avec elle. Ceci est nécessaire pour que l'API REST soit opérationnelle avec le PEOPLE
données utilisées dans la partie 1.
L'application Web présentée dans la partie 1 verra ses fichiers HTML et JavaScript modifiés de manière mineure afin de prendre également en charge les modifications. Vous pouvez consulter la version finale du code de la partie 1 ici.
Dépendances supplémentaires
Avant de commencer à créer cette nouvelle fonctionnalité, vous devrez mettre à jour le virtualenv que vous avez créé afin d'exécuter le code de la partie 1 ou en créer un nouveau pour ce projet. La façon la plus simple de le faire après avoir activé votre virtualenv est d'exécuter cette commande :
$ pip install Flask-SQLAlchemy flask-marshmallow marshmallow-sqlalchemy marshmallow
Cela ajoute plus de fonctionnalités à votre virtualenv :
-
Flask-SQLAlchemy
ajoute SQLAlchemy, ainsi que quelques liens avec Flask, permettant aux programmes d'accéder aux bases de données. -
flask-marshmallow
ajoute les parties Flask de Marshmallow, qui permettent aux programmes de convertir des objets Python vers et depuis des structures sérialisables. -
marshmallow-sqlalchemy
ajoute des crochets Marshmallow dans SQLAlchemy pour permettre aux programmes de sérialiser et de désérialiser les objets Python générés par SQLAlchemy. -
marshmallow
ajoute l'essentiel des fonctionnalités de Marshmallow.
Données sur les personnes
Comme mentionné ci-dessus, les PEOPLE
La structure de données de l'article précédent est un dictionnaire Python en mémoire. Dans ce dictionnaire, vous avez utilisé le nom de famille de la personne comme clé de recherche. La structure de données ressemblait à ceci dans le code :
# Data to serve with our API
PEOPLE = {
"Farrell": {
"fname": "Doug",
"lname": "Farrell",
"timestamp": get_timestamp()
},
"Brockman": {
"fname": "Kent",
"lname": "Brockman",
"timestamp": get_timestamp()
},
"Easter": {
"fname": "Bunny",
"lname": "Easter",
"timestamp": get_timestamp()
}
}
Les modifications que vous apporterez au programme déplaceront toutes les données vers une table de base de données. Cela signifie que les données seront enregistrées sur votre disque et existeront entre les exécutions de server.py
programme.
Comme le nom de famille était la clé du dictionnaire, le code limitait la modification du nom de famille d'une personne :seul le prénom pouvait être modifié. De plus, le passage à une base de données vous permettra de changer le nom de famille car il ne sera plus utilisé comme clé de recherche pour une personne.
Conceptuellement, une table de base de données peut être considérée comme un tableau à deux dimensions où les lignes sont des enregistrements et les colonnes sont des champs dans ces enregistrements.
Les tables de base de données ont généralement une valeur entière auto-incrémentée comme clé de recherche des lignes. C'est ce qu'on appelle la clé primaire. Chaque enregistrement de la table aura une clé primaire dont la valeur est unique dans toute la table. Avoir une clé primaire indépendante des données stockées dans la table vous permet de modifier n'importe quel autre champ de la ligne.
Remarque :
La clé primaire auto-incrémentée signifie que la base de données s'occupe de :
- Incrémenter le plus grand champ de clé primaire existant chaque fois qu'un nouvel enregistrement est inséré dans la table
- Utiliser cette valeur comme clé primaire pour les données nouvellement insérées
Cela garantit une clé primaire unique à mesure que la table grandit.
Vous allez suivre une convention de base de données consistant à nommer la table au singulier, de sorte que la table s'appellera person
. Traduire nos PEOPLE
structure ci-dessus dans une table de base de données nommée person
vous donne ceci :
id_personne | nom | fname | horodatage |
---|---|---|---|
1 | Farrell | Doug | 2018-08-08 21:16:01.888444 |
2 | Brockman | Kent | 2018-08-08 21:16:01.889060 |
3 | Pâques | Lapin | 2018-08-08 21:16:01.886834 |
Chaque colonne de la table a un nom de champ comme suit :
person_id
: champ clé primaire pour chaque personnelname
: nom de famille de la personnefname
: prénom de la personnetimestamp
: horodatage associé aux actions d'insertion/mise à jour
Interaction avec la base de données
Vous allez utiliser SQLite comme moteur de base de données pour stocker les PEOPLE
Les données. SQLite est la base de données la plus largement distribuée dans le monde, et elle est fournie gratuitement avec Python. Il est rapide, effectue tout son travail à l'aide de fichiers et convient à un grand nombre de projets. Il s'agit d'un RDBMS (Relational Database Management System) complet qui inclut SQL, le langage de nombreux systèmes de bases de données.
Pour le moment, imaginez la person
table existe déjà dans une base de données SQLite. Si vous avez déjà utilisé RDBMS, vous connaissez probablement SQL, le langage de requête structuré que la plupart des RDBMS utilisent pour interagir avec la base de données.
Contrairement aux langages de programmation comme Python, SQL ne définit pas comment pour obtenir les données :elles décrivent quoi les données sont souhaitées, en laissant le comment jusqu'au moteur de base de données.
Une requête SQL récupérant toutes les données de notre person
tableau, trié par nom de famille, ressemblerait à ceci :
SELECT * FROM person ORDER BY 'lname';
Cette requête indique au moteur de base de données d'obtenir tous les champs de la table person et de les trier dans l'ordre croissant par défaut en utilisant le lname
domaine.
Si vous deviez exécuter cette requête sur une base de données SQLite contenant la person
table, les résultats seraient un ensemble d'enregistrements contenant toutes les lignes de la table, chaque ligne contenant les données de tous les champs constituant une ligne. Vous trouverez ci-dessous un exemple utilisant l'outil de ligne de commande SQLite exécutant la requête ci-dessus sur la person
table de base de données :
sqlite> SELECT * FROM person ORDER BY lname;
2|Brockman|Kent|2018-08-08 21:16:01.888444
3|Easter|Bunny|2018-08-08 21:16:01.889060
1|Farrell|Doug|2018-08-08 21:16:01.886834
La sortie ci-dessus est une liste de toutes les lignes de person
table de base de données avec des caractères pipe ('|') séparant les champs de la ligne, ce qui est fait à des fins d'affichage par SQLite.
Python est tout à fait capable de s'interfacer avec de nombreux moteurs de base de données et d'exécuter la requête SQL ci-dessus. Les résultats seraient très probablement une liste de tuples. La liste externe contient tous les enregistrements de la person
table. Chaque tuple interne individuel contiendrait toutes les données représentant chaque champ défini pour une ligne de table.
Obtenir des données de cette façon n'est pas très Pythonique. La liste des enregistrements est correcte, mais chaque enregistrement individuel n'est qu'un tuple de données. C'est au programme de connaître l'index de chaque champ afin de récupérer un champ particulier. Le code Python suivant utilise SQLite pour montrer comment exécuter la requête ci-dessus et afficher les données :
1import sqlite3
2
3conn = sqlite3.connect('people.db')
4cur = conn.cursor()
5cur.execute('SELECT * FROM person ORDER BY lname')
6people = cur.fetchall()
7for person in people:
8 print(f'{person[2]} {person[1]}')
Le programme ci-dessus effectue les opérations suivantes :
-
Ligne 1 importe le
sqlite3
module. -
Ligne 3 crée une connexion au fichier de base de données.
-
Ligne 4 crée un curseur à partir de la connexion.
-
Ligne 5 utilise le curseur pour exécuter un
SQL
requête exprimée sous forme de chaîne. -
Ligne 6 récupère tous les enregistrements renvoyés par le
SQL
requête et les attribue auxpeople
variables. -
Lignes 7 et 8 parcourir les
people
list variable et imprimez le prénom et le nom de chaque personne.
Les people
variable de Ligne 6 ci-dessus ressemblerait à ceci en Python :
people = [
(2, 'Brockman', 'Kent', '2018-08-08 21:16:01.888444'),
(3, 'Easter', 'Bunny', '2018-08-08 21:16:01.889060'),
(1, 'Farrell', 'Doug', '2018-08-08 21:16:01.886834')
]
La sortie du programme ci-dessus ressemble à ceci :
Kent Brockman
Bunny Easter
Doug Farrell
Dans le programme ci-dessus, il faut savoir que le prénom d'une personne est à l'index 2
, et le nom de famille d'une personne est à l'index 1
. Pire, la structure interne de person
doit également être connu chaque fois que vous passez la variable d'itération person
en tant que paramètre d'une fonction ou d'une méthode.
Ce serait bien mieux si ce que vous avez obtenu en retour pour person
était un objet Python, où chacun des champs est un attribut de l'objet. C'est l'une des choses que fait SQLAlchemy.
Petites tables Bobby
Dans le programme ci-dessus, l'instruction SQL est une simple chaîne transmise directement à la base de données pour exécution. Dans ce cas, ce n'est pas un problème car le SQL est un littéral de chaîne entièrement sous le contrôle du programme. Cependant, le cas d'utilisation de votre API REST prendra l'entrée utilisateur de l'application Web et l'utilisera pour créer des requêtes SQL. Cela peut ouvrir votre application aux attaques.
Vous vous souviendrez de la partie 1 que l'API REST pour obtenir une seule person
des PEOPLE
les données ressemblaient à ceci :
GET /api/people/{lname}
Cela signifie que votre API attend une variable, lname
, dans le chemin du point de terminaison de l'URL, qu'il utilise pour rechercher une seule person
. Modifier le code Python SQLite ci-dessus pour ce faire ressemblerait à ceci :
1lname = 'Farrell'
2cur.execute('SELECT * FROM person WHERE lname = \'{}\''.format(lname))
L'extrait de code ci-dessus effectue les opérations suivantes :
-
Ligne 1 définit le
lname
variable en'Farrell'
. Cela proviendrait du chemin du point de terminaison de l'URL de l'API REST. -
Ligne 2 utilise le formatage de chaîne Python pour créer une chaîne SQL et l'exécuter.
Pour garder les choses simples, le code ci-dessus définit le lname
variable à une constante, mais en réalité, cela proviendrait du chemin du point de terminaison de l'URL de l'API et pourrait être tout ce qui est fourni par l'utilisateur. Le SQL généré par le formatage de chaîne ressemble à ceci :
SELECT * FROM person WHERE lname = 'Farrell'
Lorsque ce SQL est exécuté par la base de données, il recherche la person
table pour un enregistrement où le nom de famille est égal à 'Farrell'
. C'est ce qui est prévu, mais tout programme qui accepte les entrées de l'utilisateur est également ouvert aux utilisateurs malveillants. Dans le programme ci-dessus, où le lname
variable est définie par une entrée fournie par l'utilisateur, cela ouvre votre programme à ce qu'on appelle une attaque par injection SQL. C'est ce qu'on appelle affectueusement les tables Little Bobby :
Par exemple, imaginez qu'un utilisateur malveillant appelle votre API REST de cette manière :
GET /api/people/Farrell');DROP TABLE person;
La requête API REST ci-dessus définit le lname
variable à 'Farrell');DROP TABLE person;'
, qui dans le code ci-dessus générerait cette instruction SQL :
SELECT * FROM person WHERE lname = 'Farrell');DROP TABLE person;
L'instruction SQL ci-dessus est valide et, lorsqu'elle est exécutée par la base de données, elle trouvera un enregistrement où lname
correspond à 'Farrell'
. Ensuite, il trouvera le caractère délimiteur de l'instruction SQL ;
et ira de l'avant et laissera tomber toute la table. Cela détruirait essentiellement votre application.
Vous pouvez protéger votre programme en désinfectant toutes les données que vous obtenez des utilisateurs de votre application. La désinfection des données dans ce contexte signifie que votre programme examine les données fournies par l'utilisateur et s'assure qu'elles ne contiennent rien de dangereux pour le programme. Cela peut être difficile à faire correctement et devrait être fait partout où les données utilisateur interagissent avec la base de données.
Il existe un autre moyen beaucoup plus simple :utilisez SQLAlchemy. Il nettoiera les données utilisateur pour vous avant de créer des instructions SQL. C'est un autre gros avantage et une raison d'utiliser SQLAlchemy lorsque vous travaillez avec des bases de données.
Modélisation des données avec SQLAlchemy
SQLAlchemy est un gros projet et fournit de nombreuses fonctionnalités pour travailler avec des bases de données utilisant Python. L'une des choses qu'il fournit est un ORM, ou Object Relational Mapper, et c'est ce que vous allez utiliser pour créer et travailler avec la person
tableau de la base de données. Cela vous permet de mapper une ligne de champs de la table de base de données à un objet Python.
La programmation orientée objet vous permet de connecter des données avec un comportement, les fonctions qui opèrent sur ces données. En créant des classes SQLAlchemy, vous pouvez connecter les champs des lignes de la table de la base de données au comportement, ce qui vous permet d'interagir avec les données. Voici la définition de la classe SQLAlchemy pour les données dans le person
table de base de données :
class Person(db.Model):
__tablename__ = 'person'
person_id = db.Column(db.Integer,
primary_key=True)
lname = db.Column(db.String)
fname = db.Column(db.String)
timestamp = db.Column(db.DateTime,
default=datetime.utcnow,
onupdate=datetime.utcnow)
La classe Person
hérite de db.Model
, auquel vous accéderez lorsque vous commencerez à créer le code du programme. Pour l'instant, cela signifie que vous héritez d'une classe de base appelée Model
, fournissant des attributs et des fonctionnalités communs à toutes les classes qui en sont dérivées.
Les autres définitions sont des attributs de niveau classe définis comme suit :
-
__tablename__ = 'person'
connecte la définition de classe à laperson
table de base de données. -
person_id = db.Column(db.Integer, primary_key=True)
crée une colonne de base de données contenant un entier servant de clé primaire pour la table. Cela indique également à la base de données queperson_id
sera une valeur entière auto-incrémentée. -
lname = db.Column(db.String)
crée le champ du nom de famille, une colonne de base de données contenant une valeur de chaîne. -
fname = db.Column(db.String)
crée le champ prénom, une colonne de base de données contenant une valeur de chaîne. -
timestamp = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
crée un champ d'horodatage, une colonne de base de données contenant une valeur de date/heure. Ledefault=datetime.utcnow
paramètre définit par défaut la valeur d'horodatage sur leutcnow
actuel valeur lors de la création d'un enregistrement. Leonupdate=datetime.utcnow
le paramètre met à jour l'horodatage avec leutcnow
actuel valeur lorsque l'enregistrement est mis à jour.
Remarque :Horodatages UTC
Vous vous demandez peut-être pourquoi l'horodatage de la classe ci-dessus est par défaut et est mis à jour par datetime.utcnow()
méthode, qui renvoie un UTC, ou Coordinated Universal Time. C'est un moyen de normaliser la source de votre horodatage.
La source, ou temps zéro, est une ligne allant du nord au sud du pôle nord au sud de la Terre à travers le Royaume-Uni. Il s'agit du fuseau horaire zéro à partir duquel tous les autres fuseaux horaires sont décalés. En l'utilisant comme source de temps zéro, vos horodatages sont décalés par rapport à ce point de référence standard.
Si votre application est accessible à partir de différents fuseaux horaires, vous disposez d'un moyen d'effectuer des calculs de date/heure. Tout ce dont vous avez besoin est un horodatage UTC et le fuseau horaire de destination.
Si vous deviez utiliser les fuseaux horaires locaux comme source d'horodatage, vous ne pourriez pas effectuer de calculs de date/heure sans informations sur les fuseaux horaires locaux décalés par rapport à l'heure zéro. Sans les informations de source d'horodatage, vous ne pourriez pas faire de comparaisons de date/heure ou de calculs du tout.
Travailler avec un horodatage basé sur UTC est une bonne norme à suivre. Voici un site de boîte à outils pour travailler avec et mieux les comprendre.
Où allez-vous avec cette Person
définition de classe ? L'objectif final est de pouvoir exécuter une requête à l'aide de SQLAlchemy et de récupérer une liste d'instances de Person
classe. À titre d'exemple, regardons l'instruction SQL précédente :
SELECT * FROM people ORDER BY lname;
Montrez le même petit exemple de programme ci-dessus, mais en utilisant maintenant SQLAlchemy :
1from models import Person
2
3people = Person.query.order_by(Person.lname).all()
4for person in people:
5 print(f'{person.fname} {person.lname}')
En ignorant la ligne 1 pour le moment, ce que vous voulez, c'est toute la person
enregistrements triés par ordre croissant par le lname
domaine. Ce que vous récupérez des instructions SQLAlchemy Person.query.order_by(Person.lname).all()
est une liste de Person
objets pour tous les enregistrements de la person
table de base de données dans cet ordre. Dans le programme ci-dessus, les people
la variable contient la liste de Person
objets.
Le programme itère sur les people
variable, prenant chaque person
tour à tour et imprimer le prénom et le nom de la personne à partir de la base de données. Notez que le programme n'a pas besoin d'utiliser des index pour obtenir le fname
ou lname
valeurs :il utilise les attributs définis sur le Person
objet.
L'utilisation de SQLAlchemy vous permet de penser en termes d'objets avec un comportement plutôt qu'en SQL
brut . Cela devient encore plus avantageux lorsque vos tables de base de données deviennent plus grandes et les interactions plus complexes.
Sérialisation/Désérialisation des données modélisées
Travailler avec des données modélisées SQLAlchemy dans vos programmes est très pratique. C'est particulièrement pratique dans les programmes qui manipulent les données, en effectuant peut-être des calculs ou en les utilisant pour créer des présentations à l'écran. Votre application est une API REST fournissant essentiellement des opérations CRUD sur les données, et en tant que telle, elle n'effectue pas beaucoup de manipulation de données.
L'API REST fonctionne avec les données JSON, et ici vous pouvez rencontrer un problème avec le modèle SQLAlchemy. Étant donné que les données renvoyées par SQLAlchemy sont des instances de classe Python, Connexion ne peut pas sérialiser ces instances de classe en données au format JSON. Rappelez-vous de la partie 1 que Connexion est l'outil que vous avez utilisé pour concevoir et configurer l'API REST à l'aide d'un fichier YAML, et y connecter des méthodes Python.
Dans ce contexte, la sérialisation signifie la conversion d'objets Python, qui peuvent contenir d'autres objets Python et des types de données complexes, en structures de données plus simples pouvant être analysées en types de données JSON, répertoriés ici :
string
: un type de chaînenumber
: nombres pris en charge par Python (entiers, flottants, longs)object
: un objet JSON, qui équivaut à peu près à un dictionnaire Pythonarray
: à peu près équivalent à une liste Pythonboolean
: représenté dans JSON commetrue
oufalse
, mais en Python commeTrue
ouFalse
null
: essentiellement unNone
en Python
Par exemple, votre Person
la classe contient un horodatage, qui est un Python DateTime
. Il n'y a pas de définition de date/heure dans JSON, donc l'horodatage doit être converti en chaîne pour exister dans une structure JSON.
Votre Person
class est assez simple pour obtenir les attributs de données et créer manuellement un dictionnaire à renvoyer à partir de nos points de terminaison d'URL REST ne serait pas très difficile. Dans une application plus complexe avec de nombreux modèles SQLAlchemy plus grands, ce ne serait pas le cas. Une meilleure solution consiste à utiliser un module appelé Marshmallow pour faire le travail à votre place.
Marshmallow vous aide à créer un PersonSchema
classe, qui est comme SQLAlchemy Person
classe que nous avons créée. Ici cependant, au lieu de mapper les tables de base de données et les noms de champs à la classe et à ses attributs, le PersonSchema
class définit comment les attributs d'une classe seront convertis en formats compatibles JSON. Voici la définition de la classe Marshmallow pour les données dans notre person
tableau :
class PersonSchema(ma.ModelSchema):
class Meta:
model = Person
sqla_session = db.session
La classe PersonSchema
hérite de ma.ModelSchema
, auquel vous accéderez lorsque vous commencerez à créer le code du programme. Pour l'instant, cela signifie PersonSchema
hérite d'une classe de base Marshmallow appelée ModelSchema
, fournissant des attributs et des fonctionnalités communs à toutes les classes qui en sont dérivées.
Le reste de la définition est le suivant :
-
class Meta
définit une classe nomméeMeta
au sein de votre classe. LeModelSchema
classe que lePersonSchema
la classe hérite de looks pour ceMeta
interne class et l'utilise pour trouver le modèle SQLAlchemyPerson
et ledb.session
. C'est ainsi que Marshmallow trouve les attributs dans laPerson
class et le type de ces attributs afin qu'il sache comment les sérialiser/désérialiser. -
model
indique à la classe quel modèle SQLAlchemy utiliser pour sérialiser/désérialiser les données vers et depuis. -
db.session
indique à la classe quelle session de base de données utiliser pour introspecter et déterminer les types de données d'attribut.
Où allez-vous avec cette définition de classe ? Vous voulez pouvoir sérialiser une instance d'un Person
classe en données JSON, et pour désérialiser les données JSON et créer une Person
instances de classe à partir de celui-ci.
Créer la base de données initialisée
SQLAlchemy gère de nombreuses interactions spécifiques à des bases de données particulières et vous permet de vous concentrer sur les modèles de données ainsi que sur leur utilisation.
Maintenant que vous allez réellement créer une base de données, comme mentionné précédemment, vous allez utiliser SQLite. Vous faites cela pour plusieurs raisons. Il est livré avec Python et n'a pas besoin d'être installé en tant que module séparé. Il enregistre toutes les informations de la base de données dans un seul fichier et est donc facile à configurer et à utiliser.
L'installation d'un serveur de base de données séparé comme MySQL ou PostgreSQL fonctionnerait bien, mais nécessiterait d'installer ces systèmes et de les rendre opérationnels, ce qui dépasse le cadre de cet article.
Étant donné que SQLAlchemy gère la base de données, à bien des égards, peu importe la base de données sous-jacente.
Vous allez créer un nouveau programme utilitaire appelé build_database.py
pour créer et initialiser le SQLite people.db
fichier de base de données contenant votre person
tableau de la base de données. En cours de route, vous créerez deux modules Python, config.py
et models.py
, qui sera utilisé par build_database.py
et le server.py
modifié de la partie 1.
Voici où vous pouvez trouver le code source des modules que vous êtes sur le point de créer, qui sont présentés ici :
-
config.py
obtient les modules nécessaires importés dans le programme et configurés. Cela inclut Flask, Connexion, SQLAlchemy et Marshmallow. Parce qu'il sera utilisé à la fois parbuild_database.py
etserver.py
, certaines parties de la configuration ne s'appliqueront qu'auserver.py
application. -
models.py
est le module où vous allez créer laPerson
SQLAlchemy etPersonSchema
Définitions de classe Marshmallow décrites ci-dessus. Ce module dépend deconfig.py
pour certains des objets qui y sont créés et configurés.
Module de configuration
Le config.py
module, comme son nom l'indique, est l'endroit où toutes les informations de configuration sont créées et initialisées. Nous allons utiliser ce module à la fois pour notre build_database.py
fichier programme et le server.py
qui sera bientôt mis à jour fichier de l'article de la partie 1. Cela signifie que nous allons configurer Flask, Connexion, SQLAlchemy et Marshmallow ici.
Même si le build_database.py
programme n'utilise pas Flask, Connexion ou Marshmallow, il utilise SQLAlchemy pour créer notre connexion à la base de données SQLite. Voici le code pour le config.py
modules :
1import os
2import connexion
3from flask_sqlalchemy import SQLAlchemy
4from flask_marshmallow import Marshmallow
5
6basedir = os.path.abspath(os.path.dirname(__file__))
7
8# Create the Connexion application instance
9connex_app = connexion.App(__name__, specification_dir=basedir)
10
11# Get the underlying Flask app instance
12app = connex_app.app
13
14# Configure the SQLAlchemy part of the app instance
15app.config['SQLALCHEMY_ECHO'] = True
16app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////' + os.path.join(basedir, 'people.db')
17app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
18
19# Create the SQLAlchemy db instance
20db = SQLAlchemy(app)
21
22# Initialize Marshmallow
23ma = Marshmallow(app)
Voici ce que fait le code ci-dessus :
-
Lignes 2 à 4 importez Connexion comme vous l'avez fait dans le
server.py
programme de la partie 1. Il importe égalementSQLAlchemy
duflask_sqlalchemy
module. Cela donne à votre programme l'accès à la base de données. Enfin, il importeMarshmallow
duflask_marshamllow
module. -
Ligne 6 crée la variable
basedir
pointant vers le répertoire dans lequel le programme s'exécute. -
Ligne 9 utilise le
basedir
variable pour créer l'instance de l'application Connexion et lui donner le chemin vers leswagger.yml
fichier. -
Ligne 12 crée une variable
app
, qui est l'instance Flask initialisée par Connexion. -
Lignes 15 utilise l'
app
variable pour configurer les valeurs utilisées par SQLAlchemy. D'abord, il définitSQLALCHEMY_ECHO
àTrue
. Cela amène SQLAlchemy à faire écho aux instructions SQL qu'il exécute sur la console. Ceci est très utile pour déboguer les problèmes lors de la construction de programmes de base de données. Définissez ceci surFalse
pour les environnements de production. -
Ligne 16 définit
SQLALCHEMY_DATABASE_URI
àsqlite:////' + os.path.join(basedir, 'people.db')
. Cela indique à SQLAlchemy d'utiliser SQLite comme base de données et un fichier nommépeople.db
dans le répertoire courant en tant que fichier de base de données. Différents moteurs de base de données, comme MySQL et PostgreSQL, auront différentsSQLALCHEMY_DATABASE_URI
chaînes pour les configurer. -
Ligne 17 définit
SQLALCHEMY_TRACK_MODIFICATIONS
àFalse
, en désactivant le système d'événements SQLAlchemy, qui est activé par défaut. Le système d'événements génère des événements utiles dans les programmes événementiels, mais ajoute une surcharge importante. Puisque vous ne créez pas de programme événementiel, désactivez cette fonctionnalité. -
Ligne 19 crée la
db
variable en appelantSQLAlchemy(app)
. Cela initialise SQLAlchemy en passant leapp
les informations de configuration viennent d'être définies. Ladb
la variable est ce qui est importé dans lebuild_database.py
programme pour lui donner accès à SQLAlchemy et à la base de données. Il servira le même objectif dans leserver.py
programme etpeople.py
module. -
Ligne 23 crée le
ma
variable en appelantMarshmallow(app)
. Cela initialise Marshmallow et lui permet d'introspecter les composants SQLAlchemy attachés à l'application. C'est pourquoi Marshmallow est initialisé après SQLAlchemy.
Module Modèles
Le models.py
module est créé pour fournir la Person
et PersonSchema
classes exactement comme décrit dans les sections ci-dessus sur la modélisation et la sérialisation des données. Voici le code de ce module :
1from datetime import datetime
2from config import db, ma
3
4class Person(db.Model):
5 __tablename__ = 'person'
6 person_id = db.Column(db.Integer, primary_key=True)
7 lname = db.Column(db.String(32), index=True)
8 fname = db.Column(db.String(32))
9 timestamp = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
10
11class PersonSchema(ma.ModelSchema):
12 class Meta:
13 model = Person
14 sqla_session = db.session
Voici ce que fait le code ci-dessus :
-
Ligne 1 importe le
datetime
objet de ladatetime
module fourni avec Python. Cela vous donne un moyen de créer un horodatage dans lePerson
classe. -
Ligne 2 importe la
db
etma
variables d'instance définies dans leconfig.py
module. Cela donne au module l'accès aux attributs et méthodes SQLAlchemy attachés audb
variable, et les attributs et méthodes Marshmallow attachés auma
variables. -
Lignes 4 à 9 définir la
Person
class comme indiqué dans la section sur la modélisation des données ci-dessus, mais maintenant vous savez où se trouve ledb.Model
dont la classe hérite. Cela donne laPerson
les fonctionnalités de la classe SQLAlchemy, comme une connexion à la base de données et l'accès à ses tables. -
Lignes 11 à 14 définir le
PersonSchema
class as was discussed in the data serialzation section above. This class inherits fromma.ModelSchema
and gives thePersonSchema
class Marshmallow features, like introspecting thePerson
class to help serialize/deserialize instances of that class.
Creating the Database
You’ve seen how database tables can be mapped to SQLAlchemy classes. Now use what you’ve learned to create the database and populate it with data. You’re going to build a small utility program to create and build the database with the People
Les données. Here’s the build_database.py
program:
1import os
2from config import db
3from models import Person
4
5# Data to initialize database with
6PEOPLE = [
7 {'fname': 'Doug', 'lname': 'Farrell'},
8 {'fname': 'Kent', 'lname': 'Brockman'},
9 {'fname': 'Bunny','lname': 'Easter'}
10]
11
12# Delete database file if it exists currently
13if os.path.exists('people.db'):
14 os.remove('people.db')
15
16# Create the database
17db.create_all()
18
19# Iterate over the PEOPLE structure and populate the database
20for person in PEOPLE:
21 p = Person(lname=person['lname'], fname=person['fname'])
22 db.session.add(p)
23
24db.session.commit()
Here’s what the above code is doing:
-
Line 2 imports the
db
instance from theconfig.py
module. -
Line 3 imports the
Person
class definition from themodels.py
module. -
Lines 6 – 10 create the
PEOPLE
data structure, which is a list of dictionaries containing your data. The structure has been condensed to save presentation space. -
Lines 13 &14 perform some simple housekeeping to delete the
people.db
file, if it exists. This file is where the SQLite database is maintained. If you ever have to re-initialize the database to get a clean start, this makes sure you’re starting from scratch when you build the database. -
Line 17 creates the database with the
db.create_all()
appel. This creates the database by using thedb
instance imported from theconfig
module. Ladb
instance is our connection to the database. -
Lines 20 – 22 iterate over the
PEOPLE
list and use the dictionaries within to instantiate aPerson
classe. After it is instantiated, you call thedb.session.add(p)
une fonction. This uses the database connection instancedb
to access thesession
objet. The session is what manages the database actions, which are recorded in the session. In this case, you are executing theadd(p)
method to add the newPerson
instance to thesession
object. -
Line 24 calls
db.session.commit()
to actually save all the person objects created to the database.
Remarque : At Line 22, no data has been added to the database. Everything is being saved within the session
objet. Only when you execute the db.session.commit()
call at Line 24 does the session interact with the database and commit the actions to it.
In SQLAlchemy, the session
is an important object. It acts as the conduit between the database and the SQLAlchemy Python objects created in a program. The session
helps maintain the consistency between data in the program and the same data as it exists in the database. It saves all database actions and will update the underlying database accordingly by both explicit and implicit actions taken by the program.
Now you’re ready to run the build_database.py
program to create and initialize the new database. You do so with the following command, with your Python virtual environment active:
python build_database.py
When the program runs, it will print SQLAlchemy log messages to the console. These are the result of setting SQLALCHEMY_ECHO
to True
in the config.py
dossier. Much of what’s being logged by SQLAlchemy is the SQL
commands it’s generating to create and build the people.db
SQLite database file. Here’s an example of what’s printed out when the program is run:
2018-09-11 22:20:29,951 INFO sqlalchemy.engine.base.Engine SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
2018-09-11 22:20:29,951 INFO sqlalchemy.engine.base.Engine ()
2018-09-11 22:20:29,952 INFO sqlalchemy.engine.base.Engine SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
2018-09-11 22:20:29,952 INFO sqlalchemy.engine.base.Engine ()
2018-09-11 22:20:29,956 INFO sqlalchemy.engine.base.Engine PRAGMA table_info("person")
2018-09-11 22:20:29,956 INFO sqlalchemy.engine.base.Engine ()
2018-09-11 22:20:29,959 INFO sqlalchemy.engine.base.Engine
CREATE TABLE person (
person_id INTEGER NOT NULL,
lname VARCHAR,
fname VARCHAR,
timestamp DATETIME,
PRIMARY KEY (person_id)
)
2018-09-11 22:20:29,959 INFO sqlalchemy.engine.base.Engine ()
2018-09-11 22:20:29,975 INFO sqlalchemy.engine.base.Engine COMMIT
2018-09-11 22:20:29,980 INFO sqlalchemy.engine.base.Engine BEGIN (implicit)
2018-09-11 22:20:29,983 INFO sqlalchemy.engine.base.Engine INSERT INTO person (lname, fname, timestamp) VALUES (?, ?, ?)
2018-09-11 22:20:29,983 INFO sqlalchemy.engine.base.Engine ('Farrell', 'Doug', '2018-09-12 02:20:29.983143')
2018-09-11 22:20:29,984 INFO sqlalchemy.engine.base.Engine INSERT INTO person (lname, fname, timestamp) VALUES (?, ?, ?)
2018-09-11 22:20:29,985 INFO sqlalchemy.engine.base.Engine ('Brockman', 'Kent', '2018-09-12 02:20:29.984821')
2018-09-11 22:20:29,985 INFO sqlalchemy.engine.base.Engine INSERT INTO person (lname, fname, timestamp) VALUES (?, ?, ?)
2018-09-11 22:20:29,985 INFO sqlalchemy.engine.base.Engine ('Easter', 'Bunny', '2018-09-12 02:20:29.985462')
2018-09-11 22:20:29,986 INFO sqlalchemy.engine.base.Engine COMMIT
Using the Database
Once the database has been created, you can modify the existing code from Part 1 to make use of it. All of the modifications necessary are due to creating the person_id
primary key value in our database as the unique identifier rather than the lname
value.
Update the REST API
None of the changes are very dramatic, and you’ll start by re-defining the REST API. The list below shows the API definition from Part 1 but is updated to use the person_id
variable in the URL path:
Action | HTTP Verb | URL Path | Description |
---|---|---|---|
Create | POST | /api/people | Defines a unique URL to create a new person |
Read | GET | /api/people | Defines a unique URL to read a collection of people |
Read | GET | /api/people/{person_id} | Defines a unique URL to read a particular person by person_id |
Update | PUT | /api/people/{person_id} | Defines a unique URL to update an existing person by person_id |
Delete | DELETE | /api/orders/{person_id} | Defines a unique URL to delete an existing person by person_id |
Where the URL definitions required an lname
value, they now require the person_id
(primary key) for the person record in the people
table. This allows you to remove the code in the previous app that artificially restricted users from editing a person’s last name.
In order for you to implement these changes, the swagger.yml
file from Part 1 will have to be edited. For the most part, any lname
parameter value will be changed to person_id
, and person_id
will be added to the POST
and PUT
responses. You can check out the updated swagger.yml
file.
Update the REST API Handlers
With the swagger.yml
file updated to support the use of the person_id
identifier, you’ll also need to update the handlers in the people.py
file to support these changes. In the same way that the swagger.yml
file was updated, you need to change the people.py
file to use the person_id
value rather than lname
.
Here’s part of the updated person.py
module showing the handler for the REST URL endpoint GET /api/people
:
1from flask import (
2 make_response,
3 abort,
4)
5from config import db
6from models import (
7 Person,
8 PersonSchema,
9)
10
11def read_all():
12 """
13 This function responds to a request for /api/people
14 with the complete lists of people
15
16 :return: json string of list of people
17 """
18 # Create the list of people from our data
19 people = Person.query \
20 .order_by(Person.lname) \
21 .all()
22
23 # Serialize the data for the response
24 person_schema = PersonSchema(many=True)
25 return person_schema.dump(people).data
Here’s what the above code is doing:
-
Lines 1 – 9 import some Flask modules to create the REST API responses, as well as importing the
db
instance from theconfig.py
module. In addition, it imports the SQLAlchemyPerson
and MarshmallowPersonSchema
classes to access theperson
database table and serialize the results. -
Line 11 starts the definition of
read_all()
that responds to the REST API URL endpointGET /api/people
and returns all the records in theperson
database table sorted in ascending order by last name. -
Lines 19 – 22 tell SQLAlchemy to query the
person
database table for all the records, sort them in ascending order (the default sorting order), and return a list ofPerson
Python objects as the variablepeople
. -
Line 24 is where the Marshmallow
PersonSchema
class definition becomes valuable. You create an instance of thePersonSchema
, passing it the parametermany=True
. This tellsPersonSchema
to expect an interable to serialize, which is what thepeople
variable is. -
Line 25 uses the
PersonSchema
instance variable (person_schema
), calling itsdump()
method with thepeople
liste. The result is an object having adata
attribute, an object containing apeople
list that can be converted to JSON. This is returned and converted by Connexion to JSON as the response to the REST API call.
Remarque : The people
list variable created on Line 24 above can’t be returned directly because Connexion won’t know how to convert the timestamp
field into JSON. Returning the list of people without processing it with Marshmallow results in a long error traceback and finally this Exception:
TypeError: Object of type Person is not JSON serializable
Here’s another part of the person.py
module that makes a request for a single person from the person
base de données. Here, read_one(person_id)
function receives a person_id
from the REST URL path, indicating the user is looking for a specific person. Here’s part of the updated person.py
module showing the handler for the REST URL endpoint GET /api/people/{person_id}
:
1def read_one(person_id):
2 """
3 This function responds to a request for /api/people/{person_id}
4 with one matching person from people
5
6 :param person_id: ID of person to find
7 :return: person matching ID
8 """
9 # Get the person requested
10 person = Person.query \
11 .filter(Person.person_id == person_id) \
12 .one_or_none()
13
14 # Did we find a person?
15 if person is not None:
16
17 # Serialize the data for the response
18 person_schema = PersonSchema()
19 return person_schema.dump(person).data
20
21 # Otherwise, nope, didn't find that person
22 else:
23 abort(404, 'Person not found for Id: {person_id}'.format(person_id=person_id))
Here’s what the above code is doing:
-
Lines 10 – 12 use the
person_id
parameter in a SQLAlchemy query using thefilter
method of the query object to search for a person with aperson_id
attribute matching the passed-inperson_id
. Rather than using theall()
query method, use theone_or_none()
method to get one person, or returnNone
if no match is found. -
Line 15 determines whether a
person
was found or not. -
Line 17 shows that, if
person
was notNone
(a matchingperson
was found), then serializing the data is a little different. You don’t pass themany=True
parameter to the creation of thePersonSchema()
instance. Instead, you passmany=False
because only a single object is passed in to serialize. -
Line 18 is where the
dump
method ofperson_schema
is called, and thedata
attribute of the resulting object is returned. -
Line 23 shows that, if
person
wasNone
(a matching person wasn’t found), then the Flaskabort()
method is called to return an error.
Another modification to person.py
is creating a new person in the database. This gives you an opportunity to use the Marshmallow PersonSchema
to deserialize a JSON structure sent with the HTTP request to create a SQLAlchemy Person
objet. Here’s part of the updated person.py
module showing the handler for the REST URL endpoint POST /api/people
:
1def create(person):
2 """
3 This function creates a new person in the people structure
4 based on the passed-in person data
5
6 :param person: person to create in people structure
7 :return: 201 on success, 406 on person exists
8 """
9 fname = person.get('fname')
10 lname = person.get('lname')
11
12 existing_person = Person.query \
13 .filter(Person.fname == fname) \
14 .filter(Person.lname == lname) \
15 .one_or_none()
16
17 # Can we insert this person?
18 if existing_person is None:
19
20 # Create a person instance using the schema and the passed-in person
21 schema = PersonSchema()
22 new_person = schema.load(person, session=db.session).data
23
24 # Add the person to the database
25 db.session.add(new_person)
26 db.session.commit()
27
28 # Serialize and return the newly created person in the response
29 return schema.dump(new_person).data, 201
30
31 # Otherwise, nope, person exists already
32 else:
33 abort(409, f'Person {fname} {lname} exists already')
Here’s what the above code is doing:
-
Line 9 &10 set the
fname
andlname
variables based on thePerson
data structure sent as thePOST
body of the HTTP request. -
Lines 12 – 15 use the SQLAlchemy
Person
class to query the database for the existence of a person with the samefname
andlname
as the passed-inperson
. -
Line 18 addresses whether
existing_person
isNone
. (existing_person
was not found.) -
Line 21 creates a
PersonSchema()
instance calledschema
. -
Line 22 uses the
schema
variable to load the data contained in theperson
parameter variable and create a new SQLAlchemyPerson
instance variable callednew_person
. -
Line 25 adds the
new_person
instance to thedb.session
. -
Line 26 commits the
new_person
instance to the database, which also assigns it a new primary key value (based on the auto-incrementing integer) and a UTC-based timestamp. -
Line 33 shows that, if
existing_person
is notNone
(a matching person was found), then the Flaskabort()
method is called to return an error.
Update the Swagger UI
With the above changes in place, your REST API is now functional. The changes you’ve made are also reflected in an updated swagger UI interface and can be interacted with in the same manner. Below is a screenshot of the updated swagger UI opened to the GET /people/{person_id}
section. This section of the UI gets a single person from the database and looks like this:
As shown in the above screenshot, the path parameter lname
has been replaced by person_id
, which is the primary key for a person in the REST API. The changes to the UI are a combined result of changing the swagger.yml
file and the code changes made to support that.
Update the Web Application
The REST API is running, and CRUD operations are being persisted to the database. So that it is possible to view the demonstration web application, the JavaScript code has to be updated.
The updates are again related to using person_id
instead of lname
as the primary key for person data. In addition, the person_id
is attached to the rows of the display table as HTML data attributes named data-person-id
, so the value can be retrieved and used by the JavaScript code.
This article focused on the database and making your REST API use it, which is why there’s just a link to the updated JavaScript source and not much discussion of what it does.
Example Code
All of the example code for this article is available here. There’s one version of the code containing all the files, including the build_database.py
utility program and the server.py
modified example program from Part 1.
Conclusion
Congratulations, you’ve covered a lot of new material in this article and added useful tools to your arsenal!
You’ve learned how to save Python objects to a database using SQLAlchemy. You’ve also learned how to use Marshmallow to serialize and deserialize SQLAlchemy objects and use them with a JSON REST API. The things you’ve learned have certainly been a step up in complexity from the simple REST API of Part 1, but that step has given you two very powerful tools to use when creating more complex applications.
SQLAlchemy and Marshmallow are amazing tools in their own right. Using them together gives you a great leg up to create your own web applications backed by a database.
In Part 3 of this series, you’ll focus on the R
part of RDBMS
:relationships, which provide even more power when you are using a database.