Aller au contenu

Première base SQL

Objectif : lire une donnée en base de données avec du SQL visible, sans ORM.

Ce que vous allez apprendre : créer une table via une migration, puis la lire avec fetch_one(...) depuis un nouveau contrôleur dédié au domaine des messages.

WelcomeController couvre tous les paliers HTTP précédents, et mvc/routes/__init__.py déclare ses routes jusqu'à /welcome/validate.

Nous abordons un nouveau domaine, la base de données : selon le principe « nouveau domaine = nouveau contrôleur », nous créons un second contrôleur, MessageController.

Documentations

Pour bien comprendre ce palier :

Document Ce qu'il apporte
Migrations SQL comment créer une table et appliquer une migration
Prérequis : le cœur ne parle à aucune base

Les neuf paliers précédents n'ont touché aucune base, et le cœur de Forge n'en connaît aucune.
Trois installations et une commande sont nécessaires avant d'aller plus loin, une seule fois pour ce mini-projet.

pip install forge-mvc-sqlite
pip install forge-mvc-entities
forge db:config
Étape Ce qu'elle apporte
forge-mvc-sqlite le backend, un fichier local, sans serveur à installer ; le cœur est agnostique et n'en embarque aucun (ADR-054)
forge-mvc-entities le moteur qui porte db:init, migration:make et migration:apply (ADR-070)
forge db:config inscrit les variables du backend dans env/dev, env/prod et env/example (ADR-064)

Sans elles, la commande suivante refuse

forge db:init répond alors « module forge-mvc-entities non installé », et le palier s'arrête là.

Ces trois étapes manquaient à cette page : chacune des commandes était juste, et le manque n'existait qu'entre elles.

Migrations

Une fois les prérequis posés, initialisez la base de données :

forge db:init

forge db:init prépare la base et la table forge_migrations, qui enregistre les migrations déjà appliquées.
Sur un backend sans serveur comme SQLite, il crée directement le fichier.
Sur un backend serveur, il affiche le SQL de provisioning, base et comptes, à exécuter dans une session d'administration, ou l'exécute avec --run (ADR-067).
Sans cette étape, forge migration:apply n'a ni base ni table de suivi et échoue.

Générez ensuite le fichier de migration ; Forge l'horodate pour vous :

forge migration:make create_first_sql_messages

Forge crée mvc/migrations/<timestamp>_create_first_sql_messages.sql.
Ouvrez ce fichier et placez-y le SQL suivant :

CREATE TABLE IF NOT EXISTS first_sql_messages (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    content VARCHAR(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO first_sql_messages (content)
SELECT 'Bonjour SQL'
WHERE NOT EXISTS (SELECT 1 FROM first_sql_messages);

L'INSERT est idempotent : il n'ajoute le message « Bonjour SQL » que si la table est vide, donc rejouer la migration ne crée pas de doublon.
Appliquez enfin la migration avec forge migration:apply avant de tester /message.

Contrôleurs

Créez le fichier mvc/controllers/message_controller.py :

# mvc/controllers/message_controller.py
from core.database.db import fetch_one, insert
from core.http.request import Request
from core.http.response import Response
from core.mvc.controller.base_controller import BaseController

SELECT_FIRST_MESSAGE = "SELECT content FROM first_sql_messages ORDER BY id LIMIT 1"
INSERT_MESSAGE = "INSERT INTO first_sql_messages (content) VALUES (?)"


class MessageController(BaseController):

    @staticmethod
    def index(request: Request) -> Response:
        row = fetch_one(SELECT_FIRST_MESSAGE)
        message = row["content"] if row else "(aucun message)"
        return Response.text(f"Message depuis la base : {message}")
Élément Rôle
SELECT content FROM first_sql_messages ... Le SQL reste visible, lisible tel quel, sans couche d'abstraction.
fetch_one(...) Renvoie une seule ligne sous forme de dictionnaire, ou None si la table est vide ; d'où le repli (aucun message).
MessageController Un nouveau domaine justifie un nouveau contrôleur : il ne mélange pas la logique base de données avec les démonstrations HTTP de WelcomeController.

L'import insert et la constante INSERT_MESSAGE serviront au palier suivant ; ils sont déjà en place pour ne plus toucher aux imports.

Routes

Ajoutez l'import du contrôleur et la route /message dans mvc/routes/__init__.py :

# mvc/routes/__init__.py
from core.http.router import Router
from mvc.controllers.home_controller import HomeController
from mvc.controllers.welcome_controller import WelcomeController
from mvc.controllers.message_controller import MessageController

router = Router()

with router.group("", public=True) as public:
    public.add("GET", "/", HomeController.index, name="home-index")
    public.add("GET",  "/welcome", WelcomeController.index, name="welcome-index")
    public.add("GET",  "/welcome/hello", WelcomeController.hello, name="welcome-hello")
    public.add("GET",  "/welcome/html", WelcomeController.html, name="welcome-html")
    public.add("GET",  "/welcome/article/{id}", WelcomeController.article, name="welcome-article")
    public.add("GET",  "/welcome/debug", WelcomeController.debug, name="welcome-debug")
    public.add("GET",  "/welcome/json", WelcomeController.json_demo, name="welcome-json")
    public.add("GET",  "/welcome/csrf", WelcomeController.csrf, name="welcome-csrf")
    public.add("GET",  "/welcome/form", WelcomeController.form, name="welcome-form")
    public.add("POST", "/welcome/form-submit", WelcomeController.form_submit, name="welcome-form_submit")
    public.add("GET",  "/welcome/validate", WelcomeController.validate, name="welcome-validate")
    public.add("POST", "/welcome/validate-submit", WelcomeController.validate_submit, name="welcome-validate_submit")
    public.add("GET",  "/message", MessageController.index, name="message-index")
Tests
URL Résultat
https://localhost:8000/message Message depuis la base : Bonjour SQL
À retenir
  • La base s'initialise une fois avec forge db:init, puis chaque table naît d'une migration générée (forge migration:make), éditée, et appliquée (forge migration:apply).
  • fetch_one(...) lit une ligne avec du SQL écrit à la main.
  • Un nouveau domaine se loge dans son propre contrôleur.

Au palier suivant, nous écrivons à notre tour une ligne dans cette table.

Continuer avec Écrire en base