ORM, décorateurs et séparation des responsabilités
Bootcode — IWA-S04 — Semaine 14, Jour 4
Utilisez les flèches, cliquez ou glissez pour naviguer
1. Installer & configurer TypeORM
Avec PostgreSQL et DataSource
2. Créer des entités avec les décorateurs
@Entity, @PrimaryColumn, @Column
3. Comprendre DataSource & synchronize
Connexion et synchronisation du schéma
4. Entité TypeORM ≠ Objet métier
Le point CENTRAL de cette leçon
Pourquoi un ORM ?
Le fossé entre objet métier et table SQL
Installation & configuration
TypeORM avec PostgreSQL
DataSource : la connexion
Configuration et synchronize
Les décorateurs d'entité
@Entity, @PrimaryColumn, @Column
Entité TypeORM vs Objet métier
Pourquoi c'est DIFFÉRENT — le point clé
Le fossé entre objet métier et table SQL
TypeScript
Objets, classes, méthodes
PostgreSQL
Tables, lignes, colonnes
Le problème : le monde objet ≠ le monde relationnel
// Sans ORM — on écrit du SQL à la main
const result = await pool.query(
`SELECT * FROM articles WHERE published = true ORDER BY created_at DESC`
);
// On reçoit des objets "plats" — pas des instances de classe
const rows = result.rows; // { id, title, created_at, ... }
// ❌ Pas de validation, pas de méthode, pas de type sûr
console.log(rows[0].created_at); // string, pas Date !
Monde objet
Monde relationnel
💡 Ce fossé s'appelle le impedance mismatch — l'ORM le comble
Les dépendances à installer
# TypeORM + le driver PostgreSQL
npm install typeorm pg
# Reflect-metadata — OBLIGATOIRE pour les décorateurs
npm install reflect-metadata
# Types TypeScript pour pg
npm install -D @types/pg
⚠️ reflect-metadata DOIT être importé en TOUT PREMIER dans votre fichier principal
// index.ts — PREMIÈRE ligne du fichier !
import "reflect-metadata";
// Ensuite seulement :
import { DataSource } from "typeorm";
Les décorateurs TypeORM ont besoin de métadonnées réflexives
Comment ça fonctionne :
@Column() en un appel de fonctionreflect-metadata fournit l'API Reflect.defineMetadata()// tsconfig.json — activer les décorateurs
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"strictPropertyInitialization": false
}
}
❌ Oublier reflect-metadata → erreur runtime "Cannot read metadata" au démarrage
Le point d'entrée entre votre app et PostgreSQL
DataSource = la configuration qui dit à TypeORM :
"Quelle base ? Quel host ? Quelles entités ?"
// data-source.ts
import { DataSource } from "typeorm";
import { ArticleEntity } from "./entities/article.entity";
export const AppDataSource = new DataSource({
type: "postgres",
host: "localhost",
port: 5432,
username: "postgres",
password: "secret",
database: "bootcode_db",
synchronize: true, // ⚠️ DEV UNIQUEMENT
entities: [ArticleEntity],
logging: true, // Voir le SQL généré
})
Propriétés obligatoires
type — le SGBDhost, portusername, passworddatabasePropriétés importantes
entities — vos classes décoréessynchronize — auto-crée les tableslogging — debug SQLNe JAMAIS hardcoder les identifiants en production
// .env
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=secret
DB_DATABASE=bootcode_db
// data-source.ts — version propre
export const AppDataSource = new DataSource({
type: "postgres",
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
synchronize: process.env.NODE_ENV === "development",
entities: ["src/entities/*.entity.ts"],
logging: process.env.NODE_ENV === "development",
})
✅ synchronize: true SEULEMENT en dev — en prod on utilise des migrations
@Entity, @PrimaryColumn, @Column
Les décorateurs décrivent la structure de la table
Ils NE contiennent PAS de logique métier
Le décorateur @Entity dit à TypeORM : "Cette classe = une table"
// article.entity.ts
import { Entity } from "typeorm";
@Entity({ name: "articles" }) // nom de la table SQL
export class ArticleEntity {
// les propriétés seront ajoutées avec @Column
}
Ce que fait @Entity :
"articles"ArticleEntity) ≠ le nom de la table (articles)💡 Convention : nom de classe = PascalCase + suffixe Entity, nom de table = snake_case pluriel
// Option 1 : UUID généré automatiquement
import { Entity, PrimaryColumn } from "typeorm";
@Entity({ name: "articles" })
export class ArticleEntity {
@PrimaryColumn({
type: "uuid",
default: () => "uuid_generate_v4()"
})
id: string;
}
// Option 2 : PrimaryGeneratedColumn — plus simple
import { PrimaryGeneratedColumn } from "typeorm";
@PrimaryGeneratedColumn("uuid")
id: string;
// ⬆️ Génère automatiquement un UUID v4
💡 Toujours utiliser UUID en 2024 — jamais d'auto-increment pour des IDs publics
Chaque @Column correspond à une colonne SQL
// Les types les plus courants
@Column({ type: "varchar", length: 255 })
title: string;
@Column({ type: "text" })
content: string;
@Column({ type: "integer" })
viewCount: number;
@Column({ type: "boolean", default: false })
published: boolean;
@Column({ type: "timestamp" })
createdAt: Date;
Correspondance TypeScript ↔ PostgreSQL :
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
@Entity({ name: "articles" })
export class ArticleEntity {
@PrimaryGeneratedColumn("uuid")
id: string;
@Column({ type: "varchar", length: 255 })
title: string;
@Column({ type: "text" })
content: string;
@Column({ type: "boolean", default: false })
published: boolean;
@Column({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" })
createdAt: Date;
}
Table SQL générée
articles (
id UUID PK,
title VARCHAR(255),
content TEXT,
published BOOLEAN DEFAULT false,
created_at TIMESTAMP
)
Classe TypeScript
ArticleEntity {
id: string
title: string
content: string
published: boolean
createdAt: Date
}
Le point CENTRAL de cette leçon
⚠️ C'est l'erreur la plus fréquente chez les débutants
Mélanger ces deux concepts dans le même fichier
ArticleEntity (TypeORM)
Fichier : article.entity.ts
Article (Domaine)
Fichier : article.ts
@Entity({ name: "articles" })
export class ArticleEntity {
@PrimaryGeneratedColumn("uuid")
id: string;
@Column() title: string;
@Column() content: string;
@Column() published: boolean;
}
// ❌ AUCUNE méthode
// ❌ AUCUNE logique métier
// ❌ AUCUNE validation
// Juste : des données + des décorateurs
export class Article {
private _title: string;
private _content: string;
constructor(
title: string,
content: string,
) {
this._title = title;
this._content = content;
}
publish(): void { /* logique */ }
isReady(): boolean { /* logique */ }
excerpt(): string { /* logique */ }
}
// ✅ Méthodes métier
// ✅ Encapsulation
// ✅ Validation dans le constructeur
L'entité TypeORM est un schéma de base de données, pas un objet métier
ArticleEntity — la persistance
Article — le domaine
💡 L'entité TypeORM répond à "OÙ stocker ?" — L'objet métier répond à "QUE fait-il ?"
❌ Tout dans un fichier
@Entity({ name: "articles" })
export class Article {
@Column() title: string;
@Column() content: string;
publish(): void { ... }
isReady(): boolean { ... }
}
✅ Séparé en deux fichiers
// article.entity.ts
@Entity()
export class ArticleEntity {
@Column() title: string;
@Column() content: string;
}
// article.ts
export class Article {
publish(): void { ... }
isReady(): boolean { ... }
}
Comprendre ce que TypeORM fait sous le capot
// TypeORM — méthode find
const articles = await
AppDataSource
.getRepository(ArticleEntity)
.find({
where: { published: true },
order: { createdAt: "DESC" },
});
// SQL généré automatiquement
SELECT
id, title, content,
published, created_at
FROM articles
WHERE published = true
ORDER BY created_at DESC;
// Autres exemples find
// find() → SELECT * FROM articles
// findOne({ where: { id } }) → SELECT ... WHERE id = $1 LIMIT 1
// findBy({ published: true }) → SELECT ... WHERE published = true
// findAndCount() → SELECT + COUNT total pour pagination
💡 Activez logging: true dans DataSource pour voir le SQL en console
Les options de find() correspondent 1:1 aux clauses SQL
| Option TypeORM | Clause SQL | Exemple |
|---|---|---|
| where | WHERE | { published: true } |
| order | ORDER BY | { createdAt: "DESC" } |
| skip / take | OFFSET / LIMIT | { skip: 10, take: 5 } |
| relations | JOIN | { author: true } |
| select | SELECT (colonnes) | { id: true, title: true } |
💡 TypeORM est un traducteur : vos objets TypeScript → requêtes SQL
Que fait synchronize ?
Au démarrage de l'app, TypeORM :
⚠️ Ce que ça signifie :
❌ En production
synchronize: true // ❌ DANGER
✅ Bonnes pratiques
// Dev : auto-sync pour itérer vite
synchronize: process.env.NODE_ENV
=== "development"
// Prod : migrations contrôlées
migrations: ["src/migrations/*.ts"],
migrationsRun: true,
❌ Mauvais
@Entity()
export class Article {
@Column() title: string;
publish(): void {
this.published = true;
this.publishedAt = new Date();
}
}
Logique métier dans l'entité DB → couplage fort
✅ Bon
// article.entity.ts
@Entity()
export class ArticleEntity {
@Column() title: string;
}
// article.ts
export class Article {
publish(): void { ... }
}
Séparation claire → testable, maintenable
❌ Dangereux
export const AppDataSource = new DataSource({
synchronize: true, // toujours true !
})
✅ Sécurisé
export const AppDataSource = new DataSource({
synchronize: process.env.NODE_ENV
=== "development",
migrations: ["src/migrations/*.ts"],
})
❌ Erreur courante
// index.ts — import manquant
import { DataSource } from "typeorm";
// ❌ Pas d'import reflect-metadata
// Erreur au runtime :
TypeError: Reflect.getMetadata
is not a function
Les décorateurs ne fonctionnent pas sans reflect-metadata
✅ Correct
// index.ts
import "reflect-metadata"; // ← PREMIÈRE ligne !
import { DataSource } from "typeorm";
// ✅ Les métadonnées sont disponibles
// ✅ Les décorateurs fonctionnent
Toujours en PREMIER — avant tout autre import
1. Entité TypeORM ≠ Objet métier
L'entité décrit la table SQL. L'objet métier contient la logique. Jamais les mélanger.
2. Les décorateurs = métadonnées de schéma
@Entity, @Column, @PrimaryColumn décrivent COMMENT stocker, pas QUE faire.
3. DataSource = la connexion
Configurez host, port, credentials, entities. Utilisez des variables d'environnement.
4. synchronize: true = DEV ONLY
En production, utilisez des migrations. synchronize peut détruire des données.
5. reflect-metadata en premier
Import OBLIGATOIRE en première ligne. Sans lui, les décorateurs échouent au runtime.
🗄️
ArticleEntity
Persistance
@Entity, @Column
article.entity.ts
🧠
Article
Domaine
publish(), isReady()
article.ts
🔗
ArticleRepository
Pont
Entity ↔ Domain
article.repository.ts
// Le repository fait le pont
const entity = await repo.findOneBy({ id }); // DB → Entity
const article = toDomain(entity); // Entity → Domain
article.publish(); // Logique métier ✅
const updated = toEntity(article); // Domain → Entity
await repo.save(updated); // Entity → DB
Exercice 1 : Configuration de base
Installez TypeORM + pg + reflect-metadata. Créez une DataSource avec variables d'environnement. Vérifiez la connexion avec AppDataSource.initialize().
Exercice 2 : Créer une entité UserEntity
Créez user.entity.ts avec : id (UUID), email (varchar unique), name (varchar), isActive (boolean, default true), createdAt (timestamp). Ajoutez-la à la DataSource.
Exercice 3 : Séparer entité et domaine
Créez un objet métier User avec une méthode deactivate() et changeEmail(). Écrivez les fonctions toDomain() et toEntity() pour convertir.
Exercice 4 : Observer le SQL généré
Activez logging: true. Utilisez find(), findOne(), findBy() et observez le SQL dans la console. Comparez avec ce que vous auriez écrit à la main.
L'entité TypeORM décrit la table. L'objet métier contient la logique.
Ne les mélangez JAMAIS.
Bootcode — IWA-S04 — Semaine 14, Jour 4
Questions ?