TypeORM Config & Entités

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

Objectifs de la leçon

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

Plan du cours

1

Pourquoi un ORM ?

Le fossé entre objet métier et table SQL

2

Installation & configuration

TypeORM avec PostgreSQL

3

DataSource : la connexion

Configuration et synchronize

4

Les décorateurs d'entité

@Entity, @PrimaryColumn, @Column

5

Entité TypeORM vs Objet métier

Pourquoi c'est DIFFÉRENT — le point clé

Pourquoi un ORM ?

Le fossé entre objet métier et table SQL

TypeScript

Objets, classes, méthodes

PostgreSQL

Tables, lignes, colonnes

Écrire du SQL brut en TypeScript

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

  • • Instances avec méthodes
  • • Types forts
  • • Encapsulation
  • • Relations par référence

Monde relationnel

  • • Lignes sans comportement
  • • Types faibles (tout est string)
  • • Pas d'encapsulation
  • • Relations par clés étrangères

💡 Ce fossé s'appelle le impedance mismatch — l'ORM le comble

Installation de TypeORM

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";

Pourquoi reflect-metadata ?

Les décorateurs TypeORM ont besoin de métadonnées réflexives

Comment ça fonctionne :

  1. TypeScript compile @Column() en un appel de fonction
  2. Cet appel enregistre des métadonnées sur la classe
  3. reflect-metadata fournit l'API Reflect.defineMetadata()
  4. TypeORM lit ces métadonnées au runtime pour construire le schéma

// tsconfig.json — activer les décorateurs

{

"compilerOptions": {

"emitDecoratorMetadata": true,

"experimentalDecorators": true,

"strictPropertyInitialization": false

}

}

❌ Oublier reflect-metadata → erreur runtime "Cannot read metadata" au démarrage

DataSource : la connexion

Le point d'entrée entre votre app et PostgreSQL

DataSource = la configuration qui dit à TypeORM :

"Quelle base ? Quel host ? Quelles entités ?"

Configuration de la DataSource

// 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 SGBD
  • host, port
  • username, password
  • database

Propriétés importantes

  • entities — vos classes décorées
  • synchronize — auto-crée les tables
  • logging — debug SQL

DataSource avec variables d'environnement

Ne 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

Les décorateurs d'entité

@Entity, @PrimaryColumn, @Column

Les décorateurs décrivent la structure de la table

Ils NE contiennent PAS de logique métier

@Entity — Déclarer une table

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 :

  1. Enregistre la classe comme entité dans les métadonnées
  2. TypeORM sait qu'il faut créer/mapper la table "articles"
  3. Le nom de la classe (ArticleEntity) ≠ le nom de la table (articles)

💡 Convention : nom de classe = PascalCase + suffixe Entity, nom de table = snake_case pluriel

@PrimaryColumn — La clé primaire

// 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

@Column — Les colonnes de la table

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 :

string
varchar / text
number
integer
boolean
boolean
Date
timestamp

Entité complète : ArticleEntity

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

}

Entité TypeORM ≠ Objet métier

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

Comparaison côte à côte

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

Pourquoi c'est DIFFÉRENT ?

L'entité TypeORM est un schéma de base de données, pas un objet métier

ArticleEntity — la persistance

  • • Décrit comment stocker les données en base
  • • Les décorateurs = métadonnées pour TypeORM
  • • Les propriétés = colonnes de la table
  • • Pas de constructeur avec validation
  • • Pas de logique métier (publish, isReady, excerpt…)
  • • C'est un DTO de base de données

Article — le domaine

  • • Décrit le comportement d'un article
  • • Méthodes = logique métier
  • • Encapsulation (private, getters)
  • • Validation dans le constructeur
  • • Indépendant de la base de données
  • • C'est un objet du domaine

💡 L'entité TypeORM répond à "OÙ stocker ?" — L'objet métier répond à "QUE fait-il ?"

❌ Mélanger vs ✅ Séparer

❌ Tout dans un fichier

@Entity({ name: "articles" })

export class Article {

@Column() title: string;

@Column() content: string;

publish(): void { ... }

isReady(): boolean { ... }

}

  • • Couplage DB ↔ logique
  • • Impossible de tester sans DB
  • • Difficile à faire évoluer
  • • Violation Single Responsibility

✅ 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 { ... }

}

  • • Responsabilité unique
  • • Testable indépendamment
  • • Évolutif et maintenable
  • • Séparation des préoccupations

TypeORM find() → SQL généré

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

Correspondance find() ↔ SQL

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

synchronize: true — ⚠️ DEV UNIQUEMENT

Que fait synchronize ?

Au démarrage de l'app, TypeORM :

  1. Lit les métadonnées de vos entités (@Entity, @Column…)
  2. Compare avec le schéma actuel de la base
  3. Modifie les tables pour qu'elles correspondent

⚠️ Ce que ça signifie :

  • • Ajoute les tables manquantes ✅
  • • Ajoute les colonnes manquantes ✅
  • Peut SUPPRIMER des colonnes si vous retirez un @Column ❌
  • Peut SUPPRIMER des données en changeant un type ❌

synchronize : ❌ Prod vs ✅ Dev

❌ En production

synchronize: true // ❌ DANGER

  • • Risque de perte de données
  • • Pas de contrôle sur les changements
  • • Pas de rollback possible
  • • Schéma instable au démarrage
  • • Incompatible avec les contraintes FK

✅ 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,

  • • Migrations = changements versionnés
  • • Rollback possible
  • • Revue de code avant apply
  • • Schéma prévisible et stable

Piège #1 : Mélanger entité et objet métier

❌ 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

Piège #2 : synchronize en production

❌ Dangereux

export const AppDataSource = new DataSource({

synchronize: true, // toujours true !

})

  • • Au démarrage, TypeORM modifie le schéma
  • • Renommer une colonne = supprimer + recréer
  • Données perdues irrémédiablement

✅ Sécurisé

export const AppDataSource = new DataSource({

synchronize: process.env.NODE_ENV

=== "development",

migrations: ["src/migrations/*.ts"],

})

  • • En dev : auto-sync pour itérer vite
  • • En prod : migrations contrôlées
  • Zéro risque de perte de données

Piège #3 : Oublier reflect-metadata

❌ 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

À retenir !

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.

Architecture résumée

🗄️

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

Exercices pratiques

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.

TypeORM Config & Entités

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 ?