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.

Icône de calendrier
Intermédiaire
4 chapitres

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 :

copié !
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().

copié !
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 :

copié !
const variable = await prisma.{model}.{method}
  • variable stocke le retour de la méthode
  • {model} correspond au modèle à interroger (en camelCase)
  • {method} correspond à la méthode du client Prisma à appeler

Voilà un exemple concret, récupérant tous les utilisateurs de la BDD :

copié !
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.

app.js
copié !
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)
	})
  1. Importation du client Prisma
  2. Instanciation du client Prisma
  3. Définition d’une fonction asynchrone main() en charge d’exécuter nos requêtes vers la base de données
  4. Appel de la fonction main() pour exécuter les requêtes
  5. 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 :

schema.prisma
copié !
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.

app.js
copié !
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.

app.js
copié !
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é (avec cursor)
  • 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).

app.js
copié !
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).

app.js
copié !
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).

app.js
copié !
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.

app.js
copié !
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
			}
		]
	},
	// ...
]
app.js
copié !
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.

app.js
copié !
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érateurDescription
equalsVérifie une égalité =
notVérifie une différence
ltVérifie une infériorité stricte <
lteVérifie une infériorité ou égalité
gtVérifie une supériorité stricte >
gteVérifie une supériorité ou égalité

La requête suivante récupère les publications qui ont au moins 500 vues.

app.js
copié !
const posts = await prisma.post.findMany({
	where: {
		views: {
			gte: 500
		}
	}
})

Et 6 opérateurs secondaires :

OpérateurDescription
inVérifie si une valeur est dans une liste
notInVérifie si une valeur n’est pas dans une liste
containsVérifie la présence de caractères dans un ensemble
searchVérifie la présence de caractères dans un ensemble (accepte les opérateurs logiques : &, | et !). On parle de full-text-search
startsWithVérifie la présence d’une chaîne de caractères au début d’une autre
endsWithVé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.

app.js
copié !
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érateurDescription
ANDIl implique que toutes les conditions doivent être vérifiées
ORIl implique qu’au moins une condition doit être vérifiée
NOTIl 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.

app.js
copié !
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érateurDescription
someRetourne tous les enregistrements pour lesquels au moins 1 enregistrement lié vérifie un critère.
everyRetourne tous les enregistrements pour lesquels tous les enregistrements liés vérifient un critère.
noneRetourne tous les enregistrements pour lesquels aucun enregistrement lié ne vérifie un critère.
app.js
copié !
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.

app.js
copié !
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 :

ascdesc
TexteAlphabétiqueAnti alphabétique
NombreCroissantDécroissant
DateChronologiqueAnti chronologique
app.js
copié !
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 :

app.js
copié !
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
app.js
copié !
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
app.js
copié !
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.

seed.js
copié !
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.

copié !
npm install @faker-js/faker --save-dev

Exploitons ce paquet au sein de nos méthodes create() :

seed.js
copié !
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 :

package.json
copié !
{
	// ...
	"prisma": {
		"seed": "node prisma/seed.js"
	}
}

Désormais, on peut taper la commande :

copié !
npx prisma db seed