Le pattern d'identifiant partagé qui facilite les architectures hybrides
Beaucoup de projets modernes utilisent MongoDB pour stocker des documents flexibles (profils, logs, contenu) et PostgreSQL pour les données transactionnelles (commandes, factures, relations). Mais comment relier un utilisateur stocké dans MongoDB à ses commandes enregistrées dans PostgreSQL ? La solution la plus propre consiste à utiliser un UUID unique partagé entre les deux bases.
Dans cet article, je vous montre concrètement comment implémenter ce pattern avec Mongoose (ODM MongoDB) et Prisma (ORM PostgreSQL), du code prêt à copier, et comment gérer la cohérence des écritures cross‑database.
Pourquoi un UUID partagé plutôt qu'un ObjectID ?
L’ObjectID de MongoDB est un identifiant 12‑octets qui intègre un timestamp, une valeur aléatoire et un compteur. Il est efficace dans un cluster MongoDB, mais il devient problématique dès qu’on en sort :
Il n’est pas directement compatible avec un type UUID natif dans PostgreSQL.
Si vous l’exposez dans une API REST, il fuit de l’information (date de création, séquence).
Le stocker dans une colonne VARCHAR(24) de PostgreSQL gaspille de l’espace et pénalise les jointures.
Un UUID v4 ou v7 résout tous ces problèmes : il est universel, non‑énumérable, et supporté nativement par PostgreSQL (UUID) comme par MongoDB (stocké en String ou en BinData). Surtout, il peut être généré côté application, avant toute insertion, ce qui en fait le candidat idéal pour lier deux bases différentes.
Architecture cible
Imaginons une application avec :
MongoDB : collection users qui contient le profil détaillé (préférences, historique de navigation…), stocké sous forme de document flexible.
PostgreSQL : table users pour les informations critiques (email, mot de passe haché) et les relations (commandes, adresses).
Le lien entre les deux est un champ uuid commun, généré par l’application.
┌──────────────────────────┐ ┌──────────────────────────┐
│ MongoDB │ │ PostgreSQL │
│ │ │ │
│ users │ │ users │
│ ├── _id (ObjectID) │ │ ├── id (UUID, PK) │
│ ├── uuid (String, idx) │◄────┼──┤ (même valeur) │
│ ├── profile (Object) │ │ ├── email (VARCHAR) │
│ └── ... │ │ └── created_at │
└──────────────────────────┘ └──────────────────────────┘
Côté MongoDB avec Mongoose
On définit un schéma qui conserve l’_id ObjectID (pour les performances internes) mais ajoute un champ uuid indexé. Ce champ servira de clé publique et de lien avec PostgreSQL.
// models/mongo/User.js
import mongoose from 'mongoose';
const userSchema = new mongoose.Schema({
uuid: {
type: String,
required: true,
unique: true,
index: true,
default: () => crypto.randomUUID() // UUID v4 natif dans Node 19+
},
email: { type: String, required: true },
profile: { type: mongoose.Schema.Types.Mixed } // données flexibles
});
export const MongoUser = mongoose.model('User', userSchema);
Astuce : si vous préférez remplacer complètement l’_id par l’UUID, utilisez _id: { type: String, default: () => crypto.randomUUID() }. Cependant, garder un ObjectID interne est souvent plus performant pour les requêtes MongoDB (index plus compact).
- Côté PostgreSQL avec Prisma Dans le schéma Prisma, le modèle User utilise un champ id de type UUID, avec @default(uuid()) pour générer automatiquement un UUID v4 côté base de données. Mais pour notre pattern, nous allons plutôt fournir l’UUID depuis l’application afin qu’il soit identique à celui de MongoDB.
// prisma/schema.prisma
model User {
id String @id @default(uuid()) // UUID v4
email String @unique
createdAt DateTime @default(now()) @map("created_at")
orders Order[]
@@map("users")
}
model Order {
id Int @id @default(autoincrement())
userId String
total Float
user User @relation(fields: [userId], references: [id])
}
Notez que l’id est un String car Prisma ne gère pas nativement le type UUID PostgreSQL ; il le traite comme un String avec le décorateur @db.Uuid. Pour plus de clarté, nous utilisons @default(uuid()) mais dans notre code d’insertion, nous passerons l’UUID explicitement.
- Code Node.js complet : création d’un utilisateur dans les deux bases Voici la fonction createUser qui insère dans MongoDB et PostgreSQL avec le même UUID, en gérant les erreurs.
// services/userService.js
import { PrismaClient } from '@prisma/client';
import { MongoUser } from '../models/mongo/User.js';
import crypto from 'crypto';
const prisma = new PrismaClient();
export async function createUser({ email, profile }) {
// 1. Générer l'UUID commun (peut aussi être fait dans les modèles)
const uuid = crypto.randomUUID();
// 2. Insérer dans PostgreSQL
const pgUser = await prisma.user.create({
data: {
id: uuid, // on écrase l'auto‑génération
email
}
});
try {
// 3. Insérer dans MongoDB
const mongoUser = await MongoUser.create({
uuid,
email,
profile
});
return { pgUser, mongoUser };
} catch (mongoError) {
// Si MongoDB échoue, on annule l'insertion PostgreSQL
await prisma.user.delete({ where: { id: uuid } });
throw new Error('Échec de l’insertion MongoDB, PostgreSQL rollback effectué');
}
}
Explication de la gestion d’erreur : il n’existe pas de transaction distribuée native entre MongoDB et PostgreSQL. Ici, on utilise une compensation manuelle : si MongoDB échoue, on supprime l’enregistrement PostgreSQL. Dans un vrai système, on pourrait utiliser un Saga pattern ou un outbox pour garantir la cohérence.
Transactions cross‑database : comment maintenir la cohérence ?
Le scénario idéal serait une transaction ACID englobant les deux bases, mais ce n’est pas possible sans un coordinateur externe. Voici trois approches réalistes :
Approche Description Niveau de garantie
Compensation En cas d’échec de la seconde écriture, annuler la première (comme ci‑dessus) Au mieux (best effort)
Saga Chorégraphier les étapes avec des actions de compensation Cohérence éventuelle
Outbox pattern Écrire un événement dans une table fiable, puis un worker propage les changements Cohérence forte (si outbox en DB fiable)
Pour une application à petite échelle, la compensation est souvent suffisante. Pour un système critique, l’outbox pattern avec PostgreSQL comme source de vérité est recommandé.
- Lire et assembler les données des deux mondes Lors de la lecture, vous pouvez récupérer le profil MongoDB à partir de l’UUID stocké dans PostgreSQL (ou inversement). Exemple dans une route Express :
app.get('/users/:uuid', async (req, res) => {
const { uuid } = req.params;
// Récupérer depuis PostgreSQL
const pgUser = await prisma.user.findUnique({ where: { id: uuid } });
if (!pgUser) return res.status(404).json({ error: 'Not found' });
// Récupérer depuis MongoDB
const mongoUser = await MongoUser.findOne({ uuid });
// Fusionner les résultats
res.json({
id: uuid,
email: pgUser.email,
profile: mongoUser?.profile || null,
createdAt: pgUser.createdAt
});
});
C’est le code applicatif qui fait office de jointure entre les deux sources.
Conclusion
Utiliser un UUID comme identifiant partagé entre MongoDB et PostgreSQL est un pattern simple mais puissant pour les architectures hybrides. Il vous permet de :
Profiter du meilleur des deux mondes (documents flexibles et relations strictes).
Exposer une API cohérente sans fuiter d’informations.
Garder une porte ouverte vers une migration future.
La gestion de la cohérence cross‑database reste le point de vigilance, mais des solutions éprouvées existent.
Vous avez déjà mis en place ce genre d’architecture ? Partagez votre retour en commentaire
Top comments (0)