Zum Inhalt springen

Aufgabe 17 - ORM und Migrationen mit Prisma

Zu Zen-Modus wechseln

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.

  • Ihre Docker-Umgebung mit PostgreSQL läuft (siehe Materialien und Ressourcen).
  • Node.js (≥ 18) und npm sind installiert.
  • Sie haben pgAdmin oder psql zur Hand, um die Datenbank direkt zu inspizieren.

Auftrag 1 – Projekt einrichten und Prisma verbinden

Abschnitt betitelt „Auftrag 1 – Projekt einrichten und Prisma verbinden“

Beantworten Sie schriftlich, bevor Sie mit den Aufgaben beginnen:

  1. Was ist der Unterschied zwischen einem ORM und einem nativen Datenbanktreiber wie pg?
  2. Was versteht man unter einem Connection String (DATABASE_URL)? Welche Informationen stecken darin?
  3. Warum sollte die DATABASE_URL niemals direkt in den Quellcode geschrieben, sondern in einer .env-Datei abgelegt werden?

Legen Sie ein neues Next.js-Projekt an (oder verwenden Sie ein vorhandenes):

Terminal-Fenster
npx create-next-app@latest schulblog --typescript --app --no-tailwind --no-eslint --src-dir
cd schulblog

Prisma erstellt Tabellen, aber keine leere Datenbank. Legen Sie die Datenbank schulblog manuell an.

Terminal-Fenster
psql -h localhost -U pgadmin -d postgres
CREATE DATABASE schulblog;
\q

Aufgabe C – Prisma installieren und initialisieren

Abschnitt betitelt „Aufgabe C – Prisma installieren und initialisieren“
Terminal-Fenster
npm install prisma @prisma/client
npx prisma init

Prisma 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"
  • Öffnen Sie prisma/schema.prisma. Welche Einstellungen hat Prisma bereits vorgegeben? Was bedeutet provider = "postgresql"?
  • Was passiert, wenn Sie DATABASE_URL mit 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“

Beantworten Sie schriftlich:

  1. Was ist eine Datenbankmigrierung und warum ist es wichtig, Migrationen als Dateien in der Versionsverwaltung zu speichern?
  2. Was ist der Unterschied zwischen prisma migrate dev und prisma migrate deploy? In welcher Umgebung verwendet man welchen Befehl?
  3. Was passiert in der Datenbank, wenn Sie eine neue Spalte zum Prisma-Schema hinzufügen und anschließend eine Migration ausführen?

Ö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 @unique auf dem email-Feld?
Terminal-Fenster
npx prisma migrate dev --name init

Prisma 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:

Terminal-Fenster
psql -h localhost -U pgadmin -d schulblog
-- Welche Tabellen wurden angelegt?
\dt
-- Struktur der Post-Tabelle prüfen
\d "Post"

Dokumentieren Sie:

  • Welche Tabellen existieren jetzt in der Datenbank?
  • Hat Prisma automatisch Fremdschlüssel-Constraints angelegt? Sehen Sie diese in \d "Post"?
  • Die generierte migration.sql enthält reinen SQL-Code. Was wäre der Nachteil, wenn Sie diesen SQL direkt in psql ausführen würden, statt prisma migrate dev zu verwenden?
  • Warum ist es wichtig, die Migrationsdateien im Git-Repository zu committen?

Anforderungen ändern sich. Der Schulblog soll nun anzeigen, wann ein Post zuletzt bearbeitet wurde.

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.

Terminal-Fenster
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 (kein NOT NULL)? Überlegen Sie: Was wäre passiert, wenn Sie ein NOT NULL-Feld ohne Standardwert zu einer Tabelle hinzufügen würden, die bereits Daten enthält?

Prisma protokolliert alle angewandten Migrationen in einer eigenen Tabelle:

SELECT migration_name, finished_at
FROM "_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?

  • 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.sql und führen Sie erneut npx prisma migrate dev aus. Was meldet Prisma?
  • Wann sollte man prisma migrate reset verwenden, und warum ist dieser Befehl in einer Produktionsdatenbank gefährlich?

Beantworten Sie schriftlich:

  1. Was ist das N+1-Problem? Beschreiben Sie es an einem konkreten Beispiel aus diesem Übungsblatt (Posts und Autoren).
  2. Was ist der Unterschied zwischen include und select in einer Prisma-Abfrage?
  3. Warum ist es wichtig, einen globalen Prisma-Client-Singleton zu verwenden, statt bei jeder Anfrage new PrismaClient() aufzurufen?

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;

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({
data: { name: 'Anna Bauer', email: '[email protected]' },
});
const max = await prisma.user.create({
data: { name: 'Max Huber', email: '[email protected]' },
});
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:

Terminal-Fenster
npm install -D ts-node
npx prisma db seed

Überprüfen Sie in psql oder pgAdmin, ob die Daten korrekt eingefügt wurden.

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:

Terminal-Fenster
npm run dev

Rufen 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?

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?

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?

  • 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 psql nach 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).

  1. 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.

  2. 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`);
    }
  3. 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