Aufgabe 17 - ORM und Migrationen mit Prisma
Aufgabe 17 - ORM und Migrationen mit Prisma
Abschnitt betitelt „Aufgabe 17 - ORM und Migrationen mit Prisma“Worum geht es?
Abschnitt betitelt „Worum geht es?“In dieser Übung verbinden Sie ein Next.js-Projekt mit Ihrer laufenden PostgreSQL-Datenbank über das ORM Prisma. Der Schwerpunkt liegt nicht auf Next.js oder Prisma selbst, sondern auf den Datenbankkonzepten dahinter: Wie definiert man ein Schema im Code? Was passiert bei einer Migration genau in der Datenbank? Wie vermeidet man das N+1-Problem bei ORM-Abfragen?
Weiterführende Erklärungen zu diesen Konzepten finden Sie im Lernmaterial: Integration in Web Applications.
In dieser Übung üben Sie:
- Einrichten: Prisma in einem Next.js-Projekt installieren und mit einer laufenden Docker-PostgreSQL-Datenbank verbinden.
- Modellieren: Ein Datenbankschema in der Prisma-Schemadatei definieren und die Beziehungen zwischen Tabellen beschreiben.
- Migrieren: Schemaänderungen als versionierte Migrationsdateien erzeugen, anwenden und in der Datenbank nachvollziehen.
- Abfragen: ORM-Abfragen im Next.js Server Component schreiben und das N+1-Problem mit Eager Loading lösen.
- Beurteilen: Die Vor- und Nachteile eines ORM gegenüber direktem SQL-Zugriff in konkreten Situationen einschätzen.
Voraussetzungen
Abschnitt betitelt „Voraussetzungen“- Ihre Docker-Umgebung mit PostgreSQL läuft (siehe Materialien und Ressourcen).
- Node.js (≥ 18) und npm sind installiert.
- Sie haben pgAdmin oder
psqlzur Hand, um die Datenbank direkt zu inspizieren.
Auftrag 1 – Projekt einrichten und Prisma verbinden
Abschnitt betitelt „Auftrag 1 – Projekt einrichten und Prisma verbinden“1.1 Theorie
Abschnitt betitelt „1.1 Theorie“Beantworten Sie schriftlich, bevor Sie mit den Aufgaben beginnen:
- Was ist der Unterschied zwischen einem ORM und einem nativen Datenbanktreiber wie
pg? - Was versteht man unter einem Connection String (
DATABASE_URL)? Welche Informationen stecken darin? - Warum sollte die
DATABASE_URLniemals direkt in den Quellcode geschrieben, sondern in einer.env-Datei abgelegt werden?
1.2 Aufgaben
Abschnitt betitelt „1.2 Aufgaben“Aufgabe A – Next.js-Projekt anlegen
Abschnitt betitelt „Aufgabe A – Next.js-Projekt anlegen“Legen Sie ein neues Next.js-Projekt an (oder verwenden Sie ein vorhandenes):
npx create-next-app@latest schulblog --typescript --app --no-tailwind --no-eslint --src-dircd schulblogAufgabe B – Datenbank anlegen
Abschnitt betitelt „Aufgabe B – Datenbank anlegen“Prisma erstellt Tabellen, aber keine leere Datenbank. Legen Sie die Datenbank schulblog manuell an.
psql -h localhost -U pgadmin -d postgresCREATE DATABASE schulblog;\qVerbinden Sie sich mit dem Server, klicken Sie mit der rechten Maustaste auf Databases → Create → Database, geben Sie schulblog ein und speichern Sie.
Aufgabe C – Prisma installieren und initialisieren
Abschnitt betitelt „Aufgabe C – Prisma installieren und initialisieren“npm install prisma @prisma/clientnpx prisma initPrisma legt folgende Dateien an:
Ordnerprisma/
- schema.prisma
- .env
Öffnen Sie .env und passen Sie die DATABASE_URL an Ihre Docker-Konfiguration an:
DATABASE_URL="postgresql://pgadmin:postgres-root-password@localhost:5432/schulblog"1.3 Reflexion
Abschnitt betitelt „1.3 Reflexion“- Öffnen Sie
prisma/schema.prisma. Welche Einstellungen hat Prisma bereits vorgegeben? Was bedeutetprovider = "postgresql"? - Was passiert, wenn Sie
DATABASE_URLmit falschen Zugangsdaten befüllen? Wann würde der Fehler auffallen?
Auftrag 2 – Schema definieren und erste Migration
Abschnitt betitelt „Auftrag 2 – Schema definieren und erste Migration“2.1 Theorie
Abschnitt betitelt „2.1 Theorie“Beantworten Sie schriftlich:
- Was ist eine Datenbankmigrierung und warum ist es wichtig, Migrationen als Dateien in der Versionsverwaltung zu speichern?
- Was ist der Unterschied zwischen
prisma migrate devundprisma migrate deploy? In welcher Umgebung verwendet man welchen Befehl? - Was passiert in der Datenbank, wenn Sie eine neue Spalte zum Prisma-Schema hinzufügen und anschließend eine Migration ausführen?
2.2 Aufgaben
Abschnitt betitelt „2.2 Aufgaben“Aufgabe A – Schema definieren
Abschnitt betitelt „Aufgabe A – Schema definieren“Öffnen Sie prisma/schema.prisma und ersetzen Sie den Platzhalterinhalt durch folgendes Schema:
generator client { provider = "prisma-client-js"}
datasource db { provider = "postgresql" url = env("DATABASE_URL")}
model User { id Int @id @default(autoincrement()) email String @unique name String posts Post[] createdAt DateTime @default(now())}
model Category { id Int @id @default(autoincrement()) name String @unique posts Post[]}
model Post { id Int @id @default(autoincrement()) title String content String? published Boolean @default(false) author User @relation(fields: [authorId], references: [id]) authorId Int category Category @relation(fields: [categoryId], references: [id]) categoryId Int createdAt DateTime @default(now())}Halten Sie fest:
- Welche Beziehungstypen (1:N, N:M …) existieren zwischen den drei Modellen?
- Was bedeutet
@relation(fields: [authorId], references: [id])? Welche SQL-Konstruktion steckt dahinter? - Was bewirkt
@uniqueauf dememail-Feld?
Aufgabe B – Erste Migration ausführen
Abschnitt betitelt „Aufgabe B – Erste Migration ausführen“npx prisma migrate dev --name initPrisma erstellt eine neue Migrationsdatei und wendet sie sofort auf die lokale Datenbank an.
Kontrollieren Sie das Ergebnis auf zwei Wegen:
1. In der Migrationsdatei:
Ordnerprisma/
Ordnermigrations/
Ordner20xxxxxx_init/
- migration.sql
Öffnen Sie migration.sql. Welche SQL-Befehle hat Prisma generiert? Entsprechen die Spaltentypen und Constraints Ihren Erwartungen?
2. Direkt in der Datenbank:
psql -h localhost -U pgadmin -d schulblog-- Welche Tabellen wurden angelegt?\dt
-- Struktur der Post-Tabelle prüfen\d "Post"Verbinden Sie sich mit der Datenbank schulblog und navigieren Sie zu Schemas → public → Tables.
Dokumentieren Sie:
- Welche Tabellen existieren jetzt in der Datenbank?
- Hat Prisma automatisch Fremdschlüssel-Constraints angelegt? Sehen Sie diese in
\d "Post"?
2.3 Reflexion
Abschnitt betitelt „2.3 Reflexion“- Die generierte
migration.sqlenthält reinen SQL-Code. Was wäre der Nachteil, wenn Sie diesen SQL direkt inpsqlausführen würden, stattprisma migrate devzu verwenden? - Warum ist es wichtig, die Migrationsdateien im Git-Repository zu committen?
Auftrag 3 – Schema erweitern: zweite Migration
Abschnitt betitelt „Auftrag 3 – Schema erweitern: zweite Migration“Anforderungen ändern sich. Der Schulblog soll nun anzeigen, wann ein Post zuletzt bearbeitet wurde.
3.1 Aufgaben
Abschnitt betitelt „3.1 Aufgaben“Aufgabe A – Neues Feld hinzufügen
Abschnitt betitelt „Aufgabe A – Neues Feld hinzufügen“Fügen Sie dem Modell Post in schema.prisma ein neues optionales Feld hinzu:
model Post { // ... vorhandene Felder ... updatedAt DateTime? @updatedAt}@updatedAt ist eine Prisma-spezifische Annotation: Prisma setzt diesen Wert automatisch auf den aktuellen Zeitpunkt, sobald ein Datensatz aktualisiert wird.
Aufgabe B – Migration erstellen und anwenden
Abschnitt betitelt „Aufgabe B – Migration erstellen und anwenden“npx prisma migrate dev --name add_updated_at_to_postÖffnen Sie die neue migration.sql:
- Welchen SQL-Befehl hat Prisma generiert?
- Warum ist das Feld
NULLABLE(keinNOT NULL)? Überlegen Sie: Was wäre passiert, wenn Sie einNOT NULL-Feld ohne Standardwert zu einer Tabelle hinzufügen würden, die bereits Daten enthält?
Aufgabe C – Migrationsverlauf überprüfen
Abschnitt betitelt „Aufgabe C – Migrationsverlauf überprüfen“Prisma protokolliert alle angewandten Migrationen in einer eigenen Tabelle:
SELECT migration_name, finished_atFROM "_prisma_migrations"ORDER BY finished_at;Führen Sie diese Abfrage in psql oder pgAdmin aus. Welche Einträge sehen Sie? Was sagt finished_at aus?
3.2 Reflexion
Abschnitt betitelt „3.2 Reflexion“- Was passiert, wenn ein Teammitglied eine Migrationsdatei editiert, nachdem sie bereits in der Datenbank angewendet wurde? Probieren Sie es aus: Ändern Sie einen Buchstaben in
migration.sqlund führen Sie erneutnpx prisma migrate devaus. Was meldet Prisma? - Wann sollte man
prisma migrate resetverwenden, und warum ist dieser Befehl in einer Produktionsdatenbank gefährlich?
Auftrag 4 – ORM-Abfragen im Server Component
Abschnitt betitelt „Auftrag 4 – ORM-Abfragen im Server Component“4.1 Theorie
Abschnitt betitelt „4.1 Theorie“Beantworten Sie schriftlich:
- Was ist das N+1-Problem? Beschreiben Sie es an einem konkreten Beispiel aus diesem Übungsblatt (Posts und Autoren).
- Was ist der Unterschied zwischen
includeundselectin einer Prisma-Abfrage? - Warum ist es wichtig, einen globalen Prisma-Client-Singleton zu verwenden, statt bei jeder Anfrage
new PrismaClient()aufzurufen?
4.2 Aufgaben
Abschnitt betitelt „4.2 Aufgaben“Aufgabe A – Prisma Client Singleton anlegen
Abschnitt betitelt „Aufgabe A – Prisma Client Singleton anlegen“Erstellen Sie die Datei src/lib/prisma.ts:
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
export const prisma = globalForPrisma.prisma ?? new PrismaClient({ log: ['query'] });
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;Aufgabe B – Testdaten einfügen
Abschnitt betitelt „Aufgabe B – Testdaten einfügen“Bevor Sie Abfragen schreiben, brauchen Sie Daten. Erstellen Sie die Datei prisma/seed.ts:
import { PrismaClient } from '@prisma/client';const prisma = new PrismaClient();
async function main() { const anna = await prisma.user.create({ }); const max = await prisma.user.create({ });
const tech = await prisma.category.create({ data: { name: 'Technologie' } }); const science = await prisma.category.create({ data: { name: 'Wissenschaft' } });
await prisma.post.createMany({ data: [ { title: 'KI im Klassenzimmer', content: '…', published: true, authorId: anna.id, categoryId: tech.id }, { title: 'Quantencomputer', content: '…', published: true, authorId: anna.id, categoryId: science.id }, { title: 'Linux für Einsteiger', content: '…', published: false, authorId: max.id, categoryId: tech.id }, { title: 'Schwarze Löcher', content: '…', published: true, authorId: max.id, categoryId: science.id }, ], });}
main() .catch(console.error) .finally(() => prisma.$disconnect());Fügen Sie in package.json hinzu:
"prisma": { "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"}Installieren Sie ts-node und führen Sie den Seed aus:
npm install -D ts-nodenpx prisma db seedÜberprüfen Sie in psql oder pgAdmin, ob die Daten korrekt eingefügt wurden.
Aufgabe C – Das N+1-Problem beobachten
Abschnitt betitelt „Aufgabe C – Das N+1-Problem beobachten“Erstellen Sie src/app/blog/page.tsx mit dieser absichtlich fehlerhaften Implementierung:
import { prisma } from '@/lib/prisma';
export default async function BlogPage() { // 1 Abfrage: alle Posts laden const posts = await prisma.post.findMany({ where: { published: true } });
return ( <main> <h1>Schulblog</h1> {await Promise.all(posts.map(async (post) => { // N Abfragen: für jeden Post separat den Autor laden const author = await prisma.user.findUnique({ where: { id: post.authorId } }); return ( <article key={post.id}> <h2>{post.title}</h2> <p>von {author?.name}</p> </article> ); }))} </main> );}Starten Sie den Entwicklungsserver:
npm run devRufen Sie http://localhost:3000/blog auf und beobachten Sie die Terminal-Ausgabe (dort loggt Prisma alle SQL-Abfragen).
Dokumentieren Sie:
- Wie viele SQL-Abfragen wurden ausgeführt?
- Welche Abfragen wiederholen sich?
- Wie verändert sich die Anzahl der Abfragen, wenn Sie einen fünften Post hinzufügen?
Aufgabe D – N+1-Problem lösen
Abschnitt betitelt „Aufgabe D – N+1-Problem lösen“Ersetzen Sie die Abfrage in page.tsx durch eine einzige Abfrage mit Eager Loading:
const posts = await prisma.post.findMany({ where: { published: true }, include: { author: { select: { name: true } }, category: { select: { name: true } }, }, orderBy: { createdAt: 'desc' },});Passen Sie das JSX entsprechend an (kein separater author-Aufruf mehr nötig).
Laden Sie die Seite neu und beobachten Sie die Terminal-Ausgabe:
- Wie viele SQL-Abfragen werden jetzt ausgeführt?
- Wie sieht die generierte SQL-Abfrage aus? Finden Sie den
JOIN?
Aufgabe E – Selektives Laden
Abschnitt betitelt „Aufgabe E – Selektives Laden“Nicht immer braucht man alle Felder. Schreiben Sie eine Abfrage, die nur title und den name des Autors lädt — keine anderen Felder:
const posts = await prisma.post.findMany({ where: { published: true }, select: { title: true, author: { select: { name: true } }, },});Vergleichen Sie die generierte SQL-Abfrage mit der aus Aufgabe D. Was hat sich verändert?
4.3 Reflexion
Abschnitt betitelt „4.3 Reflexion“- Warum ist das N+1-Problem in der Eloquent/Laravel-Welt besonders tückisch, verglichen mit Prisma? (Hinweis: Lazy Loading)
- Können Sie sich eine Situation vorstellen, in der eine separate Abfrage sinnvoller wäre als
include? - Was würde passieren, wenn in einer Produktionsanwendung mit 10.000 Posts das N+1-Problem unbemerkt bleibt?
Erstellen Sie ein Word- oder PDF-Dokument mit:
- Den schriftlichen Antworten auf alle Theorie- und Reflexionsfragen.
- Screenshots der generierten
migration.sql-Dateien (Aufträge 2 und 3). - Screenshot der Tabellenstruktur in pgAdmin oder
psqlnach der ersten Migration. - Screenshot der Prisma-Migrations-Tabelle (
_prisma_migrations) nach beiden Migrationen. - Screenshot der Terminal-Ausgabe mit dem N+1-Problem (Aufgabe C) und nach der Lösung (Aufgabe D) — die SQL-Abfragen müssen sichtbar sein.
- Den fertigen Code von
src/app/blog/page.tsx(Aufgabe D).
Wissens-Check
Abschnitt betitelt „Wissens-Check“-
Sie haben eine bestehende Prisma-Migration eingecheckt, die bereits auf drei Entwicklungsrechnern angewendet wurde. Nun fällt Ihnen ein Tippfehler in einem Spaltennamen auf. Dürfen Sie die Migrationsdatei direkt editieren? Begründen Sie Ihre Antwort und beschreiben Sie das korrekte Vorgehen.
-
Ein Kollege schreibt folgenden Code und fragt sich, warum die Anwendung bei 50 Produkten so langsam ist. Erklären Sie das Problem und korrigieren Sie den Code:
const categories = await prisma.category.findMany();for (const cat of categories) {const posts = await prisma.post.findMany({ where: { categoryId: cat.id } });console.log(`${cat.name}: ${posts.length} Posts`);} -
Nennen Sie zwei Situationen, in denen man trotz ORM auf direktes SQL (
prisma.$queryRaw) zurückgreifen sollte.
HTL Villach, Schuljahr 2025-2026, https://www.htl-villach.at