Aller au contenu

Postgres Flask Logo

Back End

4.0 Je ne comprends rien !

Voici la procédure :

  1. Lire le cours back end en détail au moins 3 fois.

    ATTENTION cette tâche prend forcément plusieurs heures !

  2. Faire le TP back end pour de vrai.

    ATTENTION cette tâche prend forcément au moins une journée !

  3. RTFM chercher un peu dans les documentations pertinentes.
  4. 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.

-- name: get_one_thing^
SELECT * FROM Thing WHERE tid = :tid;
# renvoie le tuple ou None si vide
thing = db.get_one_thing(42)

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 $ avec RETURNING ...
  • 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 le RETURNING renvoie None dans ce cas.

4.3 Debug de l'application

Pour ajuster le niveau de verbosité des logs de l'application, ajouter dans local.conf :

APP_LOGGING_LEVEL = logging.DEBUG

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.

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 attribut GENERATED 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 dans local.conf et server.conf :
        "queries": [ "queries.sql", "autre.sql" ],
    
  • 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 :

-- Ajouter après les INSERT sur Foo
SELECT SETVAL('foo_fid_seq', MAX(fid)) FROM Foo;

4.10 Tester une requête directement

C'est prévu dans le Makefile :

make Q='db.version()' query

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 :

@dataclass
class Personnage:
    nom: str
    age: int
    sympa: bool

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.