TD4 : Découverte de Sequelize

1. Introduction : Les ORM

Dans ce TD, nous allons découvrir un ORM (Object-Relational Mapping) appelé Sequelize. Un ORM est une bibliothèque qui permet d'interagir avec une base de données relationnelle en utilisant des classes dans le langage de programmation utilisé, plutôt que d'écrire des requêtes SQL directement.

Le principal avantage d'utiliser un ORM est qu'il simplifie le processus de manipulation des données en fournissant une interface orientée objet. Ainsi toute l'application est écrite dans un seul langage (ici le JavaScript) et on n'a pas besoin de mélanger SQL et JavaScript.

L'inconvénient est que les performances peuvent être légèrement inférieures à celles des requêtes SQL optimisées manuellement. En effet, les ORM génèrent automatiquement des requêtes SQL, ce qui peut parfois entraîner des requêtes moins efficaces.

2. Mise en place

Copiez votre code du TD3 dans un nouveau dossier pour commencer ce TD4.

Installez Sequelize avec npm install sequelize.

Le module pg est toujours nécessaire car Sequelize l'utilise pour se connecter à PostgreSQL. Cependant nous n'utiliserons plus directement l'objet Pool mais Sequelize pour gérer la connexion à la base de données et les requêtes.

Il va donc falloir modifier le fichier de connexion db.js pour utiliser Sequelize :

const Sequelize = require('sequelize');
require('dotenv').config();

const sequelize = new Sequelize(process.env.DB_DATABASE, process.env.DB_USERNAME, process.env.DB_PASSWORD, {
	dialect: 'postgres',
	host: process.env.DB_HOST,
	port: process.env.DB_PORT,
});   

module.exports = sequelize;

Le langage de base de données (ici PostgreSQL) est spécifié avec l'option dialect. Seqelize supporte plusieurs SGBD (MySQL, SQLite, MSSQL, etc...). Pour notre cas, nous utilisons postgres.

3. Définition des modèles

Créez un dossier models dans lequel vous créerez un fichier planet_model.js, plant_model.js et scentist_model.js.

En guise d'exemple, voici le code pour le modèle planet_model.js :

const sequelize = require('../database/db');
const { DataTypes } = require('sequelize');

const Planet = sequelize.define('Planet', {
	planet_id: {
		type: DataTypes.INTEGER,
		primaryKey: true,
		autoIncrement: true
	},
	name: {
		type: DataTypes.STRING(100),
		allowNull: false
	},
	galaxy: {
		type: DataTypes.STRING(100),
		allowNull: true
	},
	atmosphere_type: {
		type: DataTypes.STRING(100),
		allowNull: true
	},
	temperature: {
		type: DataTypes.DECIMAL(5, 2),
		allowNull: true
	}
}, {
	tableName: 'planet',
	timestamps: false
});

module.exports = Planet;

Inspriez-vous en pour créer les autres modèles.

Toujours dans le répertoire models, créez un fichier index.js qui servira à centraliser l'importation des modèles et à définir les relations entre eux. Voici le squelette de ce fichier :

var sequelize = require('../database/db')

// Importation des modèles
const Planet = require('./planet_model');
// à compléter ...


async function sync_database(){
	try {
		await sequelize.authenticate();
		console.log('Connection has been established successfully.');

		// Synchronize models : (never use force: true in production -> it drops tables)
		Planet.sync({ force: false}); 
		// À compléter ...
		
	} catch (error) {
		console.error('Unable to connect to the database:', error);
	}
}

// Exemple d'association One-to-Many between Planet and Plant
// Plant.belongsTo(Planet, { foreignKey: 'planet_id' });
// Planet.hasMany(Plant, { foreignKey: 'planet_id' });


// On verifie la connexion et on synchronise les modèles avec la base de données
sync_database();

module.exports = {
	Planet : Planet,
	// à compléter ...
};

4. À vous de jouer !

Modifiez le code de votre application qu'elle utilise désormais les modèles Sequelize pour interagir avec la base de données.

Référez-vous à la documentation de Sequelize pour apprendre à effectuer des opérations CRUD (Create, Read, Update, Delete) avec les modèles. Documentation Sequelize

Les fichiers de service sont ceux qui subiront le plus de modifications. Mais leurs lisibilité sera grandement améliorée ! Voici un exemple pour la table planet :

const model = require("../models/index");

async function get_planets(){
	return await model.Planet.findAll();
}

async function get_planet(id){
	return await model.Planet.findByPk(id);
	
}

module.exports = {
	get_planets,
	get_planet
};

Executez votre application une première fois pour verifier que tout fonctionne correctement. Si la connexion à la base de données est établie le message "Connection has been established successfully." doit s'afficher dans la console. Sequelize va également afficher les requêtes PGSQL qu'il exécute.

Enfin il est fortement probable que des erreurs apparaissent car il se peut que vos contrôleur ne soit plus compatibles avec les services modifiés. Corrigez-les en conséquence.

Les concepts clé de Sequelize

Lazy Loading vs Eager Loading

Par défaut, Sequelize utilise le Lazy Loading pour charger les associations entre les modèles. Cela signifie que les données associées ne sont chargées que lorsque vous y accédez explicitement. Par exemple, si vous avez un modèle Planet qui a une association avec le modèle Plant, les plantes associées à une planète ne seront pas chargées automatiquement lorsque vous récupérez une planète.

Si vous souhaitez charger les données associées en même temps que le modèle principal, vous pouvez utiliser le Eager Loading. Cela se fait en utilisant l'option include dans les méthodes de requête en précisant les modèles associés à télégarger. Par exemple : Planet.findAll({ include: [Plant] }) chargera toutes les planètes avec leurs plantes associées.

Association Many-to-Many

Pour gérer les associations Many-to-Many une table de jointure est nécessaire. Par exemple, pour permettre à un scientifique d'avoir des permissions sur plusieurs plantes, et à une plante d'être associée à plusieurs scientifiques, vous devez créer une table de jointure appelée permission. Ensuite, vous pouvez définir l'association Many-to-Many comme suit :

Scientist.belongsToMany(Plant, { through: 'permission', foreignKey: 'user_id' });
Plant.belongsToMany(Scientist, { through: 'permission', foreignKey: 'plant_id' });

Les transactions

Les transactions permettent de regrouper plusieurs opérations de base de données en une seule unité de travail. Si une opération échoue, toutes les opérations précédentes dans la transaction sont annulées, garantissant ainsi l'intégrité des données. Voici un exemple d'utilisation des transactions avec Sequelize :

const { Sequelize } = require('sequelize');
const sequelize = require('../database/db');
async function performTransaction() {
	const t = await sequelize.transaction();
	try {
		// Effectuer des opérations de base de données ici
		await Model1.create({ ... }, { transaction: t });
		await Model2.update({ ... }, { where: { ... }, transaction: t });

		// Si tout se passe bien, valider la transaction
		await t.commit();
	} catch (error) {
		// En cas d'erreur, annuler la transaction
		await t.rollback();
		throw error;
	}
}