Concevoir des bases de données pour l'échelle : données isolées par service
Les bases de données partagées sont l'erreur microservices la plus fréquente. Quand tous les services lisent et écrivent dans les mêmes tables, vous perdez la capacité de les scaler, déployer et versionner indépendamment. Voici le pattern database-per-service et comment nous l'avons implémenté.
Le pattern « Base de données par service » garantit un couplage faible dans les microservices. Ce guide couvre les fondamentaux des ORM, le fonctionnement interne de Prisma, la conception de schémas avec des relations, et la configuration pour les architectures multi-bases de données.
Qu'est-ce qu'un ORM ?
Un ORM (Object-Relational Mapping) est une technique qui vous permet d'interagir avec des bases de données en utilisant les objets de votre langage de programmation plutôt qu'en écrivant des requêtes SQL brutes.
SANS ORM AVEC ORM
Code de l'application Code de l'application
│ │
▼ ▼
SQL brut Couche ORM
"SELECT * FROM users prisma.user.findMany()
WHERE id = 1" │
│ ▼
▼ SQL généré
Base de données │
▼
Base de données
Avantages des ORM
- Sécurité des types : Les vérifications à la compilation empêchent les requêtes invalides
- Abstraction : Code agnostique à la base de données (passer de PostgreSQL à MySQL)
- Productivité : Moins de code répétitif, code plus lisible
- Sécurité : Protection intégrée contre les injections SQL
- Migrations : Modifications de schéma versionnées
Qu'est-ce que Prisma ?
Prisma est un ORM TypeScript de nouvelle génération qui adopte une approche différente des ORM traditionnels. Au lieu de mapper des classes vers des tables, Prisma utilise un schéma déclaratif pour générer un constructeur de requêtes typé.
Composants de Prisma
- Prisma Schema : Source unique de vérité pour la structure de votre base de données
- Prisma Client : Constructeur de requêtes auto-généré et typé
- Prisma Migrate : Système de migration déclaratif
- Prisma Studio : Navigateur visuel de base de données
Comment Prisma fonctionne en interne
ARCHITECTURE PRISMA
┌─────────────────────────────────────────────────────┐
│ Votre Application │
│ │
│ prisma.user.findMany({ where: { active: true }}) │
└─────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Prisma Client (Généré) │
│ │
│ • Définitions de types depuis le schéma │
│ • Méthodes du constructeur de requêtes │
│ • Sérialise les requêtes vers le protocole │
└─────────────────────┬───────────────────────────────┘
│ Protocole Binaire
▼
┌─────────────────────────────────────────────────────┐
│ Moteur de requêtes Prisma │
│ (Binaire Rust) │
│ │
│ • Analyse et valide les requêtes │
│ • Génère du SQL optimisé │
│ • Gestion du pool de connexions │
│ • Gestion des transactions │
└─────────────────────┬───────────────────────────────┘
│ SQL
▼
┌─────────────────────────────────────────────────────┐
│ Base de données │
│ (PostgreSQL, MySQL, etc.) │
└─────────────────────────────────────────────────────┘
Le moteur de requêtes Prisma est un binaire Rust qui s'exécute en tant que processus sidecar. Cette architecture permet :
- Performance : La rapidité de Rust pour l'analyse et l'optimisation des requêtes
- Pool de connexions : Gestion efficace des connexions à la base de données
- Multi-plateforme : Différents binaires selon le système d'exploitation (binaryTargets)
Le pattern Base de données par service
Dans les microservices, chaque service doit posséder ses données. Le pattern Base de données par service maintient les données persistantes de chaque microservice privées, accessibles uniquement via son API.
BASE DE DONNÉES PAR SERVICE
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Service │ │ Service │ │ Service │
│ A │ │ B │ │ C │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Base de │ │ Base de │ │ Base de │
│ données A │ │ données B │ │ données C │
│ (PostgreSQL)│ │ (MongoDB) │ │ (Redis) │
└─────────────┘ └─────────────┘ └─────────────┘
Options d'implémentation
- Tables privées : Chaque service possède des tables spécifiques dans une base de données partagée
- Schéma par service : Schémas séparés au sein du même serveur de base de données
- Base de données par service : Serveur de base de données dédié pour chaque service
Avantages
- Couplage faible : Les modifications de la couche de données d'un service n'affectent pas les autres
- Flexibilité technologique : Chaque service peut utiliser le type de base de données le mieux adapté à ses besoins
- Scalabilité indépendante : Mise à l'échelle des bases de données en fonction des exigences de chaque service
- Isolation des pannes : Les défaillances de base de données ne se propagent pas entre les services
Défis
- Transactions distribuées : La logique métier couvrant plusieurs services nécessite une coordination
- Requêtes inter-services : La jointure de données entre les frontières des services n'est pas possible
- Cohérence des données : Cohérence éventuelle au lieu des garanties ACID
- Complexité opérationnelle : Gestion de plusieurs systèmes de base de données
Solutions aux défis
- Pattern Saga : Coordonner les transactions entre services via des événements
- Composition d'API : Agréger les données de plusieurs services au niveau de l'API Gateway
- CQRS : Séparer les modèles de lecture et d'écriture pour les requêtes complexes
- Event Sourcing : Stocker les changements d'état sous forme d'événements pour la cohérence
Configuration du générateur
Chaque microservice possède son propre fichier prisma/schema.prisma. Des chemins de sortie personnalisés empêchent les clients générés de s'écraser mutuellement.
generator client {
provider = "prisma-client-js"
output = "./generated/client"
binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
- output : Un chemin personnalisé évite les conflits entre les clients des différents services
- binaryTargets : Requis pour les images Docker Alpine (linux-musl-openssl-3.0.x)
- native : Permet le développement local sur macOS/Linux
Conception du schéma
Modèles avec relations
model User {
id String @id @default(cuid())
email String @unique
name String?
role Role @default(USER)
posts Post[]
profile Profile?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("users")
}
model Profile {
id String @id @default(cuid())
bio String?
avatar String?
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("profiles")
}
model Post {
id String @id @default(cuid())
title String
content String?
published Boolean @default(false)
authorId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
categories Category[]
@@map("posts")
}
model Category {
id String @id @default(cuid())
name String @unique
posts Post[]
@@map("categories")
}
enum Role {
USER
ADMIN
MODERATOR
}
Types de relations
- Un-à-un : Un utilisateur a un profil (
Profile?+@uniquesur la clé étrangère) - Un-à-plusieurs : Un utilisateur a plusieurs articles (
Post[]) - Plusieurs-à-plusieurs : Les articles ont plusieurs catégories (table de jointure implicite)
Attributs du schéma
- @@map("nom_table") : Nom de table personnalisé dans la base de données
- @map("nom_colonne") : Nom de colonne personnalisé
- onDelete: Cascade : Supprime automatiquement les enregistrements liés
- @@unique([champ1, champ2]) : Contrainte d'unicité composite
- @default(cuid()) : Identifiants uniques résistants aux collisions
- @updatedAt : Mise à jour automatique du timestamp lors des modifications
Utilisation du client
Importez le client généré spécifique plutôt que le client générique.
import { Injectable } from "@nestjs/common";
import { PrismaClient } from "../../prisma/generated/client";
@Injectable()
export class UserService {
private prisma = new PrismaClient();
async findByEmail(email: string) {
return this.prisma.user.findUnique({
where: { email },
include: { posts: true, profile: true }
});
}
async createWithProfile(data: CreateUserDto) {
return this.prisma.user.create({
data: {
email: data.email,
name: data.name,
profile: {
create: {
bio: data.bio
}
}
},
include: { profile: true }
});
}
async updateUser(id: string, data: UpdateUserDto) {
return this.prisma.user.update({
where: { id },
data
});
}
}
Stratégie de migration
Chaque service gère ses propres migrations de manière indépendante.
npx prisma migrate dev --schema=apps/auth-service/prisma/schema.prisma
npx prisma migrate dev --schema=apps/content-service/prisma/schema.prisma
npx prisma migrate dev --schema=apps/analytics-service/prisma/schema.prisma
Cette isolation stricte signifie que les services ne peuvent physiquement pas interroger les tables des autres services, forçant la communication via des APIs ou des files de messages.
Configuration Docker
Service PostgreSQL
services:
database:
image: postgres
container_name: database
ports:
- "5432:5432"
restart: always
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=${DB_USER}
- POSTGRES_PASSWORD=${DB_PASSWORD}
volumes:
- database_volume:/var/lib/postgresql
volumes:
database_volume:
Dockerfile multi-étapes
FROM node:18-alpine AS base
FROM base AS builder
RUN apk add --no-cache libc6-compat
WORKDIR /app
RUN npm install -g turbo
COPY . .
RUN turbo prune --scope=auth-service --docker
FROM base AS installer
RUN apk add --no-cache libc6-compat openssl
WORKDIR /app
COPY --from=builder /app/out/json/ .
COPY --from=builder /app/turbo.json ./turbo.json
COPY --from=builder /app/apps/auth-service/prisma ./prisma
RUN npm install
RUN npx prisma generate
FROM base AS sourcer
WORKDIR /app
COPY --from=installer /app/ .
COPY --from=builder /app/out/full/ .
RUN npx turbo run build --filter=auth-service...
FROM base AS runner
WORKDIR /app
RUN apk add --no-cache openssl
COPY --from=sourcer /app/ .
WORKDIR /app/apps/auth-service/
EXPOSE 3000
CMD ["node", "dist/main.js"]
Étapes de build
- builder : Élaguer le monorepo vers le service cible et ses dépendances
- installer : Installer les packages et générer le client Prisma avec les binaires corrects
- sourcer : Construire l'application avec le cache Turborepo
- runner : Image de production minimale
Le package openssl est requis dans les étapes installer et runner pour le moteur de requêtes de Prisma.
Conclusion
Prisma comble le fossé entre la sécurité des types et les performances de base de données grâce à son architecture unique. Le moteur de requêtes basé sur Rust gère le pool de connexions et l'optimisation SQL, tandis que le client TypeScript généré offre des garanties à la compilation qui préviennent les erreurs à l'exécution.
Le pattern base de données par service renforce les frontières des microservices au niveau de la couche de données. Les services ne peuvent pas accidentellement interroger les tables des autres services, forçant une communication explicite via des APIs ou des files de messages. Cette isolation permet des déploiements indépendants, une flexibilité technologique et le confinement des pannes.
Avec des sorties de générateur personnalisées, chaque service maintient son propre client Prisma sans conflits. Combiné avec des builds Docker multi-étapes et les bonnes cibles binaires, cette architecture passe sans accroc du développement local aux conteneurs de production.
Écrit par

Tech Lead et Ingénieur Full Stack pilotant une équipe de 5 ingénieurs chez Fygurs (Paris, Remote) sur un SaaS cloud-native Azure. Diplômé de 1337 Coding School (42 Network / UM6P). Écrit sur l'architecture, l'infrastructure cloud et le leadership technique.