Apprendre Prisma : Manipuler la BDD avec Prisma Client
Prisma Client est un client de base de données généré automatiquement et adapté à votre schéma de base de données, permettant d'y lire et écrire des données.
Pour configurer Prisma Client et interagir avec votre base de données, le fichier 📄 prisma.schema
doit nécessairement contenir les informations de connexion à la base de données, le générateur de client à utiliser et au moins un modèle.
Installer le client
Installez Prisma Client dans votre projet avec la commande suivante :
npm install @prisma/client
Cette commande exécute également la commande prisma generate
qui génère le client Prisma dans le dossier 📂 node_modules/.prisma/client
.
Instancier le client
Pour travailler avec le client Prisma, on l’importe via require()
, puis on instancie la classe PrismaClient()
.
const { PrismaClient } = require('@prisma/client')
const prisma = new PrismaClient()
L’objet prisma
est désormais disponible afin de lire et écrire des données en base de données.
Utiliser le client
Les requêtes exécutées vers la base de données auront la forme :
const variable = await prisma.{model}.{method}
variable
stocke le retour de la méthode{model}
correspond au modèle à interroger (encamelCase
){method}
correspond à la méthode du client Prisma à appeler
Voilà un exemple concret, récupérant tous les utilisateurs de la BDD :
const users = await prisma.user.findMany({})
Attardons nous sur les principales méthodes afin d’implémenter un CRUD. Chaque méthode possédera ses propres options (au sein d’un objet JS {}
).
CRUD
Exploitons le client Prisma afin d’implémenter les opérations élémentaires d’un CRUD.
Pour exploiter le client Prisma, créons un script Node.js élémentaire 📄 app.js
à exécuter en ligne de commande.
const { PrismaClient } = require('@prisma/client')
const prisma = new PrismaClient()
async function main() {
// Exécution des requêtes via le client Prisma
}
main()
.then(async() => {
await prisma.$disconnect()
})
.catch(async(e) => {
console.error(e)
await prisma.$disconnect()
process.exit(1)
})
- Importation du client Prisma
- Instanciation du client Prisma
- Définition d’une fonction asynchrone
main()
en charge d’exécuter nos requêtes vers la base de données - Appel de la fonction
main()
pour exécuter les requêtes - Fermeture des connexions à la base de données lorsque le script se termine
Afin de rendre les exemples les plus clairs possibles, basons nous sur le schéma Prisma de démonstration suivant :
model User {
id Int @id @default(autoincrement())
firstName String?
lastName String?
email String @unique
registeredAt DateTime @default(now())
role Role @default(USER)
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
description String
content String
published Boolean @default(true)
views Int @default(0)
author User @relation(fields: [authorId], references: [id])
authorId Int
}
enum Role {
USER
ADMIN
}
Read
Pour lire des données, on exploite principalement 2 méthodes : findUnique()
et findMany()
.
Lire un élément avec findUnique()
findUnique()
(documentation) retourne un résultat unique en fonction d’un ou plusieurs critère(s) de sélection unique (tel qu’un identifiant ou une adresse email).
Cette méthode possède 3 options :
where
(obligatoire) : spécifie des critères de filtre.select
(optionnel) : spécifie quels champs récupérer.include
(optionnel) : récupère les champs d’un modèle lié.
Ici, on récupère le prénom et le nom de l’utilisateur ayant l’identifiant 64, ainsi que toutes ses publications.
const users = await prisma.user.findUnique({
where: {
id: 64,
},
select: {
firstName: true,
lastName: true,
posts: true
}
})
Lire plusieurs éléments avec findMany()
findMany()
(documentation) retourne une liste de résultats.
const users = await prisma.user.findMany({
where: {
email: {
endsWith: "@gmail.com"
}
},
select: {
firstName: true,
lastName: true,
},
orderBy: {
registeredAt: "desc"
},
take: 10
})
Cette méthode possède 8 options :
where
(optionnel) : spécifie des critères de filtre.orderBy
(optionnel) : trie les enregistrement selon une propriété.skip
(optionnel) : ignore un nombre d’éléments retournés.cursor
(optionnel) : spécifie une position de départ à partir de laquelle récupérer les enregistrements.take
(optionnel) : spécifie combien d’objets récupérer depuis le début / la fin de la liste ou le curseur s’il est spécifié (aveccursor
)select
(optionnel) : spécifie quels champs récupérer.include
(optionnel) : récupère les champs d’un modèle lié.distinct
(optionnel) : filtre les doublons pour une propriété spécifique.
Ici, on récupère le prénom et le nom des 10 derniers utilisateurs à s’être inscrits avec une adresse gmail.
Create
Pour insérer des enregistrements en base de données, on exploite la méthode create()
(documentation).
const users = await prisma.user.create({
data: {
firstName: "John",
lastName: "Doe",
email: "[email protected]"
}
})
Cette méthode possède 3 options :
data
(obligatoire) : l’objet à insérer en base de données. Les champs marqués comme facultatifs ou ayant des valeurs par défaut dans le modèle de données sont facultatifs.select
(optionnel) : spécifie les propriétés à inclure sur l’objet retourné.include
(optionnel) : spécifie les relations à construire sur l’objet retourné.
Ici, on insère en base de données un utilisateur nommé John Doe, ayant pour adresse email [email protected].
Update
Pour modifier des enregistrement en base de données, on exploite la méthode update()
(documentation).
const users = await prisma.user.update({
data: { email: '[email protected]' },
where: { id: 64 },
})
Cette méthode possède 4 options :
data
(obligatoire) : l’objet à insérer en base de données. Les champs marqués comme facultatifs ou ayant des valeurs par défaut dans le modèle de données sont facultatifs.where
(obligatoire) : spécifie des critères de filtre pour sélectionner l’objet à modifier.select
(optionnel) : spécifie les propriétés à inclure sur l’objet retourné.include
(optionnel) : spécifie les relations à construire sur l’objet retourné.
Ici, on édite en base de données l’adresse email de l’utilisateur ayant l’id 64.
Delete
Pour supprimer des enregistrements en base de données, on exploite la méthode delete()
(documentation).
const users = await prisma.user.delete({
where: { id: 64 },
})
Cette méthode possède 3 options :
where
(obligatoire) : des critères de filtre pour sélectionner l’objet à supprimer.select
(optionnel) : spécifie les propriétés à inclure sur l’objet retourné.include
(optionnel) : spécifie les relations à construire sur l’objet retourné.
Ici, on supprime en base de données l’utilisateur ayant l’id 64.
Options de requêtes principales
Cette section est dédiée à un focus plus spécifique sur les options les plus couramment utilisées lors de l’exploitation du client Prisma.
Sélection
Par défaut, une sélection avec le client Prisma retourne pour chaque enregistrement :
- ✅ Ses champs scalaires (texte, nombre, booléens…) ainsi que ses énumérations
- ❌ Aucune relation
Il est possible de spécifier cette sélection avec le mot-clé select
.
const users = await prisma.user.findMany({
select: {
firstName: true,
lastName: true,
posts: true
},
})
👀 JS en sortie
[
{ firstName: "Johnathan", lastName: "Heidenreich", posts: [] },
{ firstName: "Graham", lastName: "O'Keefe",
posts: [
{
id: 4,
title: "Veritatis rem sapiente eum eius quia quod incidunt natus.",
description: "Nostrum optio dicta unde illo rem ullam.\nSuscipit hic sint nobis nisi.",
content: "Numquam nobis debitis debitis reiciendis commodi eligendi error. Aliquid ea doloribus enim temporibus cum tempora officia nostrum. Nemo necessitatibus accusamus repellendus...",
published: false,
views: 416,
authorId: 2
}
]
},
// ...
]
const users = await prisma.user.findMany({
select: {
firstName: true,
lastName: true,
posts: {
select: {
title: true,
views: true
}
}
},
})
👀 JS en sortie
[
{ firstName: "Johnathan", lastName: "Heidenreich", posts: [] },
{ firstName: "Graham", lastName: "O'Keefe",
posts: [
{
title: "Veritatis rem sapiente eum eius quia quod incidunt natus.",
views: 416
}
]
},
// ...
]
Compter le nombre de relations avec _count
Le mot-clé _count
permet de compter combien de relations il existe entre des données de deux modèles liés.
const users = await prisma.user.findMany({
select: {
firstName: true,
lastName: true,
_count: {
select: { posts: true }
},
},
})
👀 JS en sortie
[
{ firstName: "Johnathan", lastName: "Heidenreich", _count: { posts: 0 } },
{ firstName: "Graham", lastName: "O'Keefe", _count: { posts: 1 } },
{ firstName: "Peggie", lastName: "Sawayn", _count: { posts: 3 } }
// ...
]
Filtrage
Nombreuses sont les requêtes qui nécessitent de filtrer les données.
Lecture, modification ou encore suppression d’un enregistrement… Si filtrer un élément par son identifiant est un besoin récurrent, il est aussi parfois nécessaire de filtrer un ou plusieurs éléments sur n’importe laquelle de ses propriétés.
Ce filtrage est rendu possible avec le mot-clé where
.
Opérateurs de comparaison
Les opérateurs de comparaison permettent de comparer des propriétés du modèle à des valeurs.
On distingue 6 opérateurs principaux, largement utilisés en algorithmie :
Opérateur | Description |
---|---|
equals | Vérifie une égalité = |
not | Vérifie une différence ≠ |
lt | Vérifie une infériorité stricte < |
lte | Vérifie une infériorité ou égalité ≤ |
gt | Vérifie une supériorité stricte > |
gte | Vérifie une supériorité ou égalité ≥ |
La requête suivante récupère les publications qui ont au moins 500 vues.
const posts = await prisma.post.findMany({
where: {
views: {
gte: 500
}
}
})
Et 6 opérateurs secondaires :
Opérateur | Description |
---|---|
in | Vérifie si une valeur est dans une liste |
notIn | Vérifie si une valeur n’est pas dans une liste |
contains | Vérifie la présence de caractères dans un ensemble |
search | Vérifie la présence de caractères dans un ensemble (accepte les opérateurs logiques : & , | et ! ). On parle de full-text-search |
startsWith | Vérifie la présence d’une chaîne de caractères au début d’une autre |
endsWith | Vérifie la présence d’une chaîne de caractères à la fin d’une autre |
La requête suivante récupère les utilisateurs qui ont une adresse gmail.
const users = await prisma.user.findMany({
where: {
email: {
endsWith: "@gmail.com"
}
}
})
Opérateurs logiques
Les opérateurs logiques permettent de combiner des comparaisons.
On les utilise en tant que “wrappers” pour les conditions exprimées au sein d’un where
.
Opérateur | Description |
---|---|
AND | Il implique que toutes les conditions doivent être vérifiées |
OR | Il implique qu’au moins une condition doit être vérifiée |
NOT | Il implique que toutes les conditions ne doivent pas être vérifiées |
La requête suivante récupère les publications publiées qui ont au moins 500 vues.
const posts = await prisma.post.findMany({
where: {
AND: [
{
views: {
gte: 500
}
},
{
published: {
equals: true
}
}
]
}
})
Filtres de relation
Le client Prisma permet de récupérer des enregistrements en fonction des propriétés d’enregistrements liés.
En fonction de la cardinalité de la relation, on utilisera une approche ou une autre.
Les relations multiples (n
enregistrement liés) seront filtrées avec les opérateurs some
, every
et none
.
Opérateur | Description |
---|---|
some | Retourne tous les enregistrements pour lesquels au moins 1 enregistrement lié vérifie un critère. |
every | Retourne tous les enregistrements pour lesquels tous les enregistrements liés vérifient un critère. |
none | Retourne tous les enregistrements pour lesquels aucun enregistrement lié ne vérifie un critère. |
const posts = await prisma.user.findMany({
where: {
posts: {
some: {
title: {
contains: 'JavaScript'
}
},
none: {
title: {
contains: 'PHP'
}
},
every: {
views: {
gte: 100,
}
}
}
}
})
La requête précédente renvoie les utilisateurs pour lesquels :
- Certaines (au moins une -
some
) de ses publications contiennent “JavaScript” dans le titre - Aucune (
none
) de ses publications ne contient “PHP” dans le titre - Toutes (
every
) ses publications comptabilisent au moins 100 vues
Les relations uniques (1
enregistrement lié) seront filtrées avec un opérateur de comparaison classique à la seule différence qu’on accèdera aux propriétés liées par l’intermédiaire de l’attribut relationnel.
const posts = await prisma.post.findMany({
where: {
author: {
firstName: {
equals: 'John'
}
}
}
})
La requête précédente renvoie les publications des auteurs qui ont le prénom « John ».
Tri
Il est très fréquent de devoir trier un résultat. Le tri est défini à travers l’option Prisma orderBy
. Un tri peut être ascendant ou descendant :
- ascendant : le mot-clé
asc
permet de définir un tri ascendant. - descendant : le mot-clé
desc
permet de définir un tri descendant.
Selon le type de la propriété sur laquelle on effectue le tri, asc
ou desc
est interprété différemment :
asc | desc | |
---|---|---|
Texte | Alphabétique | Anti alphabétique |
Nombre | Croissant | Décroissant |
Date | Chronologique | Anti chronologique |
const users = await prisma.user.findMany({
orderBy: {
registeredAt: 'desc'
}
})
👀 JS en sortie
[
{
id: 3,
firstName: "Peggie",
lastName: "Sawayn",
email: "[email protected]",
registeredAt: 2023-03-06T21:14:32.354Z,
role: "USER"
},
{
id: 9,
firstName: "Sydnie",
lastName: "Bernier",
email: "[email protected]",
registeredAt: 2022-03-06T21:14:32.399Z,
role: "USER"
},
// ...
{
id: 7,
firstName: "Antone",
lastName: "Nitzsche",
email: "[email protected]",
registeredAt: 2015-08-12T21:14:32.382Z,
role: "USER"
}
]
Si le tri doit être effectué sur plusieurs champs, alors orderBy
contiendra un tableau :
const users = await prisma.user.findMany({
orderBy: [
{ lastName: 'asc' },
{ firstName: 'asc' },
],
})
👀 JS en sortie
[
{
id: 9,
firstName: "Sydnie",
lastName: "Bernier",
email: "[email protected]",
registeredAt: 2022-03-06T21:14:32.399Z,
role: "USER"
},
{
id: 1,
firstName: "Johnathan",
lastName: "Heidenreich",
email: "[email protected]",
registeredAt: 2020-03-06T21:14:32.336Z,
role: "USER"
},
// ...
{
id: 6,
firstName: "Jacklyn",
lastName: "Wunsch",
email: "[email protected]",
registeredAt: 2021-12-26T21:14:32.374Z,
role: "USER"
}
]
Trier les enregistrements d'un modèle en fonction d'une propriété liée
const posts = await prisma.post.findMany({
select: {
title: true,
views: true,
author: {
select: {
lastName: true,
firstName: true
}
}
},
orderBy: [
{ author: { lastName: 'asc' } },
{ author: { firstName: 'asc' } },
{ views: 'desc' },
]
})
Ici, on affiche le titre des publications pour chaque auteur (triés alphabétiquement par leur nom / prénom). Lorsqu’il s’agit des publications du même auteur, on affiche d’abord les plus populaires (plus grand nombre de vues)
👀 JS en sortie
[
{
title: "Veritatis rem sapiente eum eius quia quod incidunt natus.",
views: 416,
author: { lastName: "O'Keefe", firstName: "Graham" }
},
// ...
{
title: "Dicta maxime reprehenderit mollitia aliquid quae repellat unde.",
views: 541,
author: { lastName: "Towne", firstName: "Myron" }
},
{
title: "Perferendis odio beatae.",
views: 104,
author: { lastName: "Towne", firstName: "Myron" }
},
{
title: "Ratione ratione officia rem.",
views: 95,
author: { lastName: "Towne", firstName: "Myron" }
},
{
title: "Autem deserunt expedita laboriosam non.",
views: 414,
author: { lastName: "Wunsch", firstName: "Jacklyn" }
}
]
Trier les enregistrements d'un modèle contenu dans un autre
const users = await prisma.user.findMany({
select: {
lastName: true,
firstName: true,
posts: {
select: {
title: true,
views: true
},
orderBy: {
views: 'desc'
},
},
},
orderBy: [
{ lastName: 'asc' },
{ firstName: 'asc' },
]
})
Ici, on affiche la liste des auteurs dans l’ordre alphabétique (nom / prénom) et pour chaque auteur le titre et le nombre de vues de ses publications. Les publications les plus vues apparaîtront en premier.
👀 JS en sortie
[
{ lastName: "Bernier", firstName: "Sydnie", posts: [] },
// ...
{
lastName: "Towne",
firstName: "Myron",
posts: [
{
title: "Saepe ad recusandae quam ducimus sit sapiente.",
views: 745
},
{
title: "Dicta maxime reprehenderit mollitia aliquid quae repellat unde.",
views: 541
},
{ title: "Perferendis odio beatae.", views: 104 },
{ title: "Ratione ratione officia rem.", views: 95 }
]
},
{ lastName: "Wunsch", firstName: "Jacklyn",
posts: [
{
title: "Autem deserunt expedita laboriosam non.",
views: 414
}
]
}
]
Seeder sa base de données
Qu’est-ce qu’un seeder ?
« Seeder » une base de données signifie insérer des données initiales dans une base de données, la plupart du temps à des fins de test.
Les « seeders » sont les scripts responsables de ces insertions de masse automatisées.
Ils sont très utiles en développement afin de :
- Regénérer une BDD dans un état initial après l’avoir dégradée, voire corrompue, suite à des modifications et suppression de données.
- S’assurer que le système fonctionne correctement avant de le mettre en production.
Les seeders peuvent également être utilisés pour insérer des données de référence dans une base de données, telles que des listes de villes, de pays ou de produits.
Créons un seeder basé sur notre schéma Prisma afin de peupler notre base de données en une ligne de commande.
Créer un seeder
Pour créer un seeder utilisant notre client Prisma, on créera dans le dossier 📁 prisma
un fichier 📄 seed.js
.
On y importe d’abord le client Prisma puis on crée notre fonction main()
, dédiée à l’exécution des scripts de peuplement de la base de données.
const { PrismaClient } = require('@prisma/client')
const prisma = new PrismaClient()
async function main() {
// Exécution des requêtes via le client Prisma
}
main()
.then(async () => {
await prisma.$disconnect()
})
.catch(async (e) => {
console.error(e)
await prisma.$disconnect()
process.exit(1)
})
Installons l’incontournable paquet fakerjs pour générer des données aléatoires.
npm install @faker-js/faker --save-dev
Exploitons ce paquet au sein de nos méthodes create()
:
const { PrismaClient } = require('@prisma/client')
const prisma = new PrismaClient()
const { faker } = require('@faker-js/faker');
async function main() {
const deletePosts = await prisma.post.deleteMany({})
const deleteUsers = await prisma.user.deleteMany({})
for (let i=1; i <= 10; i++) {
await prisma.user.create({
data: {
firstName: faker.name.firstName(),
lastName: faker.name.lastName(),
email: faker.internet.email(),
}
})
}
for (let i=1; i <= 10; i++) {
await prisma.post.create({
data: {
title: faker.lorem.lines(1),
description: faker.lorem.lines(2),
content: faker.lorem.paragraphs(5),
published: faker.datatype.boolean(),
views: faker.datatype.number({ min: 10, max: 1000 }),
authorId: faker.datatype.number({ min: 1, max: 10 })
}
})
}
}
main()
.then(async () => {
await prisma.$disconnect()
})
.catch(async (e) => {
console.error(e)
await prisma.$disconnect()
process.exit(1)
})
- Pour éviter tout conflit, vous remarquerez que nous vidons systématiquement avec
deleteMany()
les tables de la base de données avant de la repeupler. - Ensuite, nous bouclons avec
for
pour générer des données en masse. - Chaque itération de boucle engendre un appel à la méthode
create()
pour ajouter un enregistrement aléatoire.
Exécuter un seeder
Pour exécuter ce seeder, on ajoute dans le fichier 📄 package.json
la propriété suivante :
{
// ...
"prisma": {
"seed": "node prisma/seed.js"
}
}
Désormais, on peut taper la commande :
npx prisma db seed