본문으로 건너뛰기 Prisma Complete Guide | Schema· Queries

Prisma Complete Guide | Schema· Queries

Prisma Complete Guide | Schema· Queries

이 글의 핵심

Prisma gives you a type-safe database client generated from your schema — no more SQL string mistakes or manual type definitions. This guide covers schema design, all query patterns, migrations, relations, and performance best practices.

Why Prisma?

// Without Prisma (raw SQL + manual types)
const result = await pool.query<User>(
  'SELECT id, name, email FROM users WHERE id = $1',  // SQL string — no type checking
  [userId]
);
const user: User = result.rows[0];  // Manual type assertion

// With Prisma (generated type-safe client)
const user = await prisma.user.findUnique({ where: { id: userId } });
// user is fully typed: { id: number; name: string; email: string; ... }
// TypeScript catches typos in field names at compile time

Prisma generates a client from your schema — every query is typed, every field is checked at compile time.


Setup

npm install prisma @prisma/client
npx prisma init        # Creates prisma/schema.prisma and .env
# .env
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
# Start PostgreSQL with Docker
docker run -d \
  --name postgres \
  -e POSTGRES_PASSWORD=password \
  -e POSTGRES_USER=user \
  -e POSTGRES_DB=mydb \
  -p 5432:5432 \
  postgres:16-alpine

Schema Definition

// prisma/schema.prisma

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
  role      Role     @default(VIEWER)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  posts     Post[]
  profile   Profile?

  @@index([email])
}

enum Role {
  ADMIN
  EDITOR
  VIEWER
}

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

  authorId    Int
  author      User     @relation(fields: [authorId], references: [id], onDelete: Cascade)

  tags        Tag[]    @relation("PostToTag")
  categories  Category[]

  @@index([authorId])
  @@index([published, createdAt])
}

model Tag {
  id    Int    @id @default(autoincrement())
  name  String @unique
  posts Post[] @relation("PostToTag")
}

model Profile {
  id     Int    @id @default(autoincrement())
  bio    String?
  avatar String?

  userId Int    @unique
  user   User   @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model Category {
  id    Int    @id @default(autoincrement())
  name  String @unique
  posts Post[]
}

Migrations

# Development: generate and apply migration
npx prisma migrate dev --name add-user-profile

# This creates: prisma/migrations/20240415_add_user_profile/migration.sql
# And applies it to the database

# Production: apply pending migrations (no prompts)
npx prisma migrate deploy

# Reset database (⚠️ drops all data)
npx prisma migrate reset

# Pull schema from existing database
npx prisma db pull

# Open Prisma Studio (visual DB browser)
npx prisma studio

Client Setup

// lib/prisma.ts
import { PrismaClient } from '@prisma/client';

// Prevent multiple instances in development (Next.js hot reload)
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };

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;
}

CRUD Operations

Create

// Create a single record
const user = await prisma.user.create({
  data: {
    email: '[email protected]',
    name: 'Alice',
    role: 'ADMIN',
  },
});

// Create with nested relations
const post = await prisma.post.create({
  data: {
    title: 'Hello Prisma',
    content: 'Getting started with Prisma ORM',
    author: {
      connect: { id: 1 },        // Connect to existing user
    },
    tags: {
      connectOrCreate: [          // Create tag if not exists, else connect
        { where: { name: 'typescript' }, create: { name: 'typescript' } },
        { where: { name: 'orm' }, create: { name: 'orm' } },
      ],
    },
  },
  include: { author: true, tags: true },  // Return relations
});

// Create many
await prisma.user.createMany({
  data: [
    { email: '[email protected]', name: 'Bob' },
    { email: '[email protected]', name: 'Carol' },
  ],
  skipDuplicates: true,    // Ignore duplicates (email unique constraint)
});

Read

// Find by unique field
const user = await prisma.user.findUnique({ where: { id: 1 } });
const user2 = await prisma.user.findUnique({ where: { email: '[email protected]' } });

// Find first matching
const latestPost = await prisma.post.findFirst({
  where: { published: true },
  orderBy: { createdAt: 'desc' },
});

// Find many
const publishedPosts = await prisma.post.findMany({
  where: { published: true },
  orderBy: { createdAt: 'desc' },
  take: 10,           // LIMIT
  skip: 20,           // OFFSET (for pagination)
  select: {           // Only fetch needed fields (better performance)
    id: true,
    title: true,
    author: {
      select: { name: true },
    },
  },
});

// Filtering
const results = await prisma.post.findMany({
  where: {
    AND: [
      { published: true },
      { createdAt: { gte: new Date('2026-01-01') } },
      {
        OR: [
          { title: { contains: 'typescript', mode: 'insensitive' } },
          { content: { contains: 'typescript', mode: 'insensitive' } },
        ],
      },
    ],
  },
});

// Count
const count = await prisma.post.count({ where: { published: true } });

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

Update

// Update by unique field
const updated = await prisma.user.update({
  where: { id: 1 },
  data: {
    name: 'Alice Smith',
    updatedAt: new Date(),
  },
});

// Upsert (update or create)
const user = await prisma.user.upsert({
  where: { email: '[email protected]' },
  update: { name: 'Alice Smith' },
  create: { email: '[email protected]', name: 'Alice Smith' },
});

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

// Atomic increment/decrement
await prisma.post.update({
  where: { id: 1 },
  data: {
    viewCount: { increment: 1 },
  },
});

Delete

// Delete by unique field
await prisma.user.delete({ where: { id: 1 } });

// Delete many
await prisma.post.deleteMany({
  where: {
    createdAt: { lt: new Date('2025-01-01') },
  },
});

Relations

Include — Load Nested Data

// include loads related records (JOINs)
const userWithPosts = await prisma.user.findUnique({
  where: { id: 1 },
  include: {
    posts: {
      where: { published: true },
      orderBy: { createdAt: 'desc' },
      take: 5,
      include: {
        tags: true,           // Nested include
      },
    },
    profile: true,
  },
});

// Type: user.posts[0].tags[0].name ✅

Select — Precise Field Selection

// select picks exactly the fields you need
const userNames = await prisma.user.findMany({
  select: {
    id: true,
    name: true,
    _count: {
      select: { posts: true },  // Count related records
    },
  },
});
// userNames[0]._count.posts = 5

Many-to-Many

// Connect existing tags to a post
await prisma.post.update({
  where: { id: 1 },
  data: {
    tags: {
      connect: [{ id: 1 }, { id: 2 }],
      disconnect: [{ id: 3 }],
    },
  },
});

Transactions

// Batch operations (atomic — all succeed or all fail)
const [user, post] = await prisma.$transaction([
  prisma.user.create({ data: { email: '[email protected]', name: 'User' } }),
  prisma.post.create({ data: { title: 'First post', authorId: 1 } }),
]);

// Interactive transaction (use the transaction client)
const result = await prisma.$transaction(async (tx) => {
  const sender = await tx.user.update({
    where: { id: 1 },
    data: { balance: { decrement: 100 } },
  });

  if (sender.balance < 0) {
    throw new Error('Insufficient balance');  // Rolls back the transaction
  }

  const receiver = await tx.user.update({
    where: { id: 2 },
    data: { balance: { increment: 100 } },
  });

  return { sender, receiver };
});

Raw Queries

For complex queries that Prisma can’t express:

// Type-safe raw query
const users = await prisma.$queryRaw<{ id: number; name: string }[]>`
  SELECT id, name
  FROM users
  WHERE created_at > ${new Date('2026-01-01')}
  ORDER BY name
  LIMIT ${10}
`;

// Raw execute (for mutations — returns affected row count)
const count = await prisma.$executeRaw`
  UPDATE posts SET view_count = view_count + 1 WHERE id = ${postId}
`;

Performance Patterns

Avoid N+1 Queries

// ❌ N+1: 1 query for posts + N queries for authors
const posts = await prisma.post.findMany();
for (const post of posts) {
  const author = await prisma.user.findUnique({ where: { id: post.authorId } });
}

// ✅ 1 query with include
const posts = await prisma.post.findMany({
  include: { author: true },
});

Select Only What You Need

// ❌ Fetches all columns (profile image, bio, settings...)
const users = await prisma.user.findMany();

// ✅ Only fetch name and email
const users = await prisma.user.findMany({
  select: { id: true, name: true, email: true },
});

Pagination

// Cursor-based pagination (better performance than offset for large datasets)
async function getPosts(cursor?: number, limit = 20) {
  return prisma.post.findMany({
    where: { published: true },
    take: limit,
    skip: cursor ? 1 : 0,         // Skip the cursor itself
    cursor: cursor ? { id: cursor } : undefined,
    orderBy: { id: 'asc' },
  });
}

// Offset pagination (simpler, fine for small datasets)
async function getPostsPage(page: number, limit = 20) {
  const [posts, total] = await prisma.$transaction([
    prisma.post.findMany({
      where: { published: true },
      skip: (page - 1) * limit,
      take: limit,
      orderBy: { createdAt: 'desc' },
    }),
    prisma.post.count({ where: { published: true } }),
  ]);
  return { posts, total, totalPages: Math.ceil(total / limit) };
}

Connection Pooling (PgBouncer / Prisma Accelerate)

// For serverless environments (many short-lived connections)
const prisma = new PrismaClient({
  datasources: {
    db: {
      // Use connection pool URL (PgBouncer or Prisma Accelerate)
      url: process.env.DATABASE_URL_POOLED,
    },
  },
});

Middleware (Logging, Soft Delete)

// Soft delete middleware
prisma.$use(async (params, next) => {
  if (params.model === 'Post') {
    // Redirect deletes to updates
    if (params.action === 'delete') {
      params.action = 'update';
      params.args.data = { deletedAt: new Date() };
    }

    // Exclude soft-deleted records from queries
    if (params.action === 'findMany' || params.action === 'findFirst') {
      params.args.where = { ...params.args.where, deletedAt: null };
    }
  }
  return next(params);
});

// Query timing middleware
prisma.$use(async (params, next) => {
  const start = Date.now();
  const result = await next(params);
  const duration = Date.now() - start;

  if (duration > 1000) {
    console.warn(`Slow query: ${params.model}.${params.action} took ${duration}ms`);
  }

  return result;
});

Related posts:


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Master Prisma ORM for Node.js and TypeScript. Covers schema definition, CRUD operations, relations, migrations, transact… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

Prisma, ORM, TypeScript, PostgreSQL, Node.js, Database, Backend 등으로 검색하시면 이 글이 도움이 됩니다.