Architecture des entités Forge¶
Forge sépare la description d'une entité en trois niveaux distincts : la source canonique JSON, les projections techniques générées, et le code métier manuel. Cette séparation permet de régénérer les fichiers techniques sans jamais écraser le travail manuel.
1. Vue d'ensemble¶
flowchart TD
subgraph "mvc/entities/"
RJ["relations.json<br/>source canonique des relations"]
RS["relations.sql<br/>projection SQL des relations"]
subgraph "contact/"
CJ["contact.json<br/>source canonique"]
CS["contact.sql<br/>projection SQL"]
CB["contact_base.py<br/>base Python générée"]
CP["contact.py<br/>classe métier manuelle"]
CI["__init__.py<br/>export de Contact"]
end
end
CJ -->|"forge build:model"| CS
CJ -->|"forge build:model"| CB
RJ -->|"forge build:model"| RS
CB -.->|"hérite de"| CP
| Fichier | Nature | Régénérable | Règle |
|---|---|---|---|
contact.json |
Source canonique | Non | À modifier librement |
contact.sql |
Projection SQL | Oui | Ne pas modifier manuellement |
contact_base.py |
Base Python | Oui | Ne pas modifier manuellement |
contact.py |
Classe métier | Non | Jamais écrasé par Forge |
__init__.py |
Export | Non | Jamais écrasé par Forge |
relations.json |
Source relationnelle | Non | À modifier librement |
relations.sql |
Projection relationnelle | Oui | Ne pas modifier manuellement |
2. Le modèle canonique JSON¶
Format¶
{
"$schema": "../../schemas/entity.schema.json",
"schema_version": "1.0",
"name": "Contact",
"table": "contact",
"description": "Contacts de l'application",
"fields": [
{ "name": "nom", "type": "string", "max_length": 80 },
{ "name": "email", "type": "string", "max_length": 120, "nullable": true },
{ "name": "actif", "type": "boolean", "default": true }
]
}
La clé primaire Id est générée automatiquement par build:model — elle n'est pas déclarée dans fields[].
Clés racine¶
| Clé | Obligatoire | Valeur par défaut |
|---|---|---|
schema_version |
Oui | — (doit valoir "1.0") |
name |
Oui | — |
fields |
Oui | — |
table |
Non | name converti en snake_case |
description |
Non | "" |
Clés par champ¶
| Clé | Obligatoire | Valeur par défaut |
|---|---|---|
name |
Oui | — |
type |
Oui | — (valeurs : string, integer, boolean, date, datetime, text, password, decimal) |
max_length |
Non (string) | — |
nullable |
Non | false |
unique |
Non | false |
default |
Non | absent |
Contraintes disponibles dans constraints¶
| Clé | Types compatibles |
|---|---|
not_empty |
str |
min_length |
str |
max_length |
str |
min_value |
int, float |
max_value |
int, float |
pattern |
str (regex) |
Valeurs par défaut (default)¶
La clé default accepte uniquement des valeurs simples : str, int, float, bool, null.
- Absence de
default= aucune valeur par défaut default: nulln'est autorisé que sinullable: true- Pour les types
dateetdatetime,defaultest une chaîne ISO ("2024-01-01","2024-01-01T00:00:00") - Les expressions SQL complexes (
CURRENT_TIMESTAMP,NOW()) sont hors scope
3. Les projections générées¶
contact.sql — projection SQL locale¶
Contient uniquement la table de l'entité. Pas de clé étrangère.
CREATE TABLE IF NOT EXISTS contact (
Id INT NOT NULL AUTO_INCREMENT,
Nom VARCHAR(80) NOT NULL,
Email VARCHAR(120) NULL,
Actif BOOLEAN NOT NULL DEFAULT 1,
PRIMARY KEY (Id),
UNIQUE KEY uk_contact_email (Email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Règles de formatage :
- Mots-clés SQL en majuscules
- 4 espaces d'indentation, une colonne par ligne
- PRIMARY KEY et UNIQUE KEY en contraintes de table
- Toujours ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
Règle stricte
Les clés étrangères inter-entités n'apparaissent jamais dans un .sql d'entité.
Elles appartiennent exclusivement à relations.sql.
contact_base.py — base Python générée¶
Contient le constructeur, les propriétés avec décorateurs de validation, to_dict(), from_dict() et __repr__.
from core.validation import ValidationError, max_length, not_empty, nullable, typed
class ContactBase:
"""Classe de base régénérable de Contact."""
def __init__(self, nom, actif, email=None, id=None):
self.nom = nom
self.email = email
self.actif = actif
self.id = id
@property
def nom(self):
return self._nom
@nom.setter
@typed(str)
@not_empty
@max_length(80)
def nom(self, value):
if value is None:
raise ValidationError("nom", 'La propriété "nom" ne peut pas être nulle.')
self._nom = value
def to_dict(self) -> dict:
return {"id": self.id, "nom": self.nom, "email": self.email, "actif": self.actif}
Règle du constructeur : un champ devient paramètre obligatoire s'il est non nullable, sans valeur par défaut et non auto-increment.
Décorateurs de validation disponibles¶
| Décorateur | Source JSON |
|---|---|
@typed(type_) |
python_type |
@nullable |
nullable: true |
@not_empty |
constraints.not_empty |
@min_length(n) |
constraints.min_length |
@max_length(n) |
constraints.max_length |
@min_value(n) |
constraints.min_value |
@max_value(n) |
constraints.max_value |
@pattern(regex) |
constraints.pattern |
Types Python supportés : int, str, float, bool, date, datetime.
Règle nullable
@nullable est le seul décorateur qui autorise None.
Les autres décorateurs ne doivent pas échouer sur None.
@typed(int) refuse bool.
4. Les fichiers manuels¶
contact.py — classe métier¶
Hérite de ContactBase. Créé une seule fois par Forge s'il est absent. Jamais écrasé.
from .contact_base import ContactBase
class Contact(ContactBase):
"""Point d'extension métier pour Contact."""
pass
Ajouter ici les méthodes métier, les validations croisées et les surcharges spécifiques.
__init__.py¶
Créé une seule fois. Jamais écrasé.
5. Les relations¶
Structure globale¶
flowchart LR
subgraph "relations.json"
R1["relation: commande_client<br/>many_to_one<br/>Commande.client_id → Client.id"]
R2["relation: commande_produit<br/>many_to_one<br/>LigneCommande.produit_id → Produit.id"]
end
subgraph "relations.sql"
A1["ALTER TABLE commande<br/>ADD CONSTRAINT fk_commande_client..."]
A2["ALTER TABLE ligne_commande<br/>ADD CONSTRAINT fk_lc_produit..."]
end
R1 -->|"forge build:model"| A1
R2 -->|"forge build:model"| A2
Format relations.json¶
{
"$schema": "../../schemas/relations.schema.json",
"schema_version": "1.0",
"relations": [
{
"type": "many_to_one",
"from": "Commande",
"to": "Client",
"name": "client",
"foreign_key": "client_id",
"nullable": false,
"on_delete": "restrict"
}
]
}
Règles :
- many_to_one et many_to_many sont les types supportés
- from est l'entité source, to est l'entité cible
- foreign_key est le nom du champ FK dans l'entité source
- on_delete est en minuscules : cascade, set_null, restrict, no_action
Format relations.sql¶
ALTER TABLE commande
ADD CONSTRAINT fk_commande_client
FOREIGN KEY (ClientId)
REFERENCES client (Id)
ON DELETE RESTRICT
ON UPDATE CASCADE;
Règle stricte
relations.sql ne doit contenir que des ALTER TABLE ... ADD CONSTRAINT.
Aucun CREATE TABLE dans relations.sql.
Relations many-to-many¶
many_to_many est un type de relation supporté dans relations.json. sync:relations génère la table pivot ; make:crud génère le formulaire côté source et l'affichage des libellés dans list/show. Voir docs/relations.md pour la référence complète.
Un lien many-to-many peut aussi se modéliser avec une entité pivot normale et deux relations many_to_one, utile quand la table pivot a ses propres champs ou son propre CRUD :
flowchart LR
CG["ContactGroupe<br/>id, contact_id, groupe_id"]
C["Contact"]
G["Groupe"]
CG -->|"many_to_one<br/>contact_id → id"| C
CG -->|"many_to_one<br/>groupe_id → id"| G
JSON de l'entité pivot (format canonique) :
{
"schema_version": "1.0",
"name": "ContactGroupe",
"table": "contact_groupe",
"fields": [
{ "name": "contact_id", "type": "integer" },
{ "name": "groupe_id", "type": "integer" }
]
}
La clé id et les sql_type ne sont pas déclarés dans le JSON canonique — Forge les dérive lors de build:model.
Relations associées dans relations.json :
{
"$schema": "../../schemas/relations.schema.json",
"schema_version": "1.0",
"relations": [
{
"type": "many_to_one",
"from": "ContactGroupe", "to": "Contact",
"name": "contact",
"foreign_key": "contact_id",
"nullable": false, "on_delete": "cascade"
},
{
"type": "many_to_one",
"from": "ContactGroupe", "to": "Groupe",
"name": "groupe",
"foreign_key": "groupe_id",
"nullable": false, "on_delete": "cascade"
}
]
}
6. Conventions de nommage¶
| Élément | Convention | Exemple |
|---|---|---|
| Dossier d'entité | snake_case |
contact_client/ |
Nom de table (table) |
snake_case |
contact_client |
Nom de classe (entity) |
PascalCase |
ContactClient |
Nom de champ Python (name) |
snake_case |
date_creation |
Nom de colonne SQL (column) |
PascalCase |
DateCreation |
| Nom de relation | snake_case |
commande_client |
| Nom de contrainte FK | fk_<relation> |
fk_commande_client |
7. Cycle de génération¶
flowchart TD
A["forge make:entity Contact"] --> B["contact.json créé"]
B --> C["Éditer contact.json"]
C --> D["forge check:model"]
D --> E{"Validation OK ?"}
E -->|Non| C
E -->|Oui| F["forge build:model --dry-run"]
F --> G["forge build:model"]
G --> H["contact.sql + contact_base.py générés"]
H --> I["forge make:relation (si besoin)"]
I --> J["forge build:model"]
J --> K["relations.sql généré"]
K --> L["forge db:apply"]
L --> M[("MariaDB")]
Commandes et comportement¶
| Commande | Écrit | Préserve | Rôle |
|---|---|---|---|
forge make:entity Contact |
contact.json, contact.sql, contact_base.py, contact.py, __init__.py |
fichiers existants | Création initiale |
forge sync:entity Contact |
contact.sql, contact_base.py |
contact.py, __init__.py |
Resynchronisation d'une entité |
forge make:relation |
relations.json |
existant | Ajout interactif de relation |
forge sync:relations |
relations.sql |
— | Resynchronisation des relations |
forge build:model |
tout le modèle | fichiers manuels | Régénération complète |
forge check:model |
rien | — | Validation sans écriture |
Ordre d'exécution SQL obligatoire¶
Jamais l'inverse : relations.sql référence des tables qui doivent exister.
8. Validation interne¶
Forge bloque toute génération si une validation échoue.
Validation d'entité¶
- Structure obligatoire présente (
entity,fields) - Noms valides (format, unicité des champs et colonnes)
- Une seule clé primaire par entité
- Compatibilité
python_type/sql_type - Compatibilité contraintes / type
- Valeurs par défaut cohérentes avec la nullabilité
Validation des relations¶
- Entités et champs référencés existants
- Type de relation valide (
many_to_oneactuellement) - Champ cible est une clé primaire
- Types compatibles entre
from_fieldetto_field - Unicité des noms de relation et de contrainte FK
Validation globale¶
- Unicité des noms d'entité et de table
- Cohérence dossier / nom d'entité (
ContactClient→ dossiercontact_client) - Toutes les tables sont en
snake_case
9. Limites actuelles¶
Retenu¶
- Une clé primaire simple par entité
- Source canonique JSON locale par entité
- Relations
many_to_oneetmany_to_many(SQL pivot généré, formulaire côté source) - Décorateurs de validation simples
- SQL MariaDB / InnoDB
Hors scope¶
- Clés primaires composites
one_to_onedédié- Hooks de cycle de vie
- Navigation objet automatique (ORM)
- Génération de repository
- Contraintes conditionnelles entre champs
- Expressions SQL complexes dans
default - Contraintes d'unicité composées dans le JSON (à porter dans un script SQL séparé)