SQL & PostgreSQL

PostgreSQL je nejpokročilejší open-source relační databáze. Pro webové aplikace je zlatý standard — ACID transakce, JSON podpora, full-text search, dobré škálování.

DatabázeTypKdy použít
PostgreSQLRelační (SQL)Většina aplikací — strukturovaná data, relace, transakce
MySQL / MariaDBRelační (SQL)Tradiční PHP stack, méně features než PG
MongoDBDokumentová (NoSQL)Flexibilní schema, vnořené dokumenty, časté změny struktury
RedisIn-memory K/VCache, sessions, real-time, rate limiting
SQLiteRelační (soubor)Vývoj, embedded, malé aplikace, edge

Základní SQL — rychlý přehled

-- CREATE TABLE
CREATE TABLE products (
  id         SERIAL PRIMARY KEY,
  nazev      VARCHAR(100) NOT NULL,
  cena       DECIMAL(10,2) NOT NULL,
  dostupny   BOOLEAN DEFAULT true,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- INSERT
INSERT INTO products (nazev, cena) VALUES ('Victorinox', 299.00);

-- SELECT s filtrem
SELECT id, nazev, cena
  FROM products
 WHERE dostupny = true AND cena < 500
 ORDER BY cena ASC
 LIMIT 10 OFFSET 0;

-- UPDATE
UPDATE products SET cena = 349.00 WHERE id = 1;

-- DELETE
DELETE FROM products WHERE id = 1;

-- JOIN
SELECT p.nazev, c.text AS komentar
  FROM products p
  JOIN comments c ON c.product_id = p.id
 WHERE p.id = 42;
// 01 / 09 Prisma setup →

Prisma — setup a konfigurace

Prisma je TypeScript-first ORM — místo SQL píšete typované JS metody. Automaticky generuje typy z databázového schématu. Tři komponenty: Prisma Client, Prisma Migrate, Prisma Studio.

bash — Prisma setup
# Instalace $ npm install prisma @prisma/client $ npx prisma init ✓ Created prisma/schema.prisma ✓ Created .env with DATABASE_URL # Lokální PostgreSQL přes Docker $ docker run -e POSTGRES_PASSWORD=heslo -p 5432:5432 -d postgres:16 # .env DATABASE_URL="postgresql://postgres:heslo@localhost:5432/mydb?schema=public" # Generovat Prisma Client po změně schema $ npx prisma generate # Spustit migraci $ npx prisma migrate dev --name init # Vizuální DB editor $ npx prisma studio
// src/lib/prisma.ts — Singleton (důležité pro Next.js/dev!)
import { PrismaClient } from '@prisma/client';

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined;
};

export const prisma =
  globalForPrisma.prisma ??
  new PrismaClient({
    log: process.env.NODE_ENV === 'development'
      ? ['query', 'error', 'warn']
      : ['error'],
  });

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma;
}
// Proč singleton? Next.js hot reload vytváří nové instance —
// bez singletonu dojde k "too many connections" chybě
// 02 / 09 Schema →

Prisma Schema — datový model

Prisma Schema je "pravda" o struktuře databáze — definuje modely, typy, relace a indexy. Z něj se generují migrace i TypeScript typy.

User
idInt PK
emailString @unique
nameString?
passwordString
roleRole
createdAtDateTime
ordersOrder[]
1 : N
Order
idInt PK
userIdInt FK
statusOrderStatus
totalDecimal
itemsOrderItem[]
1 : N
OrderItem
idInt PK
orderIdInt FK
productIdInt FK
quantityInt
priceDecimal
// prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

// Enum
enum Role { USER ADMIN EDITOR }
enum OrderStatus { PENDING PAID SHIPPED DELIVERED CANCELLED }

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  password  String
  role      Role     @default(USER)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  orders  Order[]
  reviews Review[]

  @@index([email])  // Index pro rychlé vyhledávání
}

model Product {
  id          Int      @id @default(autoincrement())
  nazev       String
  slug        String   @unique
  cena        Decimal  @db.Decimal(10, 2)
  popis       String?  @db.Text
  dostupny    Boolean  @default(true)
  kategorie   String?
  imageUrl    String?
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt

  orderItems OrderItem[]
  reviews    Review[]

  @@index([nazev])
  @@index([kategorie])
}

model Order {
  id        Int         @id @default(autoincrement())
  userId    Int
  status    OrderStatus @default(PENDING)
  total     Decimal     @db.Decimal(10, 2)
  createdAt DateTime    @default(now())
  updatedAt DateTime    @updatedAt

  user  User        @relation(fields: [userId], references: [id])
  items OrderItem[]
}

model OrderItem {
  id        Int     @id @default(autoincrement())
  orderId   Int
  productId Int
  quantity  Int
  price     Decimal @db.Decimal(10, 2)

  order   Order   @relation(fields: [orderId],   references: [id], onDelete: Cascade)
  product Product @relation(fields: [productId], references: [id])
}

model Review {
  id        Int      @id @default(autoincrement())
  userId    Int
  productId Int
  rating    Int
  text      String?  @db.Text
  createdAt DateTime @default(now())

  user    User    @relation(fields: [userId],    references: [id])
  product Product @relation(fields: [productId], references: [id])

  @@unique([userId, productId]) // Jeden review per user/product
}

// Kvíz: Co dělá @updatedAt v Prisma schématu?

// 03 / 09 Migrace →

Migrace — verzování schématu

Migrace jsou versioned SQL soubory popisující změny databázového schématu. Prisma Migrate je generuje automaticky z rozdílů ve schema.prisma.

bash — Prisma Migrate workflow
# Vývojový workflow — vytvoří migraci + aplikuje ji $ npx prisma migrate dev --name add-product-categories ✓ Created migration: 20240315_add-product-categories ✓ Applied migration ✓ Prisma Client regenerated # Produkční deployment — aplikuje pending migrace $ npx prisma migrate deploy ✓ Applied 3 migration(s) # Reset DB (vývoj) — smaže vše, pustí seed $ npx prisma migrate reset # Seed — naplnění testovacími daty $ npx prisma db seed # Vizuální editor $ npx prisma studio Prisma Studio is up at http://localhost:5555
// prisma/seed.ts — testovací data
import { prisma } from '../src/lib/prisma';
import bcrypt from 'bcryptjs';

async function main() {
  // Admin uživatel
  const admin = await prisma.user.upsert({
    where:  { email: 'admin@example.com' },
    update: {},
    create: {
      email:    'admin@example.com',
      name:     'Admin',
      password: await bcrypt.hash('admin123', 12),
      role:     'ADMIN',
    },
  });

  // Produkty
  const produkty = [
    { nazev: 'Victorinox Classic', slug: 'victorinox-classic', cena: 299 },
    { nazev: 'Gerber Paraframe',   slug: 'gerber-paraframe',   cena: 549 },
  ];

  for (const p of produkty) {
    await prisma.product.upsert({
      where:  { slug: p.slug },
      update: {},
      create: { ...p, cena: p.cena },
    });
  }

  console.log('Seed dokončen');
}

main().catch(console.error).finally(() => prisma.$disconnect());

// package.json
"prisma": {
  "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
}
// 04 / 09 CRUD dotazy →

CRUD dotazy s Prisma Client

import { prisma } from '../lib/prisma';

// ── CREATE ────────────────────────────────────────────────
const product = await prisma.product.create({
  data: {
    nazev:    'Victorinox Classic',
    slug:     'victorinox-classic',
    cena:     299,
    dostupny: true,
  },
});

// ── READ — findMany (seznam) ──────────────────────────────
const products = await prisma.product.findMany({
  where: {
    dostupny: true,
    cena:     { lte: 500 },  // less than or equal
    nazev:    { contains: 'nůž', mode: 'insensitive' },
  },
  orderBy: { cena: 'asc' },
  skip:    (page - 1) * limit,
  take:    limit,
  select: { id: true, nazev: true, cena: true }, // Jen potřebná pole
});

// ── READ — findUnique (jeden) ─────────────────────────────
const product = await prisma.product.findUnique({
  where: { id: 42 },
});
// or by unique field:
const product = await prisma.product.findUnique({
  where: { slug: 'victorinox-classic' },
});

// ── UPDATE ────────────────────────────────────────────────
const updated = await prisma.product.update({
  where: { id: 42 },
  data:  { cena: 349 },
});

// Upsert — vytvoř nebo aktualizuj
const upserted = await prisma.product.upsert({
  where:  { slug: 'victorinox' },
  update: { cena: 349 },
  create: { nazev: 'Victorinox', slug: 'victorinox', cena: 349 },
});

// ── DELETE ────────────────────────────────────────────────
await prisma.product.delete({ where: { id: 42 } });

// ── COUNT ─────────────────────────────────────────────────
const total = await prisma.product.count({
  where: { dostupny: true },
});

// ── TRANSAKCE ─────────────────────────────────────────────
const result = await prisma.$transaction(async (tx) => {
  // Všechny operace v jedné atomické transakci
  const order = await tx.order.create({
    data: { userId, total, status: 'PENDING' }
  });

  await tx.orderItem.createMany({
    data: items.map(i => ({
      orderId: order.id, productId: i.id,
      quantity: i.qty, price: i.cena
    }))
  });

  // Sníž sklad
  for (const item of items) {
    await tx.product.update({
      where: { id: item.id },
      data:  { sklad: { decrement: item.qty } }
    });
  }

  return order;
});

// Kvíz: Proč je Prisma $transaction důležitý pro vytváření objednávky (order + items + update skladu)?

// 05 / 09 Relace →

Relace & Include / Select

Prisma umí načítat propojená data přes include (přidá do výsledku) nebo select (přesný výběr polí). Pozor na N+1 problem!

// include — načte propojené záznamy
const orderWithItems = await prisma.order.findUnique({
  where: { id: orderId },
  include: {
    user:  true,          // Celý user objekt
    items: {
      include: {
        product: {        // Vnořené include
          select: { nazev: true, imageUrl: true }
        }
      }
    }
  }
});
// orderWithItems.items[0].product.nazev ✅

// select — přesný výběr (lepší výkon)
const products = await prisma.product.findMany({
  select: {
    id:    true,
    nazev: true,
    cena:  true,
    _count: { select: { reviews: true } }, // Počet reviews
    reviews: {
      select: { rating: true },
      orderBy: { createdAt: 'desc' },
      take: 3, // Jen poslední 3
    }
  }
});

// Agregace
const stats = await prisma.product.aggregate({
  _avg:   { cena: true },
  _min:   { cena: true },
  _max:   { cena: true },
  _count: { id:   true },
  where:  { dostupny: true },
});
// stats._avg.cena, stats._count.id

// groupBy
const byCategory = await prisma.product.groupBy({
  by:       ['kategorie'],
  _count:   { id: true },
  _avg:     { cena: true },
  orderBy:  { _count: { id: 'desc' } },
});

// N+1 problém — ŠPATNĚ
const products = await prisma.product.findMany();
for (const p of products) {
  // ❌ Nový DB dotaz pro každý produkt!
  p.reviews = await prisma.review.findMany({ where: { productId: p.id } });
}

// ✅ Správně — jeden dotaz s include
const products = await prisma.product.findMany({
  include: { reviews: true } // JOIN v jednom dotazu
});
// 06 / 09 Optimalizace →

Optimalizace dotazů

// 1. INDEXY — kritické pro vyhledávání
model Product {
  // ...
  @@index([kategorie])        // filtr podle kategorie
  @@index([nazev])            // full-text search
  @@index([dostupny, cena])   // compound index pro filtr + sort
}

// 2. SELECT jen potřebná pole (neposílat hesla, velké texty)
const users = await prisma.user.findMany({
  select: { id: true, email: true, name: true }
  // NIKDY select: true pro User — vrátí heslo!
});

// 3. Cursor-based pagination (lepší než offset pro velké datasety)
const products = await prisma.product.findMany({
  take:   20,
  cursor: lastId ? { id: lastId } : undefined,
  skip:   lastId ? 1 : 0,       // Přeskočit cursor item
  orderBy: { id: 'asc' },
});

// 4. Raw SQL pro komplexní dotazy (full-text search, custom funkce)
const results = await prisma.$queryRaw`
  SELECT id, nazev, ts_rank(search_vector, query) AS rank
  FROM products,
       to_tsquery('czech', ${searchTerm}) query
  WHERE search_vector @@ query
  ORDER BY rank DESC
  LIMIT 10
`;

// 5. Connection pooling (produkce)
// Použijte PgBouncer nebo Prisma Accelerate
// DATABASE_URL=prisma://accelerate.prisma-data.net/?api_key=...

// 6. Soft delete pattern
model Product {
  // ...
  deletedAt DateTime?  // null = aktivní, DateTime = smazáno
  @@index([deletedAt]) // rychlý filtr
}
// Dotaz:
where: { deletedAt: null } // Jen nesmazané

// Kvíz: Co je N+1 problém v kontextu ORM a databáze?

// 07 / 09 Cvičení →

Cvičení — Blog datový model

Navrhněte kompletní Prisma schema pro blog systém: uživatelé, příspěvky (s tagy), komentáře, lajky, kategorie. Přidejte správné indexy, relace a enums.

// prisma/schema.prisma — doplňte:

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  username  String   @unique
  // TODO: role enum (USER, AUTHOR, ADMIN)
  // TODO: relace na Post, Comment, Like
}

model Post {
  id          Int      @id @default(autoincrement())
  title       String
  slug        String   @unique
  // TODO: content (Text), published (Boolean), authorId (FK)
  // TODO: relace na User, Comment, Tag, Like
  // TODO: indexy pro slug a authorId
}

model Tag {
  // TODO: many-to-many s Post
  // Nápověda: v Prisma se M:N definuje přes pole na obou stranách
  // Post { tags Tag[] } a Tag { posts Post[] }
}

model Comment {
  // TODO: author, post, text, parentId (vnořené komentáře?)
}

model Like {
  // TODO: unikátní kombinace userId + postId
  @@unique([userId, postId])
}

// Zamyslete se: jak implementovat počet lajků efektivně?
// a) prisma._count na každý post (extra dotaz)
// b) denormalizace — likesCount column na Post
// c) agregační dotaz
🏅

Database Engineer

Zvládáte Prisma schema, migrace, CRUD operace, relace a optimalizaci dotazů.

// 08 / 09 Taháček →

// Taháček

// Prisma CRUD
await prisma.model.create({ data: {...} });
await prisma.model.findMany({ where, orderBy, skip, take, select });
await prisma.model.findUnique({ where: { id } });
await prisma.model.update({ where: { id }, data: {...} });
await prisma.model.upsert({ where, create, update });
await prisma.model.delete({ where: { id } });
await prisma.model.count({ where });

// Relace
include: { relation: true }          // načíst vše
include: { relation: { select: {} }} // načíst výběr
select: { _count: { select: { rel: true } }} // počet

// Transakce
await prisma.$transaction(async tx => {
  await tx.a.create({...});
  await tx.b.update({...});
});

// Migrace
npx prisma migrate dev --name popis  // vývoj
npx prisma migrate deploy             // produkce
npx prisma db seed                    // seed data
npx prisma studio                     // GUI editor
// Lekce 6 dokončena L7: Testování →