UE5 : Fondamentaux du développement web frontal, niveau 2

Séance 4 : TD : Intercations avec les données avec un ORM

L'objectif de ce TD est de réviser l'écriture d'un modèle de données avec la représentation des tables en objets SQLAlchemy, de découvrir le requêtage de la base via l'ORM, puis d'écrire des données et enfin voir comment paginer les résultats. On s'intéressera plus ici à SQLAlchemy qu'aux templates ou aux routes: routes et templates seront donc très simples, ce qui importera sera d'utiliser l'ORM pour interagir avec les données.

Installations et configurations

Afin que tout le monde réalise ce TD sur une application Flask sans erreurs et correctement configurée:

  1. Installer sur son PC l'application Seance4__base_TD
  2. Y installer un environnement virtuel Python3
  3. Installer les dépendances de l'application
  4. Télécharger la base de données factbook.sqlite quelque part sur son PC
  5. Remplir la variable SQLALCHEMY_DATABASE_URI dans le fichier .env

Terminer l'écriture du modèle de données

Pour rappel, le modèle de données du factbook est le suivant : h:300px

Il manque, dans le module factbook des models, la représentation de deux tables physiquement présentes:

Lire les données avec l'ORM

Construire un template commun

Afin de ne plus se préoccuper des templates par la suite, il vaut mieux en créer un générique rapidement. Toutes les routes écrites par la suite appeleront ce template en lui donnant des données différentes en paramètre.

Ecrire le template pages/generique.html selon les critères suivants:

Ainsi, les futures routes seront par exemple:

@app.route("/uneroute")
def uneroute():
    resultats = # mettre ici la requête ORM de récupération des données
    return render_template("pages/generique.html", donnees=resultats)

Quelques requêtes simples

Nous avons déjà vu en Séance 3 l'utilisation de Country.query.all() qui permet de ramener tous les résultats de la table Country. Grâce à la documentation, écrire les routes suivantes:

Filtrer les données

filter() est un des membres de query. Il permet de filtrer les données de la table relationnelle. Afin d'avoir les potentiels résultats en retour, il est nécessaire d'appeler all() ou first() à la suite. filter() prend autant de paramètres qu'il y a de conditions. Une condition s'exprime ainsi:

query.filter(Classe_du_modele.champ operateur_de_comparaison Valeur)

Ecrire les routes suivantes:

Aggrégations SQL

Les fonctions d'aggrégation telles que COUNT, GROUP BY, HAVING et ORDER BY sont essentielles en SQL et sont reprises avec SQLAlchemy.

Ecrire la route suivante:

Jointures naturelles

Cette partie du TD reprend ce qui est dit dans le cours. Essayer de l'implémenter dans l'application.

Cette méthode est à privilègier dès lors que les jointures sont simples puisqu'elles sont incluses dans la déclaration des classes du modèle de base de données.

Lors de la création des classes dans models/factbook.py, nous avons créé des champs qui ne sont pas physiquement dans les tables grâce à db.relationship(). Pour la classe Country(), c'est notamment le cas de resources. Dès lors qu'une relation est indiquée dans une classe, l'objet sur lequel pointe la relation est accessible depuis notre objet requêté. Prenons l'exemple des ressources des pays:

Les objets de la Resources liés aux objets Country via la table country_resources sont alors accessibles depuis les objets Country sans avoir à le spécifier dans la requête SQLAlchemy.

Avec SQLAlchemy, il suffit donc de faire la requête suivante:

Country.query.all()

Tout s'effectue ensuite dans la manière d'appeler les attributs de classes; par exemple, pour obtenir le nom de la ressource, il faut écrire : country.resources.name (où country est un des objets de la liste de résultats).

Créer une route /ressources qui affiche les ressources d'un pays donné en paramètre (le template n'est pas à créer, il est déjà fourni dans pages/):

@app.route("/ressources/<string:nom>")
def ressources(nom):
    ressources = []

    query =  Country.query
    ressources = query.filter(Country.name == nom).first()

    return render_template("pages/pays_ressources.html", pays=nom, ressources=ressources, sous_titre=nom)

Puisqu'un seul pays est indiqué en paramètre et passé en filtre, il est suffisant de faire un first() dans la requête.

Accéder à cette route n'est pas simple pour l'utilisateur (il doit taper l'URL /ressources/France par exemple). Dans le template pages/generique.html, ajouter, sur chaque nom dans la colonne "nom", un lien vers la route "/ressources/string:nom".

Jointures forcées

S'il y a le temps, creuser les jointures avec le cours sur les jointures spécifiées manuellement : Seance4_requetes.html#join et /Seance4_requetes.html#union

Ecrire sur la base de données

Pagination des résultats

# .env

...
PAYS_PER_PAGE=10
# config.py Config()

...
    PAYS_PER_PAGE = int(os.environ.get("PAYS_PER_PAGE"))

Il est ensuite temps de modifier la route en ne renvoyant pas un ensemble de résultats au template via all(), mais en renvoyant uniquement l'objet Pagination résultant de paginate().

# routes/generales.py 

...
@app.route("/pays_pagination/<int:page>")
def pays_pagination(page):
    donnees = []

    query =  Country.query
    tous_resultats = query.paginate(page=page, per_page=app.config["PAYS_PER_PAGE"])
    return render_template("pages/pays_pagination.html", donnees=tous_resultats, sous_titre="Tous les pays")

Une fois l'objet Pagination (tous_resultats dans notre cas) transmis au template, tout se fait dans ce dernier.

<!-- templates/pages/pays_pagination.html-->

{%if pagination%}
    <table class="table">
        <thead>
            <tr>
                <th scope="col">#</th>
                <th scope="col">Nom</th>
                <th scope="col">Type</th>
                <th scope="col">Description</th>
            </tr>
        </thead>
        <tbody>
            {%for pays in pagination.items%}
            <tr>
                <th scope="row">{{loop.index}}</th>
                <td>{{pays.name}}</td>
                <td>{{pays.type}}</td>
                <td>{{pays.Introduction}}</td>
            </tr>
            {%endfor%}
        </tbody>
    </table>
{%endif%}

Afin d'afficher une barre de navigation entre les pages, paginate nous aide à nouveau.

<!--templates/pages/pays_pagination.html -->
...
<div class="container">
    <nav aria-label="research-pagination">
        <ul class="pagination">
            {%- for page in pagination.iter_pages() %}
                {% if page %}
        
                {% if page != pagination.page %}
                    <li class="page-item">
                        <a class="page-link" href="{{ url_for("pays", page=page+1) }}">{{page}}</a>
                    </li>
                {% else %}
                    <li class="page-item active disabled">
                    <a class="page-link">{{page}} <span class="sr-only">(actuelle)</span></a>
                    </li>
                {% endif %}
        
                {% else %}
                    <li class="page-item disabled">
                    <a class="page-link">...</a>
                    </li>
                {% endif %}
        
            {%- endfor %}
        </ul>
    </nav>
</div>
...