Capability Repository¶
Intention¶
La capability outbound repository persiste les entités métier et leur cycle
de vie derrière le port commun Repository[T]. Le domaine et les use cases ne
dépendent ni de MongoDB, ni d'un moteur SQL, ni d'un format de fichier.
Utiliser un repository pour la source de vérité CRUD d'une entité. Ne pas l'utiliser pour stocker directement un fichier volumineux, calculer un embedding ou maintenir un index vectoriel reconstruisible.
Position Hexagonale¶
| Text Only | |
|---|---|
Chaque port correspond à une responsabilité différente :
| Besoin | Capability | Responsabilité |
|---|---|---|
| conserver une entité métier et son cycle de vie | repository |
source de vérité CRUD |
| conserver un fichier ou un blob original | storage |
octets et métadonnées objet |
| retrouver les voisins d'un vecteur | vector-store |
index de recherche reconstruisible |
Pourquoi SQL Et NoSQL Restent Une Même Capability¶
SQL et NoSQL décrivent ici des familles d'implémentation et de garanties, pas
deux responsabilités métier. Repository[T] reste donc le port commun tant que
le contrat est centré sur les entités et le CRUD.
Créer maintenant sql-repository et nosql-repository dupliquerait le même
port sans donner de capacité supplémentaire aux use cases. Un port relationnel
spécifique ne deviendrait pertinent que si Arclith devait exposer explicitement
des transactions applicatives, des contraintes uniques, des requêtes typées,
des relations ou joins, des migrations, ou des schémas structurés par champ.
storage reste séparé parce qu'il conserve les blobs originaux.
vector-store reste séparé parce qu'il conserve un index ou une projection de
recherche, et non la source de vérité métier par défaut.
Quickstart¶
Afficher le catalogue lisible par une personne ou exploitable par un outil :
Ajouter un adapter à un projet :
L'adapter global reste choisi par la configuration existante :
Il sert aussi de fallback pour toute entité qui n'a pas de binding explicite. Un service mono-store n'a donc rien à modifier.
Les facets sont uniquement des métadonnées de choix dans le catalogue CLI. Elles ne changent ni ce fichier, ni le contrat du port, ni le comportement des projets existants.
Voir aussi le quickstart historique pour le wizard complet.
Formation¶
Le projet Todo montre le chemin complet :
- initialiser le projet avec
repository: memory; - faire dépendre les use cases du port ;
- passer à MongoDB pour partager l'état entre processus.
Contrat¶
Repository[T] fournit le même contrat quel que soit l'adapter :
| Méthode | Rôle |
|---|---|
create(entity) |
persister une nouvelle entité |
read(uuid) |
lire une entité, supprimée logiquement ou non |
update(entity) |
remplacer l'état persistant de l'entité |
delete(uuid) |
supprimer physiquement une entité |
find_all() |
lister les entités non supprimées |
find_page(offset, limit) |
paginer les entités non supprimées avec leur total |
find_deleted() |
lister les entités supprimées logiquement |
duplicate(uuid) |
créer une copie avec un nouvel UUID |
Le choix de l'adapter passe par build_repository() et Arclith.repository().
Les adapters inbound ne doivent jamais instancier une implémentation concrète.
Router Des Entités Vers Plusieurs Stores¶
Un même service peut conserver le fallback global tout en sélectionnant un
adapter pour certaines classes d'entité. La clé recommandée est toujours le
chemin Python complet <module>.<qualname> :
| YAML | |
|---|---|
Les sections configurant ces deux adapters restent dans leurs fichiers scoped :
| YAML | |
|---|---|
| YAML | |
|---|---|
L'assemblage public ne change pas :
| Python | |
|---|---|
Le routing est exact et déterministe. Arclith construit la clé avec
f"{entity_class.__module__}.{entity_class.__qualname__}". Il n'interprète pas
les noms courts, ce qui évite les collisions entre deux classes portant le même
nom. Si la classe n'est pas bindée, repository reste le fallback. Si elle est
bindée vers un adapter absent du registry utilisé, la construction échoue au
lieu de revenir silencieusement au fallback.
Les adapters intégrés mongodb, duckdb, mariadb et postgresql exigent
leur section de configuration dès qu'ils sont référencés par le fallback ou un
binding. Un nom custom reste possible avec un RepositoryRegistry applicatif ;
sa présence dans ce registry est contrôlée lors de la construction.
Les use cases continuent à dépendre uniquement des ports :
| Python | |
|---|---|
Le container connaît les classes et assemble les repositories. Le use case sait qu'il coordonne deux ports, mais ne connaît ni MongoDB ni MariaDB.
Contrat JSON Des Facets¶
Chaque adapter repository expose un objet facets dans
arclith-cli capabilities --json :
| JSON | |
|---|---|
La forme est stable et entièrement sérialisable :
| Facet | Valeurs | Lecture |
|---|---|---|
storage_model |
memory, document, relational_json, embedded_analytics, relational_structured |
famille de persistance |
runtime |
in_process, file, server |
emplacement et durée de vie du moteur |
production_ready |
booléen | adapter destiné ou non à un runtime de production |
multi_process |
booléen | état partageable entre plusieurs processus applicatifs |
transactions |
none, limited, strong |
niveau transactionnel réellement accessible via l'adapter |
schema_strategy |
flexible, json_table, structured_tables |
façon dont les entités sont matérialisées |
recommended_for |
liste de chaînes | usages conseillés |
limits |
liste de chaînes | compromis à vérifier avant sélection |
strong signifie que chaque opération repository s'exécute dans une
transaction forte du moteur. Le port ne fournit toutefois pas d'unité de
travail permettant de regrouper plusieurs appels dans une même transaction.
limited couvre une atomicité limitée à l'opération ou aux garanties propres
au moteur, sans transaction applicative multi-opérations exposée.
Les autres capabilities peuvent laisser facets à null. Cela permet
d'étendre progressivement le catalogue sans imposer artificiellement ces axes
de persistance aux adapters inbound ou runtime.
Adapters¶
Matrice De Choix¶
| Adapter | Famille et runtime | Usage recommandé | Limite principale |
|---|---|---|---|
memory |
mémoire / processus | tests, smoke, développement local mono-processus | volatile et non partagé entre processus |
mongodb |
document / serveur | état métier partagé, modèle document-first ou évolutif | pas de modèle relationnel riche via le port |
duckdb |
analytique embarqué / fichier | démonstrations et traitements analytiques locaux | pas une base applicative serveur multi-processus |
mariadb |
relationnel avec payload JSON / serveur | intégration à un SI MariaDB existant | entité stockée en JSON, sans mapping relationnel métier riche |
postgresql |
JSONB par défaut, colonnes typées opt-in / serveur | SQL serveur robuste, contraintes et index ciblés | mapper et migrations structurées restent à la charge de l'application |
Matrice Des Garanties¶
| Adapter | Production ready | Multi-processus | Transactions | Stratégie de schéma |
|---|---|---|---|---|
memory |
non | non | none |
flexible |
mongodb |
oui | oui | limited |
flexible |
duckdb |
non | non | limited |
structured_tables |
mariadb |
oui | oui | strong |
json_table |
postgresql |
oui | oui | strong |
json_table par défaut, structured_tables opt-in |
production_ready décrit la cible de l'adapter Arclith, pas une certification
automatique du déploiement. La haute disponibilité, les sauvegardes, la
réplication, le chiffrement et les tests de charge restent à valider dans
l'infrastructure du service.
La facet PostgreSQL décrit le chemin par défaut generic_json. Une application
peut sélectionner structured pour une entité sans changer le port
Repository[T] ; le catalogue ne prétend pas qu'un unique schéma physique
s'applique à toutes les configurations possibles.
Memory¶
memory n'a pas de fichier scoped. Il est le choix par défaut pour les tests
rapides, mais chaque processus possède son propre état et toute donnée est
perdue à son arrêt.
MongoDB¶
| YAML | |
|---|---|
uri: null signifie que l'URI doit venir de config/secrets.yaml, de
l'environnement ou de Vault. MongoDB convient à un état partagé document-first
et à un schéma évolutif. Le port n'expose ni jointure relationnelle, ni
transaction regroupant plusieurs appels repository.
DuckDB¶
L'adapter matérialise l'entité dans une table DuckDB puis la persiste dans un fichier supporté. Il cible les démonstrations ou traitements analytiques locaux. Ne pas le choisir comme base applicative serveur partagée par défaut.
MariaDB¶
| YAML | |
|---|---|
Chaque entité est stockée dans une table générique avec les champs techniques et un payload JSON. Cet adapter réutilise les transactions du moteur pour chaque opération, sans exposer de modèle relationnel métier ni d'unité de travail multi-opérations.
PostgreSQL¶
| YAML | |
|---|---|
PostgreSQL suit le même port avec une table générique par type et un payload JSONB par défaut. L'adapter cible un serveur SQL durable et multi-processus. Une application peut opter explicitement pour des colonnes typées avec le mécanisme décrit ci-dessous. Les relations automatiques, les joins, une query DSL et la gestion des migrations restent hors du contrat Arclith.
Mapping Relationnel Structuré Optionnel¶
Quand L'utiliser¶
Conserver mapping_strategy: generic_json tant qu'une entité est naturellement
document-first et que les champs techniques suffisent pour les opérations du
repository. C'est le comportement backward-compatible et le chemin recommandé
pour démarrer.
Choisir structured seulement lorsqu'un besoin vérifié exige une représentation
relationnelle : contrainte UNIQUE, index métier, reporting SQL sur des colonnes
typées, compatibilité avec une table existante ou optimisation d'une requête
ciblée. Le mapper appartient alors à l'application et à son assemblage
d'infrastructure ; l'entité métier n'importe ni SQLAlchemy ni PostgreSQL.
Cette première itération exécute le mapping structuré uniquement dans l'adapter
postgresql. MariaDB conserve son stockage JSON générique. Sa configuration
n'accepte pas mapping_strategy: structured tant qu'un support équivalent n'a
pas été implémenté et testé.
Déclarer Un Mapper¶
Le contrat public est pur Python et peut être importé directement depuis
arclith :
Les kinds supportés sont uuid, string, integer, float, boolean,
date, datetime et json. PostgreSQL matérialise json en JSONB et les
dates/heures avec un type timezone-aware.
Pour préserver les opérations de Repository[T], chaque mapper déclare les
colonnes uuid, created_at, updated_at, deleted_at et version avec les
kinds correspondants. uuid est l'unique clé primaire et deleted_at est
nullable. Les noms de table, colonne et index sont des identifiants SQL sûrs ;
les doublons et les index pointant vers une colonne inconnue sont refusés dès
l'enregistrement. Le préfixe de colonne _arclith_ est réservé aux valeurs
internes de l'adapter, notamment au total de pagination, et ne peut pas être
utilisé par un mapper applicatif.
to_record() doit retourner exactement les colonnes déclarées et le même UUID
que l'entité. from_record() doit restituer la classe d'entité attendue. Le
mapper doit inclure tous les champs applicatifs ou d'audit que le service veut
préserver ; Arclith ne génère pas implicitement les colonnes métier depuis le
modèle Pydantic.
Enregistrer Et Câbler Le Mapper¶
Le registre utilise l'identité exacte de la classe, sans import magique ni nom court :
| Python | |
|---|---|
La configuration PostgreSQL associée sélectionne la stratégie :
| YAML | |
|---|---|
Avec ce préfixe, le mapper ci-dessus cible la table
public.identity_user_accounts. Si structured est sélectionné sans mapper
enregistré pour UserAccount, la construction du repository échoue avec le
chemin Python complet de la classe. Les autres entités peuvent continuer à
utiliser le fallback ou un adapter distinct via repository_bindings.
mapper_registry s'applique au registry repository intégré. Une application
qui fournit déjà son propre RepositoryRegistry peut partir de
default_repository_registry(EntityClass, mapper_registry=...), y enregistrer
ses factories supplémentaires, puis le passer comme registry. Cela garde une
source de wiring unique et évite deux registries concurrents dans le même appel.
Création De Schéma Et Migrations¶
auto_create_schema: true conserve le comportement PostgreSQL historique :
SQLAlchemy exécute uniquement CREATE SCHEMA IF NOT EXISTS si nécessaire, puis
create_all() pour les tables absentes. Ce mode est pratique en développement
et dans les tests ; il ne modifie ni ne supprime une colonne existante.
En production, utiliser auto_create_schema: false et appliquer les migrations
avec l'outil choisi par l'application avant le démarrage. Dans ce mode, Arclith
n'exécute aucune création implicite. Toute modification de columns,
indexes, unique, nullable, table_name, schema ou table_prefix peut
nécessiter une migration explicite.
Arclith ne génère pas de révision Alembic, n'exécute pas d'ALTER TABLE, ne
compare pas le mapper au schéma vivant et ne promet aucune migration destructive
automatique. Le mapper décrit la forme attendue par l'adapter ; l'application
reste propriétaire du cycle de vie du schéma.
Requêtes Métier Et Relations¶
Le mapping structuré n'ajoute aucune méthode à Repository[T]. Un besoin tel
que find_by_email() doit rester un port métier explicite implémenté par
l'application. De même, les relations, le lazy loading et les joins automatiques
ne font pas partie du mécanisme. Pour les agrégats et stores multiples, les
limites transactionnelles décrites plus bas restent inchangées.
Production¶
Choisir l'adapter à partir des garanties nécessaires :
- rester sur
memorysi le processus peut perdre son état et ne le partage avec aucun autre runtime ; - choisir
duckdbsi le fichier local et l'usage analytique embarqué sont des contraintes explicites ; - choisir
mongodbpour un modèle document-first partagé et évolutif ; - choisir
mariadblorsqu'un SI MariaDB existe déjà ; - choisir
postgresqlpour un backend SQL serveur général avec payload JSONB ; - activer son mapping
structuredseulement si des colonnes, contraintes ou index métier apportent une valeur mesurable.
Les URI et mots de passe passent par la capability secrets, par
exemple MONGODB_URI, MARIADB_URL, MARIADB_PASSWORD, POSTGRESQL_URL ou
POSTGRESQL_PASSWORD. Ne pas les écrire en clair dans les fichiers versionnés.
Pour plusieurs réplicas API, MCP ou agent, sélectionner un adapter
multi_process: true. Cette facet ne remplace pas la validation de la capacité
du moteur, de son pool de connexions et de son déploiement à absorber la charge.
Limites Transactionnelles Cross-store¶
Le routing ne crée aucune transaction atomique entre deux bases :
- placer dans le même aggregate et le même store les entités qui doivent respecter un invariant fort dans une mutation atomique ;
- coordonner des aggregates distincts dans la couche applicative ;
- utiliser outbox, command bus, idempotence et compensation quand une opération distribuée doit tolérer les échecs partiels ;
- construire un query service, une projection ou un read model pour une lecture
combinée, sans cacher un join cross-store derrière
Repository[T].
Le routing reste limité au bounded context du service. Traverser le domaine d'un autre service passe par son API, son MCP, ses événements ou son command bus, jamais par l'accès direct à son repository ou à sa base.
Multitenant¶
Les bindings ne remplacent pas l'isolation tenant. Si le fallback ou au moins
un adapter bindé porte multitenant: true, le pipeline tenant est activé pour
le service. Les adapters tenant-aware tels que MongoDB, MariaDB et PostgreSQL
continuent à lire leurs coordonnées dans le contexte tenant existant. Les
bindings eux-mêmes ne contiennent ni URI ni credential.
Validation¶
Vérifier le routing, la configuration, le contrat des facets et la sortie CLI :
| Bash | |
|---|---|
Avant contribution :
Troubleshooting¶
Deux Processus Ne Voient Pas Le Même État¶
Vérifier multi_process dans la sortie JSON. memory et duckdb ne sont pas
les choix par défaut pour partager l'état entre API, MCP, agents ou workers.
Une Facet Vaut null¶
Les facets de cette matrice sont renseignées pour les adapters repository.
Une autre capability peut retourner null tant qu'elle ne dispose pas d'une
taxonomie adaptée à sa responsabilité.
Un Backend SQL N'Expose Pas De Joins Métier¶
mariadb stocke un payload JSON générique ; postgresql utilise JSONB par
défaut et accepte un mapper structuré explicite. Une application qui exige des
requêtes relationnelles typées, des contraintes métier ou des transactions
multi-agrégats doit définir un port applicatif dédié dans son propre domaine.
Le mapping structuré PostgreSQL rend les colonnes disponibles à cet adapter
applicatif, mais n'élargit pas le port générique.
Le Mapping Structuré Refuse De Démarrer¶
Vérifier que la configuration PostgreSQL contient
mapping_strategy: structured, que le RelationalMapperRegistry enregistre
exactement la classe passée à arclith.repository(), et que ce registre est
fourni via mapper_registry. Le message indique le chemin complet de l'entité
sans mapper. Si la table n'existe pas et que auto_create_schema vaut false,
appliquer la migration applicative avant de relancer le service.
Un Binding Est Refusé¶
Vérifier d'abord que la clé correspond exactement au module et au qualname de
la classe. Pour un adapter intégré, vérifier ensuite que son fichier scoped est
présent. Pour un adapter custom, enregistrer le même nom dans le
RepositoryRegistry passé à arclith.repository(...). Le message d'erreur de
construction indique l'entité, l'adapter sélectionné et les adapters disponibles.
Projet¶
Le projet Todo utilise memory dans les tests
et MongoDB pour partager les données entre les processus API, MCP et agent. Le
chapitre services locaux montre
le passage de l'un à l'autre sans modifier les use cases.
Suite¶
Lire storage pour les fichiers, vector-store
pour les projections de recherche, puis secrets pour les
credentials des adapters serveur.