İçeriğe geç
Node.js

PostgreSQL ve Prisma ORM: Modern Veritabanı Yönetimi

Tarık Tunç
PostgreSQL ve Prisma ORM: Modern Veritabanı Yönetimi

PostgreSQL ve Prisma ORM: Modern Veritabanı Yönetimi

PostgreSQL, güvenilirliği ve gelişmiş özellikleriyle tanınan açık kaynaklı bir ilişkisel veritabanı sistemidir. Prisma ise Node.js ve TypeScript ekosisteminde yeni nesil bir ORM olarak öne çıkar. Type-safe sorgular, otomatik migration'lar ve güçlü bir schema tanımlama dili sunan Prisma, geleneksel ORM'lerin karmaşıklığını ortadan kaldırır. Bu rehberde PostgreSQL ve Prisma'yı birlikte kullanarak modern, güvenli ve performanslı bir veritabanı katmanı oluşturmayı adım adım öğreneceksiniz.

1. Prisma Kurulumu ve Schema Tanımlama

Prisma, schema-first yaklaşımı benimser. prisma/schema.prisma dosyasında modellerinizi, ilişkilerinizi ve veritabanı bağlantınızı tanımlarsınız. Prisma CLI bu schema'dan veritabanı migration'larını ve type-safe client'ı otomatik olarak oluşturur.

# Prisma kurulumu
npm install prisma @prisma/client
npx prisma init

# prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

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

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String
  password  String
  role      Role     @default(USER)
  avatar    String?
  bio       String?  @db.Text
  isActive  Boolean  @default(true)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  posts     Post[]
  comments  Comment[]
  profile   Profile?

  @@index([email])
  @@index([createdAt(sort: Desc)])
  @@map("users")
}

model Post {
  id          String     @id @default(cuid())
  title       String
  slug        String     @unique
  content     String     @db.Text
  excerpt     String?    @db.VarChar(300)
  status      PostStatus @default(DRAFT)
  viewCount   Int        @default(0)
  publishedAt DateTime?
  createdAt   DateTime   @default(now())
  updatedAt   DateTime   @updatedAt

  author      User       @relation(fields: [authorId], references: [id], onDelete: Cascade)
  authorId    String
  categories  Category[]
  comments    Comment[]
  tags        Tag[]

  @@index([authorId])
  @@index([status, publishedAt(sort: Desc)])
  @@map("posts")
}

model Comment {
  id        String   @id @default(cuid())
  text      String   @db.Text
  createdAt DateTime @default(now())

  user      User     @relation(fields: [userId], references: [id])
  userId    String
  post      Post     @relation(fields: [postId], references: [id], onDelete: Cascade)
  postId    String

  @@map("comments")
}

model Category {
  id    String @id @default(cuid())
  name  String @unique
  slug  String @unique
  posts Post[]

  @@map("categories")
}

model Tag {
  id    String @id @default(cuid())
  name  String @unique
  posts Post[]

  @@map("tags")
}

model Profile {
  id       String  @id @default(cuid())
  website  String?
  github   String?
  twitter  String?
  user     User    @relation(fields: [userId], references: [id], onDelete: Cascade)
  userId   String  @unique

  @@map("profiles")
}

enum Role {
  USER
  EDITOR
  ADMIN
}

enum PostStatus {
  DRAFT
  PUBLISHED
  ARCHIVED
}
Bilgi: Prisma schema'da @map ve @@map ile veritabanındaki tablo ve sütun isimlerini özelleştirebilirsiniz. cuid() veya uuid() ile unique ID'ler otomatik oluşturulur. @updatedAt dekoratörü, her güncellemede tarihi otomatik olarak günceller.

2. Migration ve Veritabanı Yönetimi

Prisma Migrate, schema değişikliklerinizi SQL migration dosyalarına dönüştürür ve veritabanınıza uygular. Her migration bir SQL dosyası olarak saklanır, bu da değişikliklerin izlenebilir ve geri alınabilir olmasını sağlar.

# İlk migration oluştur ve uygula
npx prisma migrate dev --name init

# Schema değişikliği sonrası yeni migration
npx prisma migrate dev --name add_tags_table

# Production'da migration uygula
npx prisma migrate deploy

# Prisma Client'ı yeniden oluştur
npx prisma generate

# Veritabanını görselleştir
npx prisma studio

# Seed dosyası oluşturma (prisma/seed.js)
const { PrismaClient } = require('@prisma/client');
const bcrypt = require('bcryptjs');

const prisma = new PrismaClient();

async function seed() {
  // Mevcut verileri temizle
  await prisma.comment.deleteMany();
  await prisma.post.deleteMany();
  await prisma.user.deleteMany();

  // Admin kullanıcı
  const admin = await prisma.user.create({
    data: {
      email: 'admin@example.com',
      name: 'Admin User',
      password: await bcrypt.hash('admin123', 12),
      role: 'ADMIN',
      profile: {
        create: {
          website: 'https://example.com',
          github: 'https://github.com/admin'
        }
      }
    }
  });

  // Kategoriler
  const categories = await Promise.all(
    ['Teknoloji', 'Backend', 'Frontend'].map(name =>
      prisma.category.create({
        data: { name, slug: name.toLowerCase() }
      })
    )
  );

  // Örnek yazılar
  await prisma.post.create({
    data: {
      title: 'Prisma ile Veritabanı Yönetimi',
      slug: 'prisma-veritabani-yonetimi',
      content: 'Prisma modern bir ORM çözümüdür...',
      status: 'PUBLISHED',
      publishedAt: new Date(),
      authorId: admin.id,
      categories: { connect: [{ id: categories[0].id }] }
    }
  });

  console.log('Seed verileri oluşturuldu');
}

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

3. CRUD Operasyonları ve Gelişmiş Sorgular

Prisma Client, type-safe sorgular ile veritabanı işlemlerini güvenli ve kolay hale getirir. TypeScript entegrasyonu sayesinde sorgu hataları derleme zamanında yakalanır, bu da runtime hatalarını önemli ölçüde azaltır.

const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient({
  log: ['query', 'info', 'warn', 'error']
});

// CREATE
async function createPost(data) {
  return prisma.post.create({
    data: {
      title: data.title,
      slug: data.slug,
      content: data.content,
      author: { connect: { id: data.authorId } },
      categories: {
        connect: data.categoryIds.map(id => ({ id }))
      },
      tags: {
        connectOrCreate: data.tags.map(tag => ({
          where: { name: tag },
          create: { name: tag }
        }))
      }
    },
    include: {
      author: { select: { name: true, avatar: true } },
      categories: true,
      tags: true
    }
  });
}

// READ - Gelişmiş filtreleme ve sayfalama
async function getPosts({ page = 1, limit = 10, search, status, authorId }) {
  const where = {
    ...(status && { status }),
    ...(authorId && { authorId }),
    ...(search && {
      OR: [
        { title: { contains: search, mode: 'insensitive' } },
        { content: { contains: search, mode: 'insensitive' } }
      ]
    })
  };

  const [posts, total] = await Promise.all([
    prisma.post.findMany({
      where,
      include: {
        author: { select: { id: true, name: true, avatar: true } },
        categories: { select: { name: true, slug: true } },
        _count: { select: { comments: true } }
      },
      orderBy: { publishedAt: 'desc' },
      skip: (page - 1) * limit,
      take: limit
    }),
    prisma.post.count({ where })
  ]);

  return { data: posts, total, pages: Math.ceil(total / limit) };
}

// UPDATE
async function updatePost(id, data) {
  return prisma.post.update({
    where: { id },
    data: {
      ...data,
      ...(data.status === 'PUBLISHED' && !data.publishedAt && {
        publishedAt: new Date()
      })
    }
  });
}

// Transaction kullanımı
async function publishPostWithNotification(postId) {
  return prisma.$transaction(async (tx) => {
    const post = await tx.post.update({
      where: { id: postId },
      data: { status: 'PUBLISHED', publishedAt: new Date() },
      include: { author: true }
    });

    await tx.user.update({
      where: { id: post.authorId },
      data: { /* bildirim verisi */ }
    });

    return post;
  });
}
Uyarı: Prisma Client instance'ını uygulama genelinde singleton olarak kullanın. Her istekte yeni bir PrismaClient oluşturmak connection pool tükenmesine yol açar. Development ortamında hot-reload sorunlarını önlemek için global değişken kullanabilirsiniz.

4. Raw SQL ve Performans Optimizasyonu

Prisma, karmaşık sorgular için raw SQL desteği sunar. Ayrıca select ve include ile sadece ihtiyaç duyduğunuz alanları çekerek over-fetching'i önleyebilirsiniz. Veritabanı index'leri, sorgu performansını dramatik şekilde artırır ve schema dosyasında tanımlanabilir.

N+1 sorgu problemini çözmek için Prisma'nın include ve select özelliklerini kullanın. Büyük veri setlerinde cursor-based pagination, offset pagination'dan çok daha performanslıdır. Prisma'nın query engine'i, ilişkili verileri otomatik olarak batch'leyerek veritabanı round-trip'lerini minimize eder.

İpucu: npx prisma studio komutu, veritabanınızı tarayıcıdan görsel olarak yönetmenizi sağlayan bir GUI açar. Development sürecinde verileri incelemek ve test verileri oluşturmak için çok kullanışlıdır.

Sonuç

PostgreSQL ve Prisma kombinasyonu, Node.js uygulamalarında güçlü, type-safe ve bakımı kolay bir veritabanı katmanı oluşturmanızı sağlar. Prisma'nın schema-first yaklaşımı, otomatik migration'ları ve type-safe client'ı ile veritabanı yönetimi büyük ölçüde kolaylaşır. Bu rehberdeki kavramları uygulamaya koyarak production-ready bir veritabanı altyapısı kurabilirsiniz.

Kaynaklar

İlgili Yazılar