2. Prisma
2. Prisma
Tijdens deze les breiden we het lesvoorbeeld van de vorige les uit met een database. Via Prisma, een TypeScript ORM, maken we connectie met een relationele en document database.
Startbestanden
Database server
Om persistentie toe te voegen hebben we nood aan een database. Doorheen deze cursus zullen we gebruik maken van Docker om een database server te starten.
Docker is een platform waarmee infrastructuur eenvoudig gereproduceerd kan worden op verschillende systemen aan de hand van containers. Deze containers zijn een geïsoleerde omgeving waarin enkel het absoluut noodzakelijke geïnstalleerd is. In dit geval betekent absoluut noodzakelijk dat enkel die dingen die nodig zijn om de software in de container te draaien.
De configuratie van de containers kan eenvoudig beschreven worden in een Docker Compose bestand. Zo is een volledige omgeving snel en eenvoudig reproduceerbaar op verschillende machines.
Voor de installatie van Docker verwijzen we door naar de sectie over de development environment.
Eens Docker correct geïnstalleerd is, kunnen we de database server starten door onderstaand commando uit te voeren in de root map van de startbestanden. In deze map is een bestand met de naam compose.yaml te vinden waarin de configuratie genoteerd is voor de verschillende servers die nodig zijn om het voorbeeld uit te voeren.
docker compose upNadat dit commando succesvol uitgevoerd is, zijn volgende servers beschikbaar:
- PostgreSQL, een gratis open-source en gemakkelijk uitbreidbare database server. In tegenstelling tot een klassieke relationele database biedt Postgres ondersteunen voor (multidimensionale) array kolommen, JSON kolommen en veel meer.
- pgAdmin, een grafische interface waarmee de inhoud van de PostgreSQL database bekeken kan worden.
We gebruiken de visuele interface niet rechtstreeks in de lessen, maar deze kunnen eventueel wel nuttig zijn om te controleren of dat je code correct werkt (in de oefeningen). Bekijk de appendix voor meer informatie over de werking van de webinterfaces.
Installatie van Prisma
Prisma is een TypeScript ORM-tool waarmee we op basis van een schema file een database kunnen opbouwen. Aan de hand van deze schema's (en de wijzigingen daarin) kunnen migrations opgebouwd worden om de wijzigingen in de database reproduceerbaar te maken. Daarnaast kunnen er op basis van de schema's ook TypeScript interfaces gegenereerd worden.
Prisma kan geïnstalleerd worden via onderstaand commando.
Vervolgens moet Prisma geïnitialiseerd worden. Onderstaand commando maakt een nieuwe map /prisma aan waarin het schema en de migrations bewaard zullen worden.
Binnen de nieuwe map is het bestand schema.prisma aangemaakt, hierin is te zien dat Prisma de database-URl uitleest uit de environment variables die gedefinieerd zijn in .env. Het vorige commando heeft de .env file al gegenereerd, maar de inhoud moet nog aangepast worden zodat we verbinding kunnen maken met de Postgres database in de Docker container.
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}DATABASE_URL="postgresql://postgres:postgresPassword@localhost:5432/postgres?schema=public"Schema aanmaken
Nu dat de configuratie gedaan is, kunnen we het model dat we in les 1 manueel aangemaakt hebben omvormen naar een Prisma schema.
Op de eerste lijn gebruiken we willekeurig gegenereerd UUID als primary key. Het is natuurlijk ook mogelijk om een integer te gebruiken, maar aangezien de API gebouwd is met string ID's, is het gemakkelijker om hier ook string ID's te gebruiken.
model Campus {
id String @id @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
name String @db.VarChar(255)
location String @db.VarChar(255)
address String @db.VarChar(255)
image String @db.VarChar(255)
}Migrations
Database migrations zijn een manier om wijzigingen in de database reproduceerbaar te maken en te delen met andere. Een migration beschrijft dikwijls een kleine wijziging, incrementele wijziging in de database. Meestal zijn deze wijzigingen ook terug te draaien. Elke wijziging wordt bewaard in een plain text bestand en kan dus eenvoudig toegevoegd worden aan version control.
Binnen prisma worden migrations opgebouwd aan de hand van schema.prisma, momenteel is er slechts ondersteuning voor één schema bestand en moeten we de volledige database daarin definiëren[1].
Om een migration uit te voeren kan het onderstaande commando gebruikt worden. Nadat de migrations succesvol afgerond zijn, zullen eventuele seeds scrips ook uitgevoerd worden, maar enkel op voorwaarde dat de database gerest wordt als deel van de migration.
Natuurlijk is het ook mogelijk om het commando toe te voegen aan package.json, in dat geval kan het commando als volgt opgeroepen worden.
{
"name": "mobile_lecture2_example",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
...
"migrate": "prisma migrate dev"
},
"dependencies": {
...
},
"devDependencies": {
...
}
}Als één van bovenstaande commando's gebruiken krijgen we onderstaande uitvoer te zien. Er wordt gevraagd naar de naam voor de nieuwe migration, aangezien dit de eerste migration is, gebruiken we hieronder de naam 'initial-migration'.
PS api> pnpm migrate
> mobile_lecture2_example@1.0.0 migrate C:\Projects\materiaal-javascript-leerlijn\...
> prisma migrate dev
Environment variables loaded from .env
Prisma schema loaded from prisma\schema.prisma
Datasource "db": PostgreSQL database "postgres", schema "public" at "localhost:5432"
? Enter a name for the new migration: » initial-migrationDe gegenereerde SQL-code is nu ook terug te vinden in de Prisma folder.

Merk op dat Prisma onderstaande lijn uitgeprint heeft:
✔ Generated Prisma Client (v5.4.2) to .\node_modules\.pnpm\@prisma+client@5.4.2_prisma@5.4.2\node_modules\@prisma\client in 59msTelkens dat er een migration uitgevoerd is, genereert Prisma TypeScript bindings voor de database en alle bijhorende methodes (create, read, update, delete). Daarnaast worden ook de models gegenereerd, deze kunnen vervolgens geïmporteerd worden uit @prisma/client.
Als je een bestaand Prisma project wilt openen op een nieuwe machine moet deze informatie opnieuw gegenereerd worden, dit kan via onderstaand commando. Natuurlijk is dit niet altijd nodig, als je met een lokale database werkt tijdens development, kan je natuurlijk gewoon de migration uitvoeren.
Shadow database
Als we de inhoud van de database bekijken zien we dat er twee databases aangemaakt zijn. Hieronder is het ERD te zien voor de twee aangemaakte tabellen.

De tabel _prisma_migrations wordt ook wel de shadow database genoemd en bevat alle informatie die nodig is om migrations terug te draaien naar een vorig punt en om schema drift te detecteren. Deze database is natuurlijk niet nodig in production, daarom kan je best het onderstaande commando gebruiken als je de applicatie publiceert in een productieomgeving.
Seeding
Alhoewel het mogelijk is om te beginnen programmeren met een lege database, is het dikwijls handiger als er al wat data beschikbaar is. Dit kan gaan om belangrijke data die altijd beschikbaar moet zijn, zoals een initiële administrator account, of om dummy data die enkel belangrijk is tijdens development, zoals automatisch gegenereerde klantgegeven.
Prisma kan gebruikt worden om zulke data automatisch toe te voegen tijdens een migration reset of als we het seed commando manueel uitvoeren. Een reset is dikwijls nodig als je wisselt tussen branches, elke branch kan tenslotte wijzigingen aan het schema vereisen.
Om Prisma deze data te laten genereren moet er natuurlijk wel een seed-script aangemaakt worden. Vervolgens moet het commando om dit script uit te voeren toegevoegd worden aan package.json. Tenslotte kunnen we het script dan uitvoeren.
De startbestanden bevatten reeds twee scripts, seed.ts en seedDev.ts, deze moeten in de map /prisma geplaatst worden. In seedDev.ts definiëren we de data die toegevoegd moet worden en in seed.ts controleren we de omgeving waarin de code draait, waarna we het gepaste script (prod of dev) uitvoeren. Omdat we deze applicatie nooit zullen publiceren in een productieomgeving en het onderscheid hier enkel maken ter illustratie, wordt hetzelfde script gebruikt voor prod als dev.
We bespreken de inhoud van deze seed scripts niet, doorheen de les zien we alle theorie die nodig is om deze scripts te begrijpen. We gebruiken de scripts hier om de campussen toe te voegen en zo de API endpoints gemakkelijker te testen.
Na elke aanpassing die we tijdens deze les doen aan het schema, kan je het seed script opnieuw uitvoeren, zo worden de nieuwe tabellen steeds opgevuld met enkele records. Let op, we voeren enkel records in waarvoor de foreign key verwijst naar de campus Geel, Lier of Turnhout verwijzen.
{
"name": "mobile_lecture2_example",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
...
},
"prisma": {
"seed": "tsx ./prisma/seed.ts"
},
"dependencies": {
...
},
"devDependencies": {
...
}
}Deze scripts kunnen op twee manieren uitgevoerd worden. Het eerste commando voert enkel de scripts uit, het tweede commando verwijderd alles in de database, reset naar de laatste migration en voert tenslotte het seed script uit. Dit laatste commando is vooral interessant als je moet wisselen tussen branches (en dus migration history).
Prisma gebruiken
Hieronder bespreken we hoe elke CRUD-operatie geïmplementeerd kan worden met Prisma. We bouwen de campus API eerst om zodat Prisma gebruikt wordt. Vervolgens voegen we een tabel Room toe en illustreren zo het gebruik van een één-op-veel relatie. Tenslotte voegen we een tabel Program toe en illustreren zo een veel-op-veel relatie.
Single Table
De Campus tabel is reeds aangemaakt, we moeten dus enkel de CRUD-operaties implementeren.
Begrip: Dynamische opgebouwde Prisma API
De Prisma API (hier gebruikt in de betekening "verzameling methodes die aangeboden worden door een library"), is grotendeels dynamisch opgebouwd op basis van het schema.
Telkens er een migration uitgevoerd wordt, of het pnpm exec prisma generate commando gebruikt wordt, wordt de Prisma API opnieuw opgebouwd.
Omdat de API dynamisch opgebouwd is, bevat elke instantie van PrismaClient properties die overeenkomen met de tabellen in de database.
import {PrismaClient} from '@prisma/client'
const prisma = new PrismaClient()
const newFooRow = prisma.foo.someCrudMethod(...)Create
We gebruiken de create methode om een nieuwe campus aan te maken, omdat de Prisma API dynamisch is, kunnen we dus campus.create gebruiken.
Natuurlijk moeten de methodes aangepast worden naar asynchrone methodes omdat we nu niet langer in memory data gebruiken, maar communiceren met een database.
Omdat de database slechts een gelimiteerd aantal verbindingen toestaat en omdat elke open verbinding geheugen gebruikt is het best als er maar één verbinding gebruikt wordt. Daarom zonderen we de code die verbinding maakt met de database af in een aparte file. Node bewaard elke geïmporteerde module in de cache, als we de client dan gebruiken in verschillende bestanden, wordt de versie uit de cache gebruikt. Zo wordt dezelfde verbinding met de database steeds herbruikt.
Begrip: Prisma Create
Via de create en createMany methodes kunnen we respectievelijk één of meerdere rijen aangemaakt worden.
import {PrismaClient} from '@prisma/client'
const prisma = new PrismaClient()
const newFooRow = prisma.foo.create({
data: {
// ... Data
},
})
await prisma.$disconnect()import {PrismaClient} from '@prisma/client'
const prisma = new PrismaClient()
const {count: numberOfInsertedRows} = prisma.foo.createMany({
data: [
{
// ... Data
},
{
// ... Data
},
],
})
await prisma.$disconnect():::
Merk op dat we het Campus type importeren uit @prisma/client, dit model is dus ook automatisch gegeneerd door Prisma.
import prismaClient from './prismaClient.js'
import {Campus} from '@prisma/client'
export async function addCampus(campus: IdOptional<Campus>): Promise<Campus> {
return prismaClient.campus.create({
data: campus,
})
}campuses.post('/campus', async (req: Request<void, ICampus, IdOptional<ICampus>>, res: Response) => {
const {name, location, address, image} = req.body
if (!name || name.length === 0 || !location || location.length === 0 || !address || address.length === 0 || !image || image.length === 0) {
res.statusCode = 400
res.send('Invalid data, please modify the request and try again.')
}
const newCampus = await addCampus({name, location, address, image} as ICampus)
res.statusCode = 201
res.send(newCampus)
})import {PrismaClient} from '@prisma/client'
const prismaClient = new PrismaClient()
export default prismaClientRead
Begrip: Prisma Read
Prisma bevat verschillende methodes om data uit te lezen, er zijn methodes beschikbaar om één of meerdere rijen op te halen en om deze te filteren.
Het is optioneel mogelijk om aan te geven welke kolommen teruggegeven moeten. Alle methodes behalve diegene
import {PrismaClient} from '@prisma/client'
const prisma = new PrismaClient()
const oneFoo = prisma.foo.findUnique({
where: { someUniqueProperty: 'someValue'},
// Select is optioneel, als je dit meegeeft moet je alle
// properties meegeven die je nodig hebt.
// Als de parameter ontbreekt worden alle kolommen teruggeven.
select: {
// ...
}
})
const oneFoo2 = prisma.foo.findUniqueOrThrow({
where: { someUniqueProperty: 'someValue'},
select: {}, // Optioneel
})
const firstMatchingFoo = prisma.findFirst({
where: {}, // Optioneel
select: {}, // Optioneel
})
const allMatchingFoos = prisma.findMany({
where: {}, // Optioneel
select: {}, // Optioneel
orderBy: { // Optioneel
someColumn: 'asc' // 'desc' is ook geldig.
}
})import prismaClient from './prismaClient.js'
import {Campus} from '@prisma/client'
export async function getCampuses(): Promise<Campus[]> {
return prismaClient.campus.findMany({
orderBy: {
name: 'asc',
},
})
}campuses.get('/campus', async (_: Request, res: Response) => {
const campuses = await getCampuses()
res.send(campuses)
})Natuurlijk moet het ook mogelijk zijn om één campus op te halen, hiervoor gebruiken we de findUnique methode.
import prismaClient from './prismaClient.js'
import {Campus} from '@prisma/client'
export async function getCampus(id: string): Promise<Campus | null> {
return prismaClient.campus.findUnique({
where: {
id,
},
})
}campus.get('/:campusId', async (req: Request<RouteParams>, res: Response) => {
const campus = await getCampus(req.params.campusId)
if (campus) {
res.send(campus)
} else {
// Bad Request
res.sendStatus(400)
}
})Update
Begrip: Prisma Update
Via de update methode kunnen we via Prisma één rij aanpassen. Het is, via deze methode, onmogelijk om meerdere rijen tegelijkertijd aan te passen, daarvoor bestaat de updateMany methode.
import {PrismaClient} from '@prisma/client'
const prisma = new PrismaClient()
const newFooRow = prisma.foo.update({
where: { someUniqueProperty: 'someValue'},
data: {
// ... Data
},
select: {}, // Optioneel
})
await prisma.$disconnect()import {PrismaClient} from '@prisma/client'
const prisma = new PrismaClient()
const {count: numberOfUpdatedRows} = prisma.foo.updateMany({
where: { someProperty: 'someValue'},
data: {
// ... Data
},
})
await prisma.$disconnect():::
import prismaClient from './prismaClient.js'
import {Campus} from '@prisma/client'
export async function updateCampus(id: string, newValues: Partial<Campus>): Promise<Campus> {
delete newValues.id
return prismaClient.campus.update({
where: {
id,
},
data: {
...newValues,
},
})
}campus.put('/:campusId', async (req: Request<RouteParams, Campus, IdOptional<Campus>>, res: Response) => {
const campus = await updateCampus(req.params.campusId, req.body)
if (!campus) {
res.sendStatus(400)
} else {
res.send(campus)
}
})Delete
Begrip: Prisma Delete
Via de delete en deleteMany methodes kunnen we respectievelijk één of meerdere rijen verwijderen.
import {PrismaClient} from '@prisma/client'
const prisma = new PrismaClient()
const deletedFooRow = prisma.foo.delete({
where: { someUniqueProperty: 'someValue'},
})
await prisma.$disconnect()import {PrismaClient} from '@prisma/client'
const prisma = new PrismaClient()
const {count: numberOfDeletedRows} = prisma.foo.deleteMany({
where: { someUniqueProperty: 'someValue'},
})
await prisma.$disconnect():::
import prismaClient from './prismaClient.js'
export async function deleteCampus(id: string): Promise<void> {
return prismaClient.campus.delete({
where: {
id,
},
})
}campus.delete('/:campusId', async (req: Request<RouteParams>, res: Response) => {
await deleteCampus(req.params.campusId)
res.sendStatus(200)
})Eén-op-veel relaties
Het schema uitbreiden met een nieuwe tabel gebeurd op exact dezelfde manier als waarop de tabel Campus gedefinieerd is.
model Campus {
id String @id @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
name String @db.VarChar(255)
location String @db.VarChar(255)
address String @db.VarChar(255)
image String @db.VarChar(255)
}
model Room {
id String @id @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
name String @unique @db.VarChar(255)
capacity Int
}Bovenstaande code bevat natuurlijk nog geen relaties, een campus bevat meerdere lokalen en een lokaal bevindt zich in exact één campus. Om deze relatie te definiëren kunnen we gebruik maken van de prisma plugin. Zodra we in het model Campus een verwijzing naar de tabel Room definiëren en het bestand opslaan, wordt de relatie automatisch gebouwd door de plug-in. Omdat een campus meerdere lokalen bevat definiëren we dus een array van lokalen in het Campus model.
model Campus {
id String @id @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
name String @db.VarChar(255)
location String @db.VarChar(255)
address String @db.VarChar(255)
image String @db.VarChar(255)
rooms Room[]
}
model Room {
id String @id @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
name String @unique @db.VarChar(255)
capacity Int
Campus Campus? @relation(fields: [campusId], references: [id])
campusId String? @db.Uuid
}Merk op dat de automatisch gegenereerde code de property Campus op lijn 14 met een hoofdletter geschreven heeft en dat zowel de navigation property Campus als de foreign key campusId optioneel gemarkeerd zijn. Dit moet natuurlijk aangepast wordt.
model Room {
id String @id @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
name String @unique @db.VarChar(255)
capacity Int
campus Campus @relation(fields: [campusId], references: [id])
campusId String @db.Uuid
}Nadat we de migration uitgevoerd hebben (via pnpm migrate), ziet het datamodel er als volgt uit.

Joins
Stel we willen voor een campus alle lokalen weergeven, we kunnen dit natuurlijk oplossen met twee queries. In de eerste query wordt de campus opgehaald en vervolgens, in de tweede query, de lokalen voor de campus.
We kunnen dit natuurlijk ook met joins oplossen. Het is echter geen goed idee om er van uit te gaan dat alles in één request opgehaald wordt, dit zou betekenen dat de gebruiker altijd alle lokalen voor een campus moet ophalen, ook als deze niet nodig zijn voor de applicatie. Het is beter om een optionele query parameter toe te voegen aan de GET /campus en GET /campus/:id routes waarmee we kunnen aangeven dat de lokalen ingeladen moeten worden.
Wat betreft update en create, is het geen goed idee om de tabel Campus en Room met één prisma opdracht aan te passen. Prisma biedt hier wel ondersteuning voor, maar elk van deze operatie vereist dat we weten welke lokalen verwijderd, aangepast of toegevoegd moeten worden. De gebruiker van de API zou deze data dus moeten doorsturen in één ingewikkeld request. Het is gemakkelijker om dit af te zonderen in aparte requests, zowel vanuit het oogpunt van de API ontwikkelaar als de programmeur die de API moet gebruiken in applicatie.
Lokalen ophalen
We voorzien twee manieren om de lokalen op te halen. Ten eerste voegen we een query parameter toe aan de route GET /campus, vervolgens bouwen we de route GET /campus/:id/room uit om alle lokalen voor een specifieke campus op te halen.
GET /campus
Hierbij is het belangrijk om op te merken dat deze parameter steeds doorgegeven wordt als een string, we moeten dus zelf de conversie naar een boolean uitvoeren.
Vervolgens geven we deze parameter mee aan de getCampuses methode die alle campussen ophaalt. Binnen deze methode kan deze parameter dan gebruikt worden om de lokalen eventueel mee op te halen.
interface IncludeParams {
includeRooms?: string
}
campuses.get('/campus', async (req: Request<void, Campus[], void, IncludeParams>, res: Response) => {
const {includeRooms} = req.query
const campuses = await getCampuses(includeRooms?.toLowerCase() === 'true')
res.send(campuses)
})export async function getCampuses(includeRooms = false): Promise<Campus[]> {
return prismaClient.campus.findMany({
orderBy: {
name: 'asc',
},
include: {
rooms: includeRooms,
},
})
}Als we deze queries uitvoeren (op een database met informatie in (zie seeding), dan krijgen we onderstaand resultaat.


Begrip: Prisma Joins
Prisma biedt ondersteuning voor joins in elke methode die data ophaalt. De enige uitzonderingen hierop zijn de updateMany, createMany en deleteMany methodes die enkel het aantal bijgewerkte rijen teruggeven in de plaats van de aangepaste data.
Het is ook mogelijk om enkel specifieke velden toe te voegen, zoals het id op te halen via joins. In dit geval geeft je een object mee dat dezelfde structuur heeft als het object dat je meegeeft aan find of findMany.
In onderstaand voorbeeld kan de methode x dus één van de volgende zijn:
- create
- update
- delete
- find
- findUnique
- findUniqueOrThrow
- findFirst
- find
- findMany
const data = await foo.x({
// ... Parameters om data te filteren, toe te voegen, ...
include: {
// Relation is de naam van de property in het schema die
// de link legt met een andere tabel.
relation: true | false
},
})const data = await foo.x({
// ... Parameters om data te filteren, toe te voegen, ...
include: {
// Relation is de naam van de property in het schema die
// de link legt met een andere tabel.
relation: {
select: {
// Kolommen die geselecteerd moeten worden.
},
where: {
// Filtervoorwaarde
},
include: {
// Relaties die vanuit de tabel Relation beschikbaar zijn toeveogen.
},
}
},
}):::
GET /campus/:id/room
Om een lokaal aan te maken moeten we weten in welke campus dat lokaal zich bevindt, daarom nesten we de route waarmee alle lokalen opgehaald kunnen worden voor een campus onder de /campus/:id route.
We beginnen met een nieuwe file aan te maken voor de route /campus/:id/room, hierin definiëren we opnieuw een router. Dit gebeurt op een iets andere manier dan de voorheen. De route bevat een parameter die de campus aanduidt, als we de router definiëren zoals in de vorige les, wordt deze parameter niet doorgegeven. Om de parameter door te geven, moet een extra configuratie-object meegegeven worden aan de constructor van de router.
Vervolgens kunnen we de parameter gebruiken in de database call om enkel de lokalen terug te geven waar de foreign key gelijk is aan deze parameter.
Tenslotte moeten we de router dan nog importeren in de bovenliggende router file.
import express, {Request, Response} from 'express'
import {getRoomsForCampus} from '../../../../dal/rooms.js'
const rooms = express.Router({mergeParams: true})
interface RouteParams {
campusId: string
}
rooms.get('/room', async (req: Request<RouteParams>, res: Response) => {
const rooms = await getRoomsForCampus(req.params.campusId)
if (rooms) {
res.send(rooms)
} else {
// Bad Request
res.sendStatus(400)
}
})
export default roomsimport prismaClient from './prismaClient.js'
export async function getRoomsForCampus(campusId: string): Promise<Room[]> {
return prismaClient.room.findMany({
where: {
campusId,
},
orderBy: {
name: 'asc',
},
})
}import express, {Request, Response} from 'express'
import {deleteCampus, getCampus, updateCampus} from '../../../dal/campuses.js'
import IdOptional from '../../../models/IdOptional.js'
import {Campus} from '@prisma/client'
import rooms from './room/rooms.js'
const campus = express.Router()
// Zie vorige les
campus.use('/:campusId', rooms)
export default campusVeel-op-veel relaties
Als laatste uitbreiding voegen een veel-op-veel relatie toe. Op elke campus worden meerdere opleidingen gegeven en een opleiding kan op meerdere campussen gevolgd worden.
Prisma ondersteund twee manieren om deze relatie weer te geven, de expliciete en impliciete manier. Via de expliciete manier moet de associatie tabel uitdrukkelijk vermeld worden in het schema, dit is meer werk maar betekent ook dat we deze tabel rechtstreeks kunnen aanspreken via Prisma. Als we de impliciete manier gebruiken, moeten we de associatie tabel niet uitdrukkelijk aanmaken en kunnen we deze dus niet rechtstreeks aanspreken.
In het verdere verloop van deze cursus maken we gebruik van de impliciete manier, voor meer informatie over de expliciete manier verwijzen we door naar de documentatie.
Om een impliciete veel-op-veel relatie aan te maken moeten we een array toevoegen in beide tabellen. Als je gebruik maakt van de Prisma plugin voor WebStorm of Visual Studio Code mag je pas bewaren als beide array gedefinieerd zijn, anders wordt een één-op-veel relatie aangemaakt.
model Campus {
id String @id @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
name String @db.VarChar(255)
location String @db.VarChar(255)
address String @db.VarChar(255)
image String @db.VarChar(255)
rooms Room[]
programs Program[]
}
model Program {
id String @id @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
name String @unique @db.VarChar(255)
campuses Campus[]
}Nadat de migration uitgevoerd is, ziet het ERD er als volgt uit.

We bespreken de CRUD-operaties voor de opleidingen niet, dit gebeurt op dezelfde manier als de CRUD-operaties die we eerder gebouwd hebben voor de campussen en bevatten dus geen nieuwe leerstof. In de startbestanden zijn deze functionaliteiten reeds beschikbaar.
Aangezien we het model op de impliciete manier gebouwd hebben, is het niet mogelijk om de associatie tabel rechtstreeks te bewerken. In de plaats daarvan gebruiken we de update methode van Prisma, hieraan kunnen we, via de connect en disconnect attributen, respectievelijk meegeven welke veel-op-veel relaties toegevoegd of verwijderd moeten worden. Beide attributen verwachten een array van het type {id: string}[], of {id: number}[] indien we een integer als primary key gebruiken.
We bouwen eerst de dal laag uit en vervolgens definiëren we twee routes, POST /programs/[programId]/campuses en DELETE /programs/[programId]/campuses. Via de POST route worden campussen gelinkt aan een opleiding en via het DELETE request worden bestaande links verwijderd.
interface RouteParams {
programId: string
}
const campuses = express.Router({mergeParams: true})
campuses.post(
'/campuses',
async (req: Request<RouteParams, Program, {campusIds: string[]}>, res: Response<Program>) => {
const program = await addCampusToProgram(req.params.programId, req.body.campusIds)
res.send(program)
})
campuses.delete(
'/campuses',
async (req: Request<RouteParams, Program, {campusIds: string[]}>, res: Response<Program>) => {
const program = await removeCampusFromProgram(req.params.programId, req.body.campusIds)
res.send(program)
},
)
export default campusesexport async function addCampusToProgram(programId: string, campusIds: string[]): Promise<Program> {
return prismaClient.program.update({
where: {
id: programId,
},
data: {
campuses: {
connect: campusIds.map(id => ({id})),
},
},
include: {
campuses: true,
},
})
}
export async function removeCampusFromProgram(programId: string, campusIds: string[]): Promise<Program> {
return prismaClient.program.update({
where: {
id: programId,
},
data: {
campuses: {
disconnect: campusIds.map(id => ({id})),
},
},
include: {
campuses: true,
},
})
}Zoals onderstaande video aantoont, vertoont Prisma geen fouten als reeds gelinkte data opnieuw gelinkt wordt of als een niet bestaande link verbroken worden.
Voorbeeldcode
Uitgewerkt lesvoorbeeld met commentaar
Appendix: PgAdmin
Om de inhoud van de PostgreSQL database te bekijken kan je gebruik maken van PgAdmin, een webinterface die, via docker compose, opgestart wordt.
Via http://localhost:8800 kan je deze interface bekijken. Je zal moeten inloggen, dit kan via de gebruikersnaam pgAdmin@example.com en het wachtwoord postgresPassword.

Vervolgens navigeer je via het menu naar Object > Register > Server.

Geef de server vervolgens een naam in het general tabblad en gebruik onderstaande instellingen voor het connection tabblad.

Tenslotte kan je dan via de object explorer navigeren naar Servers > DATABASE_NAAM > Databases > postgres > Schemas > public > tables en zo de inhoud van een tabel bekijken of aanpassen.
