Back End
4.0 Je ne comprends rien !
Voici la procédure :
- Lire le cours back end en détail au moins 3 fois.
⚠ ATTENTION cette tâche prend forcément plusieurs heures !
- Faire le TP back end pour de vrai.
⚠ ATTENTION cette tâche prend forcément au moins une journée !
- RTFM chercher un peu dans les documentations pertinentes.
- Demander à un·e prof…
4.1 Récupérer un seul tuple
Si SELECT renvoie au plus un tuple, il faut ajouter un ^
derrière le nom de
la fonction dans queries.sql
pour éviter à gérer la liste retournée.
Voir transparents 74 du cours Back-end.
Si le tuple ne contient qu'un seul attribut, voir la suivante.
4.2 Insérer et récupérer l'identifiant
Voir transparents 76 à 78 du cours Back-end
- dans
queries.sql
: requête$
avecRETURNING ...
- dans
app.py
: affectation du résultat de la requête à une variable
💡 On peut même faire mieux en détectant au passage si il y a un conflit avec
ON CONFLIT DO NOTHING
et leRETURNING
renvoieNone
dans ce cas.
4.3 Debug de l'application
Pour ajuster le niveau de verbosité des logs de l'application, ajouter dans local.conf
:
Cette configuration affecte le niveau de traces de log
dans app.py
.
RTFM Python logging
.
4.4 Je veux des images
Une base de données relationnelle n'est pas l'endroit idéal pour stocker des images.
Si c'était nécessaire on utiliserait le type BYTEA
(tableau d'octets).
Plus raisonnablement :
- on stocke plutôt l'image comme une URL pointant vers un fichier dans un dossier, et on garde dans la base de données le chemin.
- pour donner un nom aléatoire à un fichier, voir
uuid
. - le serveur de test flask sert automatiquement le contenu du sous répertoire
./static/
avec la route/static/...
. Pour des premiers tests locaux, c'est un bon endroit pour placer quelques fichiers. - dans tous les cas, le répertoire utilisé par l'application doit être configurable.
- voir aussi la question upload de fichiers ci-dessous.
Dans la vraie vie, les images sont souvent stockées par un autre service (AWS S3, Azure Blob Storage ou Google Cloud Storage) et juste référencées par l'application. Transférer des images de l'application vers ce genre de service nécessite de régler quelques questions d'authentification et d'autorisations.
💡 Voir aussi la question 4.8 sur le téléversement de fichiers.
4.5 Je veux du Full Text Search !
Il s'agit de faire de la recherche plein texte dans des textes potentiellement un peu longs (paragraphes, pages…). Les textes à analyser sont préparés en retirant les caractères et mots inutiles (un à de le…), en normalisant ( conjugaison, déclinaison, pluriel, racinisation…), en classant (mots fréquents plus importants…). Cette préparation dépend de la langue du texte. Ensuite, une recherche peut être faite sur le texte préparé avec un petit langage d'interrogation. Il est aussi possible de classer les résultats de la recherche (mots proches vs éloignés, fréquences…). Des index spécifiques permettent d'accélérer une recherche sur des gros volumes.
- v0 les mains dans les poches
J'écris une requête qui fait un filtrage (du style %foo%
) sur les champs qui m'intéressent.
Quand ça marche, et si ça ne convient pas, ou si j'ai un peu de temps pour améliorer des choses à la fin,
je regarde le point suivant.
Note: si on veut une liste de mots avec cette approche, on peut faire une fonction PL/pgSQL qui gère un tableau/liste SQL.
- v1 avec Postgres
Il y a un chapitre entier sur la question,
RTFM !
On fait des requêtes avec tsquery
et l'opérateur @@
sur un tsvector
construit sur un ou des champs textes, on
peut aider avec un index GIN
sur une expression identique. Préciser la langue permet de filtrer (mots
inutiles un une des
) et normaliser (singulier vs pluriel, conjugaisons) donc améliorer la qualité.
-- recherche avec calcul du rang
SELECT
ts_rank_cd(to_tsvector('french', steps),
to_tsquery('french', 'fraise|framboise')), *
FROM Recipe
WHERE to_tsvector('french', steps) @@ to_tsquery('french', 'fraise|framboise')
ORDER BY 1 DESC
LIMIT 3;
-- on peut éventuellement aider les recherches avec un index GIN
CREATE INDEX recipe_fts ON Recipe USING GIN ((to_tsvector('french', steps)));
💡 On pourrait aussi stocker le
tsvector
comme un attributGENERATED ALWAYS
ou le recalculer via une trigger pour éviter de le regénérer à chaque requête.
Quand ça marche, et si ça ne convient pas (ce qui serait très étonnant), et si on a du temps disponible (toutes les users stories essentielles sont implémentées), on regarde éventuellement le point suivant, pour se cultiver.
-
v42 avec un outil type ElasticSearch, ou un service (AWS OpenSearch, Azure Elastic ou Google Cloud Search).
-
v128 avec l'extension
pgvector
on peut stocker des vecteurs de features générés pour chaque tuples par un réseau de neurones de OpenAPI (€), et on regarde ensuite une distance (locale) entre ces vecteurs et celui du critère de recherche (à nouveau €).
4.6 Découpage de queries.sql
C'est une excellente idée. Si c'est la priorité pour créer de la valeur client :
- corriger la configuration
ANODB
danslocal.conf
etserver.conf
: - ajouter bien sûr le(s) nouveau(x) fichier(s) de requête sous
git
.
💡 il est aussi possible de mettre des fichiers SQL dans un sous répertoire et de le charger à la racine. RTFM.
4.7 Découpage de app.py
C'est une excellente idée. Si c'est la priorité pour créer de la valeur
client, voir dans le cours back end les 3 slides de la section blueprint.
Vous devrez aussi importer l'objet db
de database
. Attention de bien
utiliser current_app
à la place de app
dans le fichier principal.
4.8 Téléverser un fichier (côté back end…)
Avec FlaskSimpleAuth,
l'accès aux fichiers téléversés se fait via les paramètres de type FileStorage
.
Cet objet a un attribut stream vers le contenu du fichier.
Il y a une méthode save
pour écrire le contenu quelque part.
# attention, cette route devrait être protégée !
@app.post("/upload", authorize="OPEN")
def post_upload(hi: fsa.FileStorage):
# usually: hi.save(some-clever-path)
hi_data = hi.stream.read()
return f"received '{hi.filename}' ({hi.content_type}): {hi_data}", 201
Ensuite pour tester avec curl
(ATTENTION bien mettre le @
!) :
curl -i -X POST -F "hi=@./hello.txt" http://0.0.0.0:5000/upload
# received 'hello.txt' (text/plain): b'Hello World!\n'
La taille maximale du fichier peut être ajustée dans la configuration avec MAX_CONTENT_LENGTH
.
💡 Voir la question 2.8 pour le front-end.
4.9 Données initiales et synchro des séquences
Si votre table utilise une clé primaire SERIAL et que vous insérez des données initiales (data.sql
) avec des ids
constants, attention il faut re-synchroniser la séquence pour que les insertions de l'application puissent l'utiliser :
4.10 Tester une requête directement
C'est prévu dans le Makefile
:
4.11 Ajouter des utilisateurs initiaux
Les comptes (login, password, isAdmin) sont définis dans le fichier
test_users.in
qui sert à fabriquer le fichier CSV test_users.csv
, importé
directement dans la table Auth
par data.sql
.
Cette dérivation permet de calculer les hash des mots de passe.
Pour ajouter des utilisateurs à l'initialisation, modifier le fichier
test_users.in
.
4.12 Manipuler des objets de JS à SQL
Côté front end, les données sont manipulées sous forme d'objets JavaScript. Ces objets peuvent être naturellement exportés en JSON et envoyer au back end dans une requête.
Côté back end, on peut déclarer de tels objets avec pydantic
ou dataclasses
,
qui seront importés automatiquement à partir du JSON reçu :
Ce type peut également être utilisé directement au niveau SQL par AnoDB :
-- name: add_personnage$
INSERT INTO Personnage(nom, age, sympa)
VALUES (:p.nom, :p.age, :p.sympa)
RETURNING pid;
On peut ensuite utiliser ce nouveau type directement pour les paramètres d'une requête et le transmettre à la base de données sans se soucier du détail des attributs :
@app.post("/personnage", authorize="AUTH")
def post_personnage(p: Personnage):
pid = db.add_personnage(p=p)
return ...
On peut même passer ça brutalement comme une chaîne de caractère plutôt qu'un objet JSON :
curl -si -X POST -u admin:admin \
-d 'p={"nom":"Mélusine","age":21,"sympa":true}' \
https://api.comics.net/personnage
Il est également possible de stocker directement du JSON dans
une base de données Postgres avec les types JSONB
et JSON
.
Le principal avantage est qu'il a moins de manipulations.
Le principal inconvénient est que la base de données n'assure plus,
ou plus difficilement, de contrôle sur la qualité des données
ainsi stockées.