Skip to content

21 • ORM

ORM, schéma, migrace databáze, dotazování přes ORM

Formát: 30 min praktická úloha, 15 min obhajoba + teorie. Praktika je s Prisma 7 (Next.js), teorie pokrývá ORM obecně, hlavní nástroje (Prisma, EF Core, Drizzle) a workflow.


🚨

POZOR: Prisma 7 (vydaná listopad 2025) zavedla zásadní změny oproti starší Prismě 6. Pokud někde najdeš starší tutorial, většina jeho konfigurace už nebude fungovat. Konkrétně v Prismě 7:

  • Connection URL se přesunula ze schema.prisma do nového souboru prisma.config.ts
  • new PrismaClient() vyžaduje driver adapter (pro SQLite: @prisma/adapter-better-sqlite3)
  • Generator se přejmenoval z prisma-client-js na prisma-client a generuje Client mimo node_modules (většinou do app/generated/prisma/)

API operace (findMany, create, update, ...) zůstávají stejné. Tyhle zápisky pokrývají Prismu 7.

⚠️

Taky bych byl tady opatrnej s LLMkama. Většina o Prismě 7 vůbec neví – nejaktuálnější asi budou Prisma docs.

Část 1: Teorie

Co je ORM a proč ho používáme

ORM (Object-Relational Mapper) je vrstva mezi aplikačním kódem a relační databází. Místo psaní SQL dotazů pracuješ s objekty a metodami ve svém programovacím jazyce. ORM je přeloží na SQL za tebe.

Tvůj kód (TypeScript / C# / Python)

        │  prisma.post.findMany({ where: { published: true } })

       ORM

        │  SELECT * FROM Post WHERE published = true

Relační databáze (SQLite / PostgreSQL / MySQL / SQL Server)

Raw SQL vs ORM

Raw SQLORM
SyntaxeSELECT * FROM Post WHERE id = 1prisma.post.findUnique({ where: { id: 1 } })
Typová bezpečnostString, chyba až za běhuTypeScript typy, chyba při kompilaci
MigraceRuční SQL souboryAutomatické z definice schématu
Přenositelnost DBDialekty se liší (SQLite ≠ PostgreSQL)ORM to abstrahuje
Složité dotazyPlná kontrola, optimálníNěkdy omezenější, escape hatch raw SQL
UčeníSQL syntaxAPI frameworku
RefactoringRisk, IDE neumí najít všechny dotazyTypeScript najde všechny použití

Pravidlo z praxe: 90 % dotazů ORM zvládne výborně. Zbylých 10 % (komplexní raportní dotazy, agregace) raději napsat raw SQL pod ORM (Prisma má $queryRaw).

Code-first vs DB-first

  • Code-first (běžnější): definuješ strukturu DB v kódu (schéma), ORM vygeneruje a aplikuje SQL přes migrace. Pravda v kódu, DB je odvozená.
  • DB-first: DB už existuje, ORM z ní vygeneruje kód (modely, klienta). Pravda v DB, kód je odvozený.

Prisma a moderní EF Core typicky používají code-first, je to čistější workflow.


Přehled ORM nástrojů

ORMJazykTypické prostředíCharakteristika
PrismaTypeScript / JavaScriptNext.js, Node.jsDeklarativní schema, generuje typed client
Drizzle ORMTypeScriptModern stack (edge, serverless)SQL-first, lightweight, výborný DX
Entity Framework CoreC#ASP.NET CoreLINQ dotazy, code-first migrations
TypeORMTypeScriptNestJS, Node.jsDecorator-based, starší přístup
SequelizeJavaScriptNode.js (legacy)Z doby před TypeScriptem
HibernateJavaSpringMature, mnoho features, složitější
SQLAlchemyPythonFastAPI, FlaskVelmi expressive

Tahle otázka je obvykle s Prisma (Next.js context).


Bonus: Prisma vs EF Core vs Drizzle

Praktická srovnávací tabulka, kterou si zkoušející asi cení.

Prisma 7EF CoreDrizzle ORM
JazykTypeScript / JSC#TypeScript
Definice schématuVlastní DSL (schema.prisma)C# třídy + DataAnnotations / Fluent APITypeScript objekty
Generování typůGenerated do app/generated/prisma/Reflection / EF runtimeInferred z definic
Migraceprisma migrate devdotnet ef migrations adddrizzle-kit migrate
Query syntaxMethod chaining (objektový)LINQ (declarative)SQL-like builder
Filozofie"Zjednoduš ORM""Plný ORM se vším""Tenký wrapper nad SQL"
ArchitekturaJavaScript + WASM query compilerLINQ providerTS-only
Driver modelVyžaduje explicit driver adapterBuilt-in providersVyžaduje driver
Performance overheadMedium (WASM compile, JSON parse)Medium (LINQ provider)Minimální (closer to raw SQL)
Lazy loadingNe (explicit include)Ano (default v EF)Ne (explicit JOIN)
Raw SQL escape$queryRawFromSqlRawSnadné (je SQL-blízké)
KomunitaVelká, Next.js standardVelká, .NET enterpriseRostoucí
Kdy si vybratTS/JS projekt s rychlým prototypingem.NET enterprise, komplexní entityEdge runtime, serverless, výkon

Praktický příklad stejného dotazu (najdi posty s autory):

tsx
// Prisma
const posts = await prisma.post.findMany({
    where: { published: true },
    include: { author: true },
    orderBy: { createdAt: 'desc' }
});
csharp
// EF Core (LINQ)
var posts = await db.Posts
    .Where(p => p.Published)
    .Include(p => p.Author)
    .OrderByDescending(p => p.CreatedAt)
    .ToListAsync();
tsx
// Drizzle (SQL-like)
const posts = await db
    .select()
    .from(posts)
    .leftJoin(authors, eq(posts.authorId, authors.id))
    .where(eq(posts.published, true))
    .orderBy(desc(posts.createdAt));

Pro maturitu: Prisma. Pro reálné Next.js projekty: oboje (Prisma pro rychlý start, Drizzle pro výkon/edge runtime). EF Core pro ASP.NET.


Co se v Prismě 7 zásadně změnilo (oproti 6)

Tuhle sekci si zapamatuj, pokud bys narazil na starší tutorial.

CoPrisma 6 (staré)Prisma 7 (nové)
Generatorprovider = "prisma-client-js"provider = "prisma-client"
Generated locationnode_modules/@prisma/clientvětšinou app/generated/prisma/ (explicit)
Import pathfrom '@prisma/client'from '../generated/prisma/client'
Connection URLurl = env(...) v schema.prismaV novém prisma.config.ts
PrismaClient initnew PrismaClient()new PrismaClient({ adapter })
DriverBuilt-in Rust engineDriver adapter (@prisma/adapter-*)
ModulesCommonJS i ESMESM only ("type": "module")
EngineRust (binary)WebAssembly + JavaScript
Automatic seed on migrateAnoOdstraněno
Client middleware$use()Odstraněno (použij Client Extensions)

Důsledek: instalace má víc kroků, ale runtime je výkonnější (žádný Rust process, menší bundle, lepší pro edge runtime).


Prisma 7: instalace a setup

bash
# 1. Prisma CLI (vývojová závislost)
npm install prisma --save-dev

# 2. Prisma Client (runtime)
npm install @prisma/client

# 3. Driver adapter pro SQLite + samotný driver
npm install @prisma/adapter-better-sqlite3 better-sqlite3

# 4. dotenv pro načítání env variables
npm install dotenv

# 5. Inicializace
npx prisma init

Po prisma init vzniknou:

prisma/
└── schema.prisma           ← definice modelů
prisma.config.ts             ← **NOVÁ** konfigurace (URL, schema path)
.env                         ← DATABASE_URL

package.json musí mít ESM

json
{
    "type": "module",
    "scripts": {
        "dev": "tsx src/index.ts",
        "generate": "prisma generate",
        "migrate": "prisma migrate dev"
    }
}

tsconfig.json pro ESM

json
{
    "compilerOptions": {
        "module": "ESNext",
        "moduleResolution": "bundler",
        "target": "ES2023",
        "strict": true,
        "esModuleInterop": true
    }
}

prisma.config.ts: nová konfigurace

V Prismě 7 se URL přesunulo ze schema.prisma do TypeScript konfigurace.

tsx
// prisma.config.ts (v ROOT projektu, vedle package.json)
import "dotenv/config";
import { defineConfig } from "prisma/config";

export default defineConfig({
  schema: "prisma/schema.prisma",
  migrations: {
    path: "prisma/migrations",
  },
  datasource: {
    url: process.env["DATABASE_URL"],
  },
});

CLI (prisma migrate dev, prisma generate, prisma studio) čte URL odtud. Runtime kód (tvůj Next.js) ho dostává přes driver adapter (viz dál).


Schéma (prisma/schema.prisma)

// Generátor: NOVÝ provider "prisma-client" + explicit output
generator client {
    provider = "prisma-client"
    output   = "../app/generated/prisma"
}

// Zdroj dat: BEZ url (to je v prisma.config.ts)
datasource db {
    provider = "sqlite"        // nebo "postgresql", "mysql", "sqlserver"
}

// Model = tabulka v DB
model Post {
    id        Int      @id @default(autoincrement())
    title     String
    content   String?
    published Boolean  @default(false)
    createdAt DateTime @default(now())

    author   Author @relation(fields: [authorId], references: [id])
    authorId Int
}

model Author {
    id    Int    @id @default(autoincrement())
    name  String
    email String @unique

    posts Post[]
}

Klíčové změny v schema.prisma vs Prisma 6:

  1. provider = "prisma-client" (ne prisma-client-js)
  2. output je povinný (generuje mimo node_modules)
  3. Datasource block nemá url (přesunuto do prisma.config.ts)

Typy sloupců

Prisma typSQL ekvivalentPoznámka
IntINTEGERCelé číslo
StringTEXT / VARCHARŘetězec
BooleanBOOLEANtrue/false
DateTimeDATETIMEDatum a čas
FloatREALDesetinné číslo
DecimalDECIMALPřesné decimální (pro peníze)
JsonJSONJSON sloupec
BytesBLOBBinární data
typ?NULL povolenoOtazník = volitelné
typ[](relace)Pro 1:N relace

Atributy (column-level)

AtributVýznam
@idPrimární klíč
@default(autoincrement())Auto-increment
@default(now())Aktuální čas při vytvoření
@default("hodnota")Statická výchozí hodnota
@uniqueUnikátní hodnota
@relation(...)Definice cizího klíče
@updatedAtAuto-update na změnu
@map("nazev_v_db")Mapování na jiný název v DB

Atributy (block-level)

model Post {
    id    Int    @id @default(autoincrement())
    slug  String
    lang  String

    @@unique([slug, lang])      // složený unique constraint
    @@index([createdAt])         // index pro rychlejší dotazy
    @@map("blog_posts")          // jiné jméno tabulky v DB
}

Typy relací

// 1:N (one-to-many)
model Author {
    posts Post[]
}
model Post {
    author   Author @relation(fields: [authorId], references: [id])
    authorId Int
}

// 1:1 (one-to-one)
model User {
    profile Profile?
}
model Profile {
    user   User @relation(fields: [userId], references: [id])
    userId Int  @unique
}

// N:M (many-to-many): implicit
model Post {
    tags Tag[]
}
model Tag {
    posts Post[]
}

Migrace

Migrace = aplikování změn schématu do skutečné databáze. Verzování databáze jako git verzuje kód.

bash
# Vytvoří migraci a aplikuje ji (development)
npx prisma migrate dev --name init

# Popisné jméno změny: init, add-user-table, add-published-field

Co příkaz udělá:

  1. Načte prisma.config.ts pro URL
  2. Porovná aktuální schéma s předchozím stavem (snapshot)
  3. Vygeneruje SQL soubor do prisma/migrations/
  4. Aplikuje SQL na databázi
  5. Regeneruje Prisma Client do app/generated/prisma/ (aby TypeScript typy odpovídaly schématu)
prisma/
├── schema.prisma
└── migrations/
    ├── 20260513120000_init/
    │   └── migration.sql           ← CREATE TABLE ...
    └── 20260514150000_add_published/
        └── migration.sql            ← ALTER TABLE ...

Důležité migrace příkazy

bash
# Vytvoří + aplikuje migraci (dev only, interaktivní)
npx prisma migrate dev --name add-categories

# Aplikuje migrace v produkci (nepromptuje)
npx prisma migrate deploy

# Reset DB: smaže vše, znovu aplikuje všechny migrace
npx prisma migrate reset

# Jen vygeneruje Client bez migrace
npx prisma generate

# Push schématu do DB bez migrace (rychlý prototyping)
npx prisma db push

db push vs migrate dev: db push neproduktuje migrace, jen aplikuje schema přímo. Hodí se pro rychlý prototyping. Pro produkci vždy migrate.

Důležité: v Prismě 7 už nedochází k automatickému seeding při migrate dev. Pokud máš seed script, musíš ho spustit zvlášť (npx prisma db seed nebo tsx prisma/seed.ts).


Prisma Client v Prismě 7

Klíčová změna: PrismaClient se vytváří s driver adapterem.

tsx
// lib/prisma.ts
import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3";
import { PrismaClient } from "@/app/generated/prisma/client";

const databaseUrl = process.env.DATABASE_URL ?? "file:./prisma/dev.db";

// V dev mode globální instance, aby hot-reload nezasypal connection pool
const globalForPrisma = globalThis as unknown as {
  prisma?: PrismaClient;
};

function createPrismaClient() {
  const adapter = new PrismaBetterSqlite3({ url: databaseUrl });

  return new PrismaClient({ adapter });
}

export const prisma = globalForPrisma.prisma ?? createPrismaClient();

if (process.env.NODE_ENV !== "production") {
  globalForPrisma.prisma = prisma;
}

Adaptery pro různé DB

DBAdapterDriver
SQLite (local)@prisma/adapter-better-sqlite3better-sqlite3
SQLite (Bun/edge)@prisma/adapter-libsql@libsql/client
PostgreSQL@prisma/adapter-pgpg
MySQL/MariaDB@prisma/adapter-mariadbmariadb
SQL Server@prisma/adapter-mssqlmssql
Cloudflare D1@prisma/adapter-d1(built-in v Cloudflare)
PlanetScale@prisma/adapter-planetscale@planetscale/database

Prisma Client: základní operace

Tahle část je stejná jako v Prismě 6, API operací se nezměnilo.

Čtení dat

tsx
// Najdi všechny posty
const posts = await prisma.post.findMany();

// Najdi konkrétní post podle id
const post = await prisma.post.findUnique({
  where: { id: 1 },
});
// findUnique vrátí jeden záznam nebo null

// Najdi první match (víc je možné)
const first = await prisma.post.findFirst({
  where: { published: true },
});

// Filtrování: jen publikované posty
const published = await prisma.post.findMany({
  where: { published: true },
});

// Složitější filtry
const posts = await prisma.post.findMany({
  where: {
    published: true,
    author: { name: "axo" }, // přes relaci
    createdAt: { gte: new Date("2026-01-01") },
    title: { contains: "Next" }, // SQL LIKE
  },
});

// OR podmínka
const posts = await prisma.post.findMany({
  where: {
    OR: [{ title: { contains: "React" } }, { title: { contains: "Vue" } }],
  },
});

// Řazení a stránkování
const posts = await prisma.post.findMany({
  orderBy: { createdAt: "desc" },
  take: 10, // LIMIT 10
  skip: 20, // OFFSET 20 (3. stránka)
});

Výběr sloupců a načítání relací

tsx
// SELECT: vyber jen některé sloupce
const posts = await prisma.post.findMany({
  select: {
    id: true,
    title: true,
    // content vynecháno, šetří se data
  },
});

// INCLUDE: načti i relaci (JOIN)
const postsWithAuthor = await prisma.post.findMany({
  include: {
    author: true, // přidá celý Author objekt
  },
});

// SELECT + INCLUDE kombinace (s SELECT na relaci)
const posts = await prisma.post.findMany({
  select: {
    id: true,
    title: true,
    author: {
      select: { name: true, email: true },
    },
  },
});

// Hluboké relace
const author = await prisma.author.findUnique({
  where: { id: 1 },
  include: {
    posts: {
      where: { published: true },
      orderBy: { createdAt: "desc" },
      take: 5,
    },
  },
});

select vs include: select určuje co vrátit (limit fieldů). include přidává relace navíc k defaultu (všechny scalar fields + tahle relace).

Vytvoření záznamu

tsx
const newPost = await prisma.post.create({
  data: {
    title: "Můj první post",
    content: "Obsah článku",
    author: {
      connect: { id: 1 }, // připoj existujícího autora
    },
  },
});

// Nebo create autora + post najednou
const newPost = await prisma.post.create({
  data: {
    title: "První",
    author: {
      create: { name: "axo", email: "axo@example.com" },
    },
  },
  include: { author: true },
});

// Batch insert
await prisma.post.createMany({
  data: [
    { title: "A", authorId: 1 },
    { title: "B", authorId: 1 },
    { title: "C", authorId: 2 },
  ],
});

Aktualizace záznamu

tsx
const updated = await prisma.post.update({
  where: { id: 1 },
  data: {
    published: true,
    title: "Nový název",
  },
});

// Batch update
await prisma.post.updateMany({
  where: { authorId: 1 },
  data: { published: true },
});

// Upsert (update nebo create)
const post = await prisma.post.upsert({
  where: { id: 1 },
  update: { title: "Aktualizováno" },
  create: { title: "Nový", authorId: 1 },
});

Smazání záznamu

tsx
const deleted = await prisma.post.delete({
  where: { id: 1 },
});

// Batch delete
await prisma.post.deleteMany({
  where: { published: false },
});

Agregace

tsx
const stats = await prisma.post.aggregate({
  _count: { id: true },
  where: { published: true },
});

const grouped = await prisma.post.groupBy({
  by: ["authorId"],
  _count: { id: true },
});

Prisma Studio

Vizuální prohlížeč databáze.

bash
npx prisma studio
# Otevře prohlížeč na http://localhost:5555

Můžeš procházet, přidávat, editovat záznamy bez SQL. Skvělý nástroj pro maturitu (ukaž zkoušejícímu data v Prisma Studiu, nepotřebuješ vědět žádné SQL).


.env soubor

V Prismě 7 se .env nenačítá automaticky. Musíš ho ručně loadovat:

bash
# .env
DATABASE_URL="file:./dev.db"
tsx
// V kódu:
import "dotenv/config";    // PRVNÍ řádka, načte .env

Pro CLI příkazy (prisma migrate dev, atd.) Prisma 7 načte .env automaticky přes prisma.config.ts (env('DATABASE_URL')).

Soubor .env přidej do .gitignore, obsahuje citlivé údaje. Místo něj se commitne .env.example se vzorem.


Celý workflow od začátku (Prisma 7)

bash
# 1. Setup projektu
npm init -y
npm install prisma --save-dev
npm install @prisma/client
npm install @prisma/adapter-better-sqlite3 better-sqlite3
npm install dotenv

# 2. ESM v package.json: přidej "type": "module"

# 3. Inicializace Prismy
npx prisma init

# 4. Uprav .env:
#    DATABASE_URL="file:./dev.db"

# 5. Vytvoř prisma.config.ts (viz výše)

# 6. Definuj modely v prisma/schema.prisma

# 7. Vytvoř a aplikuj migraci
npx prisma migrate dev --name init

# 8. (Volitelně) zkontroluj data
npx prisma studio

# 9. Vytvoř lib/prisma.ts s adapterem (viz výše)

# 10. Použij v kódu
#     import prisma from '@/lib/prisma';
#     const posts = await prisma.post.findMany();

Krátká zmínka o Entity Framework Core

Pro úplnost, kdyby zkoušející chtěl porovnání.

EF Core funguje podobně, jen v C#:

csharp
// Definice entit (modely)
public class Post {
    public int Id { get; set; }
    public string Title { get; set; } = "";
    public Author Author { get; set; } = null!;
    public int AuthorId { get; set; }
}

// DbContext (jako Prisma Client)
public class AppDbContext : DbContext {
    public DbSet<Post> Posts => Set<Post>();
    public DbSet<Author> Authors => Set<Author>();
}

// Použití
var posts = await db.Posts
    .Where(p => p.Published)
    .Include(p => p.Author)
    .OrderByDescending(p => p.CreatedAt)
    .ToListAsync();

Migrace v EF Core

bash
dotnet ef migrations add InitialCreate
dotnet ef database update

nebo ve VS built-in CLI:

powershell
Add-Migration InitialCreate
Update-Database

Časté chyby (Prisma 7)

ChybaDůsledekŘešení
Import z '@prisma/client'Cannot find moduleImport z '../generated/prisma/client'
new PrismaClient() bez adapteruRuntime errornew PrismaClient({ adapter })
Zapomenutý import "dotenv/config"process.env.DATABASE_URL je undefinedPřidat na 1. řádku
url v schema.prismaValidation error v Prismě 7Přesunout do prisma.config.ts
package.json bez "type": "module"ESM import nefungujePřidat "type": "module"
Vytváření new PrismaClient() v každém APIConnection pool overflowSdílená instance v lib/prisma.ts
Editovat schema bez migraceDB se neupdatujeVždy npx prisma migrate dev
findUnique vrací null bez kontrolyCrash při .title na nullVždy zkontrolovat if (!post) return ...
.env v gituBezpečnostní leakPřidat do .gitignore, použít .env.example
find v cyklu místo includeN+1 query, pomaléPoužít include
Forgotten awaitPromise místo datVždy await u Prisma operací
Použití db push v produkciŽádná verzovatelnost změnV produkci migrate deploy

Část 2: Praktická úloha

Co může praktická úloha obsahovat

Typická úloha:

  • Definice schématu v prisma/schema.prisma (2-3 modely s relacemi)
  • prisma.config.ts s URL z env
  • Migrace databáze (npx prisma migrate dev --name init)
  • lib/prisma.ts s driver adapterem a sdílenou instancí
  • API routes pro CRUD: GET (list), GET (detail), POST (create), DELETE (smazat)
  • include pro JOIN s relací
  • orderBy, take, skip pro řazení a stránkování
  • SSR stránka v Next.js, která čte data přímo přes Prisma (bez fetch z vlastního API)
  • Bonus: seed script s testovacími daty

Příklad zadání: Blog s Prisma 7 + Next.js

Vytvoř Next.js aplikaci s Prisma 7 pro správu blogu. Aplikace bude mít:

  • Schéma s Post a Author (relace 1:N)
  • Migraci
  • API routes pro CRUD
  • Stránku zobrazující data

Řešení

Setup

bash
npx create-next-app@latest blog --typescript --app
cd blog

npm install prisma --save-dev
npm install @prisma/client
npm install @prisma/adapter-better-sqlite3 better-sqlite3
npm install dotenv

npx prisma init --datasource-provider sqlite --output ../generated/prisma

package.json (relevantní část)

json
{
    "type": "module",
    "scripts": {
        "dev": "next dev",
        "build": "next build",
        "start": "next start"
    }
}

.env

bash
DATABASE_URL="file:./dev.db"

prisma.config.ts (v rootu)

tsx
import "dotenv/config";
import { defineConfig } from "prisma/config";

export default defineConfig({
  schema: "prisma/schema.prisma",
  migrations: {
    path: "prisma/migrations",
  },
  datasource: {
    url: process.env["DATABASE_URL"],
  },
});

Schéma: prisma/schema.prisma

generator client {
    provider = "prisma-client"
    output   = "../app/generated/prisma"
}

datasource db {
    provider = "sqlite"
}

model Author {
    id    Int     @id @default(autoincrement())
    name  String
    email String  @unique

    posts Post[]
}

model Post {
    id        Int      @id @default(autoincrement())
    title     String
    content   String?
    published Boolean  @default(false)
    createdAt DateTime @default(now())

    author   Author @relation(fields: [authorId], references: [id])
    authorId Int

    @@index([createdAt])    // index pro rychlejší řazení
}

Migrace

bash
npx prisma migrate dev --name init

To vytvoří:

  • dev.db (SQLite soubor)
  • prisma/migrations/.../migration.sql
  • app/generated/prisma/ (Prisma Client)

Prisma Client: lib/prisma.ts

tsx
// lib/prisma.ts
import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3";
import { PrismaClient } from "@/app/generated/prisma/client";

const databaseUrl = process.env.DATABASE_URL ?? "file:./prisma/dev.db";

// V dev mode globální instance, aby hot-reload nezasypal connection pool
const globalForPrisma = globalThis as unknown as {
  prisma?: PrismaClient;
};

function createPrismaClient() {
  const adapter = new PrismaBetterSqlite3({ url: databaseUrl });

  return new PrismaClient({ adapter });
}

export const prisma = globalForPrisma.prisma ?? createPrismaClient();

if (process.env.NODE_ENV !== "production") {
  globalForPrisma.prisma = prisma;
}

API: seznam + vytvoření (app/api/posts/route.ts)

tsx
import prisma from '@/lib/prisma';
import { NextResponse } from 'next/server';

// GET /api/posts
export async function GET() {
    const posts = await prisma.post.findMany({
        include: { author: true },
        orderBy: { createdAt: 'desc' }
    });
    return NextResponse.json(posts);
}

// POST /api/posts
// Body: { title, content?, authorId }
export async function POST(req: Request) {
    try {
        const data = await req.json();

        // Validace
        if (!data.title || !data.authorId) {
            return NextResponse.json(
                { error: 'title a authorId jsou povinné' },
                { status: 400 }
            );
        }

        const newPost = await prisma.post.create({
            data: {
                title: data.title,
                content: data.content ?? null,
                author: { connect: { id: data.authorId } }
            },
            include: { author: true }
        });

        return NextResponse.json(newPost, { status: 201 });
    } catch (err) {
        return NextResponse.json(
            { error: 'Něco se pokazilo' },
            { status: 500 }
        );
    }
}

API: detail + smazání (app/api/posts/[id]/route.ts)

tsx
import prisma from '@/lib/prisma';
import { NextResponse } from 'next/server';

// GET /api/posts/[id]
export async function GET(
    req: Request,
    { params }: { params: { id: string } }
) {
    const id = Number(params.id);

    const post = await prisma.post.findUnique({
        where: { id },
        include: { author: true }
    });

    if (!post) {
        return NextResponse.json({ error: 'Not Found' }, { status: 404 });
    }

    return NextResponse.json(post);
}

// DELETE /api/posts/[id]
export async function DELETE(
    req: Request,
    { params }: { params: { id: string } }
) {
    const id = Number(params.id);

    try {
        await prisma.post.delete({ where: { id } });
        return new NextResponse(null, { status: 204 });
    } catch {
        return NextResponse.json({ error: 'Not Found' }, { status: 404 });
    }
}

Stránka se seznamem (app/page.tsx)

tsx
import prisma from '@/lib/prisma';
import Link from 'next/link';

// Server Component (SSR): čte data přímo přes Prisma, bez fetch
export default async function HomePage() {
    const posts = await prisma.post.findMany({
        include: { author: true },
        orderBy: { createdAt: 'desc' },
        take: 20
    });

    return (
        <main style={{ maxWidth: 800, margin: '2rem auto', padding: '1rem' }}>
            <h1>Blog</h1>

            {posts.length === 0 ? (
                <p>Zatím žádné posty.</p>
            ) : (
                <ul>
                    {posts.map(post => (
                        <li key={post.id}>
                            <Link href={`/posts/${post.id}`}>
                                <strong>{post.title}</strong>
                            </Link>
                            <p>
                                {post.author.name} ·{' '}
                                {new Date(post.createdAt).toLocaleDateString('cs-CZ')}
                            </p>
                            {post.content && <p>{post.content.substring(0, 150)}...</p>}
                        </li>
                    ))}
                </ul>
            )}
        </main>
    );
}

Bonus: seed script

V Prismě 7 už automaticky neběží při migrate. Musíš ho spustit ručně.

tsx
// prisma/seed.ts
import "dotenv/config";
import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3";
import { PrismaClient } from "../src/generated/prisma/client";

const adapter = new PrismaBetterSqlite3({
    url: process.env.DATABASE_URL ?? "file:./dev.db"
});
const prisma = new PrismaClient({ adapter });

async function main() {
    // Smaž existující data (pro idempotenci)
    await prisma.post.deleteMany();
    await prisma.author.deleteMany();

    // Vytvoř autora
    const axo = await prisma.author.create({
        data: { name: 'axo', email: 'axo@example.com' }
    });

    const martin = await prisma.author.create({
        data: { name: 'martin', email: 'martin@example.com' }
    });

    // Vytvoř posty
    await prisma.post.createMany({
        data: [
            { title: 'První post', content: 'Obsah...', published: true, authorId: axo.id },
            { title: 'Next.js základy', content: 'App Router...', published: true, authorId: axo.id },
            { title: 'Draft', content: null, published: false, authorId: martin.id }
        ]
    });

    console.log('Seed hotov');
}

main()
    .catch(console.error)
    .finally(() => prisma.$disconnect());
bash
# Spuštění
npx tsx prisma/seed.ts

Co se v řešení děje

Setup: Instalace Prismy + driver adapter pro SQLite + dotenv. package.json musí mít "type": "module" kvůli ESM. prisma init vytvoří schema.prisma + prisma.config.ts.

prisma.config.ts: Centrální konfigurace, kterou používá CLI (migrate dev, studio...). Z .env čte DATABASE_URL. Tahle separace umožňuje různé URL pro migrace vs runtime.

Schéma: Dva modely s 1:N relací. Generator má provider = "prisma-client" (Prisma 7) a explicit output. Datasource block nemá url (je v prisma.config.ts). @@index([createdAt]) zrychluje řazení podle data.

Migrace: prisma migrate dev načte URL z prisma.config.ts, porovná schéma s předchozím stavem, vygeneruje SQL a aplikuje ho. Také regeneruje Prisma Client do src/generated/prisma/.

lib/prisma.ts: Sdílená instance Prisma Clienta. Klíčové změny v Prismě 7:

  1. import "dotenv/config" na 1. řádce, jinak process.env.DATABASE_URL je undefined
  2. Import z '../app/generated/prisma/client', ne z '@prisma/client'
  3. Vytvoření adapteru PrismaBetterSqlite3 s URL
  4. new PrismaClient({ adapter }) s tím adapterem

GET /api/posts: findMany s include: { author: true } udělá JOIN. orderBy seřadí podle data sestupně.

POST /api/posts: Přijme JSON tělo, validuje povinná pole. create s author.connect propojí na existujícího autora.

GET /api/posts/[id]: Dynamická route, params.id jako string, převedeno na number. findUnique vrátí null pokud neexistuje.

DELETE /api/posts/[id]: delete v try-catch (Prisma vyhodí chybu pokud záznam neexistuje). Vrátí 204 No Content.

Stránka jako Server Component: Čte z DB přímo přes Prisma (žádný fetch). Tohle je obrovská výhoda Next.js: API route ani neexistuje pro tuhle stránku, je to přímé volání DB ze serveru.


Část 3: Tipy pro obhajobu

Co u obhajoby říct

"V zadání jsem vytvořil Next.js aplikaci s Prismou 7. V schema.prisma jsem definoval modely Author a Post s 1:N relací: Post má authorId jako cizí klíč a navigation property author, Author má kolekci posts. Použil jsem code-first přístup: schéma je zdrojová pravda, migrace ho převede na SQL. Důležité je, že Prisma 7 má URL v novém prisma.config.ts, ne v schema.prisma. Po prisma migrate dev se vytvořil SQLite soubor s tabulkami a generated Prisma Client v src/generated/prisma. Pro Prisma Client jsem vytvořil sdílenou instanci v lib/prisma.ts. Klíčová věc Prismy 7: PrismaClient potřebuje driver adapter, pro SQLite je to PrismaBetterSqlite3 z @prisma/adapter-better-sqlite3. Také musíš ručně importovat dotenv/config, protože env variables se v Prismě 7 nenačítají automaticky. API routes používají findMany s include pro JOIN, findUnique pro detail s null-checkem, create s connect pro propojení autora. Hlavní stránka je Server Component, který čte z DB přímo přes Prisma, bez vlastní fetch na API."

Klíčové pojmy pro teorii

PojemRychlá odpověď
ORMObject-Relational Mapper, vrstva mezi kódem a DB
Code-firstDefinuji schéma v kódu, migrace vygeneruje DB
DB-firstDB existuje, generuji kód z ní
Prisma 7TypeScript ORM s deklarativním schématem, vyžaduje driver adapter
schema.prismaSoubor s definicí modelů
prisma.config.tsNOVÉ v Prismě 7: konfigurace pro CLI (URL, schema path)
Driver adapterNOVÉ v Prismě 7: most mezi Prisma Client a DB driverem
@prisma/adapter-better-sqlite3Adapter pro SQLite
@prisma/adapter-pgAdapter pro PostgreSQL
Prisma ClientVygenerovaný typed API pro práci s DB (v Prismě 7 v src/generated/prisma/)
MigraceVerzování změn DB schématu (jako git)
migrate devVytvoří + aplikuje migraci (development)
migrate deployAplikuje migrace v produkci (bez interakce)
db pushAplikuje schema bez migrace (prototyping)
findManyVrátí pole záznamů
findUniqueVrátí jeden záznam nebo null
includeNačte i relaci (JOIN)
selectVyber jen některé sloupce
@id, @unique, @defaultAtributy v Prisma schématu
@relation(...)Definice cizího klíče
Prisma StudioGUI pro procházení DB
DATABASE_URLPřipojovací řetězec v .env
N+1 problemCyklus s dotazem v každé iteraci, řeší include
EF CoreC# ekvivalent Prismy, LINQ-based
DrizzleModernější TypeScript ORM, SQL-blízký

Časté chytáky

OtázkaOdpověď
Co je ORM?Vrstva mezi kódem a databází. Píšeš metody a objekty, ORM je přeloží na SQL.
Proč ORM místo SQL?Typová bezpečnost, přenositelnost mezi DB, automatické migrace, méně chyb, lepší refactoring.
Co je code-first?Definuji strukturu DB v kódu (schéma), migrace ji vygeneruje. Opak je db-first (DB existuje, generuji modely z ní).
Co dělá migrace?Porovná aktuální schéma s předchozím a vygeneruje SQL (CREATE TABLE, ALTER TABLE) pro tu změnu. Aplikuje ji na DB.
Co je prisma.config.ts?Nový konfigurační soubor v Prismě 7. Obsahuje URL k DB a cestu ke schématu. CLI z něj čte při migrate, generate, studio.
Co je driver adapter?Most mezi Prisma Client a JS database driverem. V Prismě 7 povinný. Příklady: @prisma/adapter-better-sqlite3, @prisma/adapter-pg.
Proč v Prismě 7 driver adapter?Prisma 7 odstranila Rust engine, klient běží čistě v JS/WASM. Driver adapter zajišťuje skutečné připojení k DB.
Rozdíl findMany a findUnique?findMany vrátí vždy pole (může být prázdné). findUnique vrátí jeden záznam nebo null.
Rozdíl include a select?Include přidá relaci k defaultu (všechny scalar fields + relace). Select určuje, co se vrátí (jen vyjmenované).
Proč sdílená instance Prisma Client?Aby se v dev modu (s hot reload) nevytvářely nové instance a nezaplňovaly connection pool.
Co je Prisma Studio?GUI prohlížeč DB v prohlížeči, můžeš procházet/editovat data bez SQL.
Proč DATABASE_URL v .env?Citlivá data nepatří do kódu. .env je v .gitignore. Plus snadné přepínání mezi dev/prod.
Co dělá prisma generate?Vygeneruje TypeScript Client podle aktuálního schématu do src/generated/prisma/.
Co je N+1 problém?Cyklus, kde voláš dotaz v každé iteraci. Místo 1 dotazu s JOIN máš 1+N dotazů. Řešení: include.

Časté chyby v praktické úloze

  • Import z '@prisma/client' (Prisma 7 generuje do src/generated/prisma/)
  • new PrismaClient() bez adapteru (runtime crash)
  • Zapomenutý import "dotenv/config" (env je undefined)
  • url ponechané v schema.prisma (validation error)
  • package.json bez "type": "module" (ESM nefunguje)
  • Vytváření new PrismaClient() v každém API route (connection pool overflow)
  • Změna schema.prisma bez prisma migrate dev (DB neaktualizovaná)
  • findUnique bez null check (crash při .title na null)
  • delete bez try-catch (crash při neexistujícím ID)
  • include bez potřeby (over-fetching, pomalé)
  • find v cyklu místo include nebo where: { in: [...] } (N+1)
  • Hardcoded DATABASE_URL v kódu (citlivá data v gitu)
  • .env v gitu (security leak)
  • Chybějící await u Prisma operací (vrátí Promise místo dat)
  • createMany s relacemi (nepodporuje connect)
  • POST endpoint bez validace vstupu (zápis garbage do DB)
  • Použití db push v produkci (žádné verzování změn)