A quoi sert un ORM ?
Un ORM permet de faire le pont entre une base de données comme SQLite et la représentation d’un objet dans un langage de programmation comme Typescript.
Pour cela, un ORM fournit un ensemble d’outils qui permettent d’intéragir entre le code et la base de données. Ces outils permettent de:
- Définir le sockage des données en base avec les modèles
- Récupérer, sauvegarder, supprimer un objet Typescript en base de données (CRUD)
- Construire des requêtes SQL complexes pour la base de données
Dans cet article, on va voir quelques utilisations de base d’un ORM avec pour exemple Sequelize qui utilisera une base de données SQLite pour continuer sur la lancée de mon dernier article.
Installation de Sequelize
Sequelize s’installe via npm: npm install --save sequelize
. On
installe ensuite les définitions Typescript de Sequelize: npm install --save-dev @types/sequelize
. Si vous n’avez pas installé le driver
SQLite dans le projet, vous devez le faire (même procédure que dans
l’article précédent).
Connexion à la base SQLite
Le premier avantage d’un ORM est qu’il permet d’intéragir avec plusieurs types de bases de données SQL, et de prendre en charge les spécificités de chaque base de données tout en fournissant une interface générique. Sequelize permet de se connecter aux bases de données suivantes:
- SQLite
- MySQL/MariaDB
- PostgreSQL
Pour commencer, on crée un fichier database.ts
qui contient la
connexion à la base de données SQLite:
import { Sequelize } from "sequelize";
export const sequelize = new Sequelize(
{ dialect: 'sqlite', storage: __dirname + '/db.sqlite' }
);
Pour créer une connexion à la base SQLite, on indique à Sequelize que
le type de base de données SQL (sqlite
). Puis on précise que le
stockage des données se fait dans le fichier db.sqlite
, situé à la
racine du projet.
C’est l’unique étape où on doit spécifier à Sequelize qu’on utilise SQLite. Si on choisit par exemple de migrer vers MariaDB par la suite, on devra seulement modifier ce fichier de connexion.
Définition d’un modèle de données
On part du mini-projet réalisé dans l’article d’intégration de SQLite avec Typescript dans lequel on a déjà installé le driver SQLite et réalisé un petit script d’introduction.
Dans cet exemple, on a créé une table d’articles, et inséré quelques exemples en base de données. Puis on a réalisé quelques requêtes vers la base SQLite.
L’objectif va être de transformer ces requêtes SQL en appels Typescript à l’ORM Sequelize.
Un modèle Sequelize va faire le lien entre la table SQL articles
et
l’objet Javascript Article
. On stocke ce fichier dans
models/article.ts
.
import { DataTypes } from "sequelize";
import { sequelize } from "../database";
export const ArticleModele = sequelize.define('Article', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
},
titre: {
type: DataTypes.STRING,
allowNull: false
},
description: {
type: DataTypes.TEXT,
allowNull: false
}
}, { tableName: 'articles', timestamps: false });
La table articles
présente trois colonnes, id
, titre
et
description
. Les propriétés du modèle Article
correspondent aux
colonnes de la base de données.
On a précisé le nom de la table dans laquelle sont stockées les
articles via l’option tableName: 'articles'
.
L’option timestamps: false
désactive l’ajout de deux colonnes
createdAt
et updatedAt
qui servent à stocker la date d’insertion
et de modification d’un article.
Vous pouvez demander à Sequelize de créer la table correspondant au
modèle Article
avec ArticleModele.sync()
. N’utilisez cette option
que pour le développement. En production, Sequelize recommande
d’utiliser Umzug pour réaliser
vos migrations de base de données
Ajout d’un type TypeScript au modèle
Si on inspecte le type de ArticleModele
, on obtient
ModelCtor<Model<any, any>>
Les propriétés du modèle déduites par TypeScript sont donc de type any
pour l’instant. L’initialisation d’un modèle Sequelize ne suffit pas
pour obtenir un type utile et précis
Heureusement, le guide d’intégration de TypeScript avec Sequelize donne la solution à ce problème:
A la place de définir un constructeur de modèle via la méthode
sequelize.define
, on va définir une classe ArticleModele
qui
hérite de Model
, puis initialiser cette classe via la méthode init
héritée de Model
import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model } from "sequelize";
import { sequelize } from "../database";
export class ArticleModele extends Model<InferAttributes<ArticleModele>, InferCreationAttributes<ArticleModele>> {
declare id: CreationOptional<number>;
declare titre: string;
declare description: string;
};
ArticleModele.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
},
titre: {
type: DataTypes.STRING,
allowNull: false
},
description: {
type: DataTypes.TEXT,
allowNull: false
}
}, { sequelize, tableName: 'articles', timestamps: false });
Le mot clef
declare
de TypeScript permet de déclarer que la variable de classe va exister
dans le code JavaScript sans que le compilateur TypeScript ne crée
cette variable de classe. Ce comportement est souhaitable car les
propriétés id
, titre
et description
seront ajoutées à la classe
lors de l’appel à la méthode ArticleModele.init
La propriété id
est annotée comme étant optionnelle via
CreationOptional
lors de la sauvegarde d’un nouveau modèle en base
de données car la colonne id
est auto-générée par AUTOINCREMENT
Le modèle ArticleModele
dispose maintenant d’un type TypeScript, ce
qui sera utile par la suite
Manipulation des données
Dans l’article précédent sur SQLite, on a vu comment manipuler les données de la base avec le driver SQLite.
On écrivait nos requêtes SQL (parfois avec des paramètres), puis on demandait au driver de les exécuter et de récupérer les résultats.
C’est maintenant l’ORM Sequelize qui va construire les requêtes SQL à partir des arguments qu’on va lui fournir.
Lecture
Comme dans le premier tutoriel, on va récupérer les deux articles dont la description est la plus longue:
import { ArticleModele } from "./models/article";
async function main () {
const deuxArticlesPlusLongs = await ArticleModele.findAll({
order: [[sequelize.fn('length', sequelize.col('description')), 'DESC']],
limit: 2
});
}
main();
On peut afficher la requête générée par Sequelize avec l’option
logging: console.log
passée à la méthode findAll
. L’appel à
Sequelize précédent génère cette requête SQL:
SELECT `id`, `titre`, `description`
FROM `articles` AS `Article`
ORDER BY length(`description`) DESC
LIMIT 2
La requête SQL générée par Sequelize correspond à la requête précédemment écrite à la main. Sauf que cette fois, on a seulement indiqué la partie importante de la requête, à savoir le nombre d’articles à récupérer et la méthode de tri.
Le code est aussi devenu plus adaptable: si par exemple on ajoute
une colonne à la table articles
, il sera seulement nécéssaire
d’ajouter cette colonne à la définition Sequelize du modèle, peu
importe le nombre d’appels aux méthodes du modèle. Avec des requêtes
SQL écrites à la main, il aurait fallu ajouter cette colonne à chaque
requête
Modification et sauvegarde d’une instance de modèle
Avec Sequelize, on peut faire des modifications sur les objets
retournés par la méthode findAll
de Sequelize, puis les sauvegarder
avec les méthodes set
et save
disponibles sur chacun des objets
renvoyés.
import { ArticleModele } from "./models/article";
async function main () {
const deuxArticlesPlusLongs = await ArticleModele.findAll({
order: [[sequelize.fn('length', sequelize.col('description')), 'DESC']],
limit: 2
});
const premierArticle = deuxArticlesPlusLongs[0];
premierArticle.set('titre', 'Article le plus long');
await premierArticle.save();
}
main();
Notez que si l’instance de modèle est déjà à jour (i.e pas d’opération
modifiant l’instance du modèle exécutée depuis le dernier save
),
Sequelize ne lancera pas de requête SQL d’UPDATE
Suppression
Pour supprimer une instance Sequelize de la base de données, il suffit
d’appeler la méthode destroy
.
const secondArticle = deuxArticlesPlusLongs[1];
await secondArticle.destroy();
Ce morceau de code a supprimé le deuxième article retournée par
findAll
de la base SQLite.
Conclusion
L’ORM Sequelize permet de faciliter l’intégration d’une base de données SQL externe à votre application Typescript. Vous pouvez récupérer une archive du projet correspondant à ce tutoriel.
Sequelize fournit des méthodes permettant de se connecter à la base, de définir son schéma de données et de réaliser les opérations CRUD.