02_Prisma

Code-Dateien

DateinameAktion
CODECode_bank.zipDownload
CODECode_pizza.zipDownload
CODECode_student.zipDownload

Videos

DateinameAktion
VIDEOVideo_Bank_DAbspielen
VIDEOVideo_Pizza_EAbspielen

Lernmaterialien

ORM

Ein ORM (Object–Relational Mapping) ist eine Technik bzw. Bibliothek, die Objekte aus deinem Code (Klassen/Objekte) auf Tabellen in einer relationalen Datenbank abbildet.

Statt SQL von Hand zu schreiben, arbeitest du mit Objekten – das ORM erzeugt (meist) die passenden SQL-Statements für dich.

Beispiel-Idee

Du hast eine Tabelle users in der DB und im Code ein User-Objekt:

  • Ohne ORM:
    SELECT * FROM users WHERE id = 1; (SQL selbst schreiben)

  • Mit ORM (vereinfacht):
    User.findById(1) (ORM macht daraus SQL)

Was macht ein ORM typischerweise?

  • CRUD: Create, Read, Update, Delete als Methoden

  • Mapping: Spalten ↔︎️ Objektfelder, Tabellen ↔︎️ Klassen

  • Beziehungen: 1:n, n:m (z. B. User–Posts) als Objekt-Relationen

  • Migrationen: Tabellen/Schema-Versionierung per Code

  • Validierung & Typen (je nach ORM)

Vorteile

  • Weniger „Boilerplate“-SQL (Standard-Code) für Standardfälle

  • Einheitlicher Zugriff auf DB, oft bessere Wartbarkeit

  • Praktische Features (Migrations, Relations, Lazy/Eager Loading)

Nachteile

  • Kann komplexe SQL-Abfragen verstecken oder erschweren

  • Performance-Fallen (z. B. N+1-Problem)

  • Abhängigkeit vom ORM-Stil/Framework

Prisma

Prisma ist ein modernes ORM (Object-Relational Mapping) für TypeScript & JavaScript, das den Zugriff auf relationale Datenbanken stark vereinfacht und typsicher macht.

001.png

Kurzdefinition

Prisma ist:

  • ein Datenbank-Toolkit (nicht nur ein ORM)

  • entwickelt von der Prisma

  • TypeScript-first

  • stark typisiert

  • kompatibel mit PostgreSQL, MySQL, SQL Server, SQLite

➡️ Prisma übersetzt Typen & Objekte in SQL – sicher und kontrolliert.

Wozu braucht man Prisma?

Klassischer DB-Zugriff (Node.js):

const result = await db.query("SELECT * FROM users WHERE id = " + id);

❌ SQL-Injection ❌ Keine Typprüfung ❌ Fehler erst zur Laufzeit

Mit Prisma:

const user = await prisma.user.findUnique({
  where: { id: 1 }
});

✅ Typsicher ✅ Autocomplete ✅ Sicher ✅ Lesbar

Die 3 Kernbestandteile von Prisma

1. Prisma Schema (Datenmodell)

model Student {
  id     Int     @id @default(autoincrement())
  name   String
  course String
}

➡️ Single Source of Truth

➡️ Vergleichbar mit:

  • ER-Modell

  • DDL in SQL

2. Prisma Client (Typsichere API)

Aus dem Schema wird automatisch Code generiert:

const users = await prisma.user.findMany();
  • Methoden sind typisiert

  • Fehler schon im Editor

3. Migrationen

Eine Prisma Migration ist eine versionierte Änderung am Datenbankschema, die Prisma aus deinem schema.prisma ableitet und als SQL-Änderungsschritt speichert und auf die Datenbank anwendet.

npx prisma migrate dev
  • Versionskontrolle für das DB-Schema

  • Reproduzierbare DB-Zustände

  • Vergleichbar mit Flyway / Liquibase

Unterstützte Datenbanken

Datenbank Support
PostgreSQL
MySQL
SQL Server
SQLite
Oracle ❌ (Stand heute)

➡️ Für Oracle wäre weiterhin:

  • JDBC / JPA

  • oder klassische SQL-Zugriffe nötig

Prisma vs. klassische ORMs

Aspekt Prisma Hibernate / JPA
Sprache TypeScript Java
Typisierung Sehr stark Stark
Konfiguration Einfach Komplex
Performance Sehr gut Gut
SQL-Nähe Mittel Abstrakt

➡️ Prisma ist weniger Magie, mehr Kontrolle.

Prisma & TypeScript (großer Vorteil)

Prisma nutzt TypeScript maximal aus:

  • Autocomplete

  • Compile-Fehler bei falschen Queries

  • Typisierte Relationen

user.posts[0].title // garantiert vorhanden

➡️ Keine NullPointer-Überraschungen

Prisma & moderne Runtimes

Prisma funktioniert mit:

  • Node.js

  • Deno

  • Bun

  • Serverless & Cloud

Besonders beliebt in:

  • REST-APIs

  • GraphQL

  • Microservices

Typische Architektur mit Prisma

Controller / API
        ↓
Prisma Client
        ↓
SQL-Datenbank

➡️ Saubere Schichten

Implementierung

002b.png
002c.png
deno add npm:prisma@6
generator client {
  provider = "prisma-client"
  output   = "../generated"
  runtime = "deno"
}

datasource db {
  provider = "sqlite"
  url      = "file:../student.db"
}

model Student {
  id     Int    @id @default(autoincrement())
  name   String
  course String
}
002d.png
002e.png
023.png
022.png
004.png

.env

DATABASE_URL=file:../student.db
005.png

.gitignore

.env
**/*.db

deno.json

006.png
{ 
  "tasks": {
    "dev": "deno --watch -A --env-file server.ts",
    "pv": "deno -A prisma validate",
    "pg": "deno -A prisma generate",
    "pmd": "deno -A prisma migrate dev",
    "pms": "deno -A prisma migrate status",
    "seed": "deno -A --env-file ./prisma/seed.ts"
  },

  ...

"dev": "deno --watch -A --env-file server.ts"

deno Startet die Deno Runtime.

--watch Deno überwacht alle verwendeten Dateien. Bei jeder Änderung wird das Programm wird automatisch neu gestartet.

-A Allow All Permissions. –allow-read –allow-write –allow-net –allow-env –allow-run –allow-ffi. Nur für Development empfohlen!

--env-file Lädt Umgebungsvariablen aus der Datei .env.

server.ts Einstiegspunkt der Anwendung.

"pv": "deno -A prisma validate"

pv Name des Tasks ist frei wählbar. Prisma Validate.

validate prüft die Datei prisma/schema.prisma. Syntax des Schemas, Models & Relationen und Datasource-Konfiguration.

"pg": "deno -A prisma generate"

pg Prisma Generate. startet die Deno Runtime. führt die Prisma-CLI aus

liest prisma/schema.prisma erzeugt den Prisma Client und schreibt Code in ./generated/.

Wann muss man prisma generate ausführen?

  • Schema geändert

  • Neue Relation

  • Neue DB

"pmd": "deno -A prisma migrate dev"

Dieser Befehl:

  1. liest prisma/schema.prisma

  2. vergleicht es mit dem aktuellen DB-Schema

  3. erstellt neue Migrationen (SQL)

  4. wendet sie direkt auf die Datenbank an

  5. führt automatisch prisma generate aus

"pms": "deno -A prisma migrate status"

Zeigt:

  • welche Migrationen lokal existieren

  • welche in der Datenbank angewendet wurden

  • ob Migrationen fehlen, ausstehen oder driften

"seed": "deno -A --env-file seed.ts"

Initialdaten anlegen

-A

Notwendig, weil seed.ts meist:

  • auf die Datenbank zugreift

  • Umgebungsvariablen liest

  • evtl. Dateien lädt

008.png
007.png
009.png
010.png

.gitignore

.env
**/*.db
generated/
011.png
012.png
013.png

Restart Visual Studio Code!!!

014.png
deno add npm:prisma@6
deno add npm:@prisma/client@6

prisma/seed.ts

import { PrismaClient } from "../generated/client.ts";

const prisma = new PrismaClient();

const students = [
  { id: -1, name: "Anna", course: "Computer Science" },
  { id: -2, name: "Susi", course: "Mathematics" },
  { id: -3, name: "Fritz", course: "English" },
  { id: -4, name: "Andrea", course: "Mathematics" },
  { id: -5, name: "Thomas", course: "German" },
  { id: -6, name: "Verena", course: "Mathematics" },
  { id: -7, name: "Marion", course: "Mathematics" },
  { id: -8, name: "Karl", course: "Computer Science" },
  { id: -9, name: "Hans", course: "Mathematics" },
  { id: -10, name: "Barbara", course: "Computer Science" },
];

async function main() {
  console.log("Remove old testdata...");

  const result = await prisma.student.deleteMany({
    where: {
      id: { lt: 0 },
    },
  });
  console.log(`Deleted ${result.count} students with negative id`);

  console.log("Start seeding...");

  for (const student of students) {
    await prisma.student.create({
      data: student,
    });
    console.log(`Created student with id: ${student.id}`);
  }

  console.log("Seeding finished.");
}

try {
    await main();
}
catch (e) {
    console.error(e);
    Deno.exit(1);
}
finally {
    await prisma.$disconnect();
}
015.png

Das ARRAY im server.ts gehört gelöscht, da es durch eine Tabelle ersetzt wird!

statt Array

import { PrismaClient } from "./generated/client.ts";

const prisma = new PrismaClient();
const students = await prisma.student.findMany();

get route

app.get("/students", async (_req: Request, res: Response) => {
  const students = await prisma.student.findMany();
  res.json(students);
});

Das array students muss bei jedem get neu geladen werden, da sich die Daten in der Tabelle ändern können!

async bedeutet in JavaScript/TypeScript: Diese Funktion arbeitet “asynchron” und kann auf Dinge warten, die Zeit brauchen (z. B. Datenbankzugriffe), ohne den Server zu blockieren.

await bedeutet: „Warte, bis das Promise fertig ist, und gib mir das Ergebnis.“

Es wird benutzt, wenn eine Funktion asynchron arbeitet (z. B. Datenbank, Netzwerk, Datei).

  • prisma.student.findMany() startet die DB-Abfrage und liefert sofort ein Promise.

  • await sorgt dafür, dass dein Code an dieser Stelle pausiert, bis die Abfrage fertig ist.

  • Danach steckt im students wirklich das Array mit Daten.

get id

app.get("/students/:id", async (req: Request, res: Response) => {
  const id = parseInt(req.params.id);
  const student = await prisma.student.findUnique({
    where: { id }
  });
  if (!student) {
    return res.status(404).json({ error: "Student not found" });
  }
  res.json(student);
});

Das Suchen eines Studenten muss auch wieder über die Tabelle erfolgen

Änderungen:

  1. async wurde hinzugefügt (damit await möglich ist)

  2. students.find(...) wurde ersetzt durch await prisma.student.findUnique(...)

  3. Der Rest (id parsen, 404 wenn nicht gefunden, sonst res.json) bleibt gleich.

post

app.post("/students", async (req: Request, res: Response) => {
  const { name, course } = req.body;
  if (!name || !course) {
    return res.status(400).json({ error: "Name and course are required!" });
  }
  const newStudent = await prisma.student.create({
    data: { name, course }
  });
  res.status(201).json(newStudent);
});

Änderungen:

  1. async hinzugefügt

  2. newId + students.push(...) fällt weg (die DB/Prisma erzeugt und speichert den Datensatz)

  3. Stattdessen: await prisma.student.create({ data: ... })

Wichtig: Das funktioniert, da das Prisma-Model Student ein id Int @id @default(autoincrement()) hat. Wenn id nicht automatisch erzeugt wird, muss die id im data mitgeben.

put

app.put("/students/:id", async (req: Request, res: Response) => {
  const id = parseInt(req.params.id);
  const { name, course } = req.body;
  if (!name || !course) {
    return res.status(400).json({ error: "Name and course are required!" });
  }
  try {
    const updatedStudent = await prisma.student.update({
      where: { id },
      data: { name, course }
    });
    res.json(updatedStudent);
  } catch {
    return res.status(404).json({ error: "Student not found" });
  }
});

Änderungen

  • async + await

  • findIndex/Array-Update weg

  • Prisma update übernimmt Speichern in DB

  • try/catch, weil Prisma bei “id existiert nicht” einen Fehler wirft

patch

app.patch("/students/:id", async (req: Request, res: Response) => {
  const id = parseInt(req.params.id);
  const { name, course } = req.body;

  const data: { name?: string; course?: string } = {};

  if (name !== undefined) data.name = name;
  if (course !== undefined) data.course = course;

  try {
    const updatedStudent = await prisma.student.update({
      where: { id },
      data
    });

    res.json(updatedStudent);
  } catch {
    return res.status(404).json({ error: "Student not found" });
  }
});

!!! Bei einem numerischen Wert die Typkonvertierung nicht vergessen !!!

if (balance !== undefined) data.balance = Number(balance);

Änderungen:

  • async + await

  • kein students.find(...) und kein Mutieren im Array

  • Prisma update speichert direkt in der DB

  • data: { ... } sorgt dafür, dass nur die Felder aktualisiert werden, die wirklich im Body vorhanden sind

  • try/catch, weil Prisma bei nicht vorhandener id einen Fehler wirft

const data: { name?: string; course?: string } = {};

bedeutet:

  • data ist eine Variable (ein Objekt), die am Anfang leer ist: {}

  • Der Teil nach dem Doppelpunkt : { name?: string; course?: string } ist ein TypeScript-Typ für dieses Objekt.

Was sagt dieser Typ aus?

  • name?: string heißt:
    data kann ein Feld name haben, und wenn es existiert, ist es ein string.
    Das ? bedeutet optional (kann fehlen).

  • course?: string heißt dasselbe für course.

Warum macht man das bei PATCH?

Bei PATCH willst du nur die Felder setzen, die im Request vorkommen.
Deshalb baust du ein Objekt schrittweise auf:

const data: { name?: string; course?: string } = {};

if (name !== undefined) data.name = name;
if (course !== undefined) data.course = course;

Am Ende kann data sein:

  • {} (nichts zu ändern)

  • { name: "Anna" }

  • { course: "DB" }

  • { name: "Anna", course: "DB" }

Und genau so ein Objekt erwartet Prisma in update({ data: ... }).

delete

app.delete("/students/:id", async (req: Request, res: Response) => {
  const id = parseInt(req.params.id);

  try {
    await prisma.student.delete({
      where: { id }
    });

    return res.status(204).send(); // No Content
  } catch {
    return res.status(404).json({ error: "Student not found" });
  }
});

Änderugnen:

  • async + await

  • findIndex/splice weg

  • Prisma delete({ where: { id } }) löscht in der DB

  • try/catch, weil Prisma bei nicht vorhandener ID einen Fehler wirft

017.png
017b.png
018.png

endpoints.rest

GET http://localhost:3000/students

###
GET http://localhost:3000/students/-3

###
POST http://localhost:3000/students
Content-Type: application/json

{
  "name": "Max",
  "course": "Italien"
}

###
PATCH http://localhost:3000/students/-1
Content-Type: application/json

{
  "name": "Annalisa",
  "course": "Informatik"
}

###
DELETE http://localhost:3000/students/-2
019.png
020.png
021.png