לדלג לתוכן

9.7 פריזמה ובסיס נתונים הרצאה

פריזמה ובסיס נתונים - Prisma and Database

בשיעור זה נלמד להשתמש ב-Prisma ORM כדי לחבר את אפליקציית Next.js לבסיס נתונים. נלמד להגדיר סכמה, לבצע פעולות CRUD, ולהשתמש ב-Prisma יחד עם Server Components ו-Server Actions.


מה זה ORM?

  • ORM הוא ראשי תיבות של Object-Relational Mapping
  • זו שכבה שמתרגמת בין קוד TypeScript/JavaScript לשאילתות SQL
  • במקום לכתוב SQL ישירות, עובדים עם אובייקטים ופונקציות
  • Prisma הוא ה-ORM הפופולרי ביותר בעולם ה-Node.js

למה Prisma?

  • Type-safe - קבלת שגיאות TypeScript כשהשאילתה לא תקינה
  • אוטו-קומפליט מלא בעורך
  • מיגרציות אוטומטיות
  • ממשק גרפי לניהול הנתונים (Prisma Studio)
  • תמיכה ב-SQLite, PostgreSQL, MySQL, MongoDB ועוד

התקנה והגדרה

התקנת Prisma

npm install prisma @prisma/client
npx prisma init --datasource-provider sqlite
  • prisma - כלי CLI להגדרה ומיגרציות
  • @prisma/client - הספרייה שמשתמשים בה בקוד
  • --datasource-provider sqlite - מגדיר SQLite כבסיס הנתונים (פשוט לפיתוח)

הפקודה יוצרת:

prisma/
  schema.prisma       # הגדרת הסכמה
.env                  # משתני סביבה

הגדרת חיבור

# .env
DATABASE_URL="file:./dev.db"
  • ל-SQLite המסד הוא קובץ מקומי
  • ל-PostgreSQL: postgresql://user:password@localhost:5432/mydb

הגדרת סכמה - Schema

קובץ prisma/schema.prisma מגדיר את מבנה בסיס הנתונים:

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

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

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String
  password  String
  role      String   @default("user")
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  posts     Post[]
  comments  Comment[]
}

model Post {
  id        Int       @id @default(autoincrement())
  title     String
  content   String
  published Boolean   @default(false)
  slug      String    @unique
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
  author    User      @relation(fields: [authorId], references: [id])
  authorId  Int
  comments  Comment[]
  tags      Tag[]
}

model Comment {
  id        Int      @id @default(autoincrement())
  content   String
  createdAt DateTime @default(now())
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
  post      Post     @relation(fields: [postId], references: [id], onDelete: Cascade)
  postId    Int
}

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

סוגי שדות

  • Int - מספר שלם
  • String - טקסט
  • Boolean - ערך בוליאני
  • DateTime - תאריך ושעה
  • Float - מספר עשרוני

תכונות שדות

  • @id - מפתח ראשי
  • @unique - ערך ייחודי
  • @default() - ערך ברירת מחדל
  • @updatedAt - מתעדכן אוטומטית
  • @relation - קשר בין טבלאות

סוגי קשרים

  • One-to-Many: משתמש אחד יכול להיות בעלים של הרבה פוסטים (User -> Post[])
  • Many-to-Many: פוסט יכול להיות עם הרבה תגיות, ותגית עם הרבה פוסטים (Post <-> Tag)

מיגרציות - Migrations

יצירת מיגרציה

npx prisma migrate dev --name init
  • יוצר את בסיס הנתונים אם לא קיים
  • יוצר את הטבלאות לפי הסכמה
  • יוצר קובץ מיגרציה בתיקיית prisma/migrations
  • מייצר את Prisma Client

פקודות שימושיות

# יצירת מיגרציה חדשה
npx prisma migrate dev --name add_tags

# איפוס בסיס הנתונים
npx prisma migrate reset

# ייצור Prisma Client בלי מיגרציה
npx prisma generate

# סנכרון סכמה ישיר (לפיתוח בלבד)
npx prisma db push

# פתיחת ממשק גרפי
npx prisma studio
  • prisma studio פותח ממשק ווב בפורט 5555 לצפייה ועריכת נתונים

הגדרת Prisma Client

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

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

export const prisma = globalForPrisma.prisma ?? new PrismaClient();

if (process.env.NODE_ENV !== "production") {
  globalForPrisma.prisma = prisma;
}
  • בפיתוח, Next.js עושה hot reload שיוצר חיבורים חדשים
  • הקוד שומר את ה-client ב-global כדי למנוע חיבורים מיותרים
  • בייצור, יש רק instance אחד

פעולות CRUD

יצירה - Create

// יצירת רשומה בודדת
const user = await prisma.user.create({
  data: {
    name: "ישראל",
    email: "israel@example.com",
    password: "hashed_password",
  },
});

// יצירה עם קשרים
const post = await prisma.post.create({
  data: {
    title: "הפוסט הראשון",
    content: "תוכן הפוסט",
    slug: "first-post",
    author: {
      connect: { id: 1 }, // חיבור למשתמש קיים
    },
    tags: {
      connectOrCreate: [
        {
          where: { name: "Next.js" },
          create: { name: "Next.js" },
        },
      ],
    },
  },
});

// יצירת רשומות מרובות
const users = await prisma.user.createMany({
  data: [
    { name: "אלי", email: "eli@example.com", password: "hash1" },
    { name: "דנה", email: "dana@example.com", password: "hash2" },
  ],
});

קריאה - Read

// כל הרשומות
const users = await prisma.user.findMany();

// רשומה בודדת לפי id
const user = await prisma.user.findUnique({
  where: { id: 1 },
});

// רשומה בודדת לפי שדה ייחודי
const user = await prisma.user.findUnique({
  where: { email: "israel@example.com" },
});

// עם סינון
const publishedPosts = await prisma.post.findMany({
  where: {
    published: true,
    title: { contains: "Next" },
  },
  orderBy: { createdAt: "desc" },
  take: 10,
  skip: 0,
});

// עם קשרים (include)
const userWithPosts = await prisma.user.findUnique({
  where: { id: 1 },
  include: {
    posts: {
      where: { published: true },
      orderBy: { createdAt: "desc" },
    },
    _count: { select: { posts: true } },
  },
});

// בחירת שדות ספציפיים (select)
const userNames = await prisma.user.findMany({
  select: {
    id: true,
    name: true,
    email: true,
  },
});

עדכון - Update

// עדכון רשומה בודדת
const updatedUser = await prisma.user.update({
  where: { id: 1 },
  data: { name: "שם חדש" },
});

// עדכון רשומות מרובות
const result = await prisma.post.updateMany({
  where: { authorId: 1 },
  data: { published: true },
});

// עדכון או יצירה (upsert)
const user = await prisma.user.upsert({
  where: { email: "israel@example.com" },
  update: { name: "שם מעודכן" },
  create: {
    name: "ישראל",
    email: "israel@example.com",
    password: "hash",
  },
});

מחיקה - Delete

// מחיקת רשומה בודדת
await prisma.user.delete({
  where: { id: 1 },
});

// מחיקת רשומות מרובות
await prisma.post.deleteMany({
  where: { published: false },
});

שימוש ב-Server Components

// app/posts/page.tsx
import { prisma } from "@/lib/prisma";
import Link from "next/link";

export default async function PostsPage() {
  const posts = await prisma.post.findMany({
    where: { published: true },
    include: {
      author: { select: { name: true } },
      _count: { select: { comments: true } },
    },
    orderBy: { createdAt: "desc" },
  });

  return (
    <div className="max-w-3xl mx-auto p-6">
      <h1 className="text-3xl font-bold mb-6">פוסטים</h1>
      <ul className="space-y-4">
        {posts.map((post) => (
          <li key={post.id} className="border p-4 rounded">
            <Link href={`/posts/${post.slug}`}>
              <h2 className="text-xl font-semibold hover:text-blue-600">
                {post.title}
              </h2>
            </Link>
            <p className="text-gray-500 text-sm mt-1">
              מאת {post.author.name} |{" "}
              {new Date(post.createdAt).toLocaleDateString("he-IL")} |{" "}
              {post._count.comments} תגובות
            </p>
            <p className="mt-2 text-gray-700 line-clamp-2">{post.content}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

שימוש ב-Server Actions

// app/actions/posts.ts
"use server";

import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";

export async function createPost(formData: FormData) {
  const title = formData.get("title") as string;
  const content = formData.get("content") as string;
  const slug = title
    .toLowerCase()
    .replace(/\s+/g, "-")
    .replace(/[^a-z0-9-]/g, "");

  await prisma.post.create({
    data: {
      title,
      content,
      slug,
      authorId: 1, // יתחלף באותנטיקציה
    },
  });

  revalidatePath("/posts");
  redirect("/posts");
}

export async function publishPost(id: number) {
  await prisma.post.update({
    where: { id },
    data: { published: true },
  });

  revalidatePath("/posts");
}

export async function deletePost(id: number) {
  await prisma.post.delete({
    where: { id },
  });

  revalidatePath("/posts");
  redirect("/posts");
}

export async function addComment(postId: number, formData: FormData) {
  const content = formData.get("content") as string;

  if (!content || content.trim().length < 2) {
    return { error: "התגובה חייבת להכיל לפחות 2 תווים" };
  }

  await prisma.comment.create({
    data: {
      content: content.trim(),
      postId,
      authorId: 1, // יתחלף באותנטיקציה
    },
  });

  revalidatePath(`/posts`);
  return { success: true };
}
// app/posts/new/page.tsx
import { createPost } from "@/app/actions/posts";

export default function NewPostPage() {
  return (
    <div className="max-w-lg mx-auto p-6">
      <h1 className="text-3xl font-bold mb-6">פוסט חדש</h1>
      <form action={createPost} className="space-y-4">
        <div>
          <label className="block font-semibold mb-1">כותרת</label>
          <input
            name="title"
            required
            className="w-full border rounded px-3 py-2"
          />
        </div>
        <div>
          <label className="block font-semibold mb-1">תוכן</label>
          <textarea
            name="content"
            required
            rows={8}
            className="w-full border rounded px-3 py-2"
          />
        </div>
        <button
          type="submit"
          className="w-full bg-blue-500 text-white py-2 rounded hover:bg-blue-600"
        >
          פרסם
        </button>
      </form>
    </div>
  );
}

Prisma Studio

ממשק גרפי לצפייה ועריכת הנתונים:

npx prisma studio
  • נפתח בדפדפן בכתובת http://localhost:5555
  • אפשר לצפות בכל הטבלאות
  • אפשר ליצור, לערוך ולמחוק רשומות
  • שימושי לדיבוג ולבדיקה

Seeding - מילוי נתונים ראשוניים

// prisma/seed.ts
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

async function main() {
  // ניקוי נתונים קיימים
  await prisma.comment.deleteMany();
  await prisma.post.deleteMany();
  await prisma.user.deleteMany();

  // יצירת משתמשים
  const user1 = await prisma.user.create({
    data: {
      name: "ישראל ישראלי",
      email: "israel@example.com",
      password: "hashed_password_1",
    },
  });

  const user2 = await prisma.user.create({
    data: {
      name: "דנה כהן",
      email: "dana@example.com",
      password: "hashed_password_2",
    },
  });

  // יצירת פוסטים
  await prisma.post.create({
    data: {
      title: "מדריך Next.js למתחילים",
      content: "במדריך זה נלמד את הבסיס של Next.js...",
      slug: "nextjs-beginners-guide",
      published: true,
      authorId: user1.id,
      comments: {
        create: [
          { content: "מדריך מצוין!", authorId: user2.id },
          { content: "תודה רבה", authorId: user1.id },
        ],
      },
    },
  });

  console.log("Seed completed");
}

main()
  .catch(console.error)
  .finally(() => prisma.$disconnect());
// package.json
{
  "prisma": {
    "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
  }
}
npx prisma db seed

סיכום

  • Prisma הוא ORM שמחבר בין TypeScript לבסיס נתונים עם type-safety מלא
  • הסכמה מוגדרת בקובץ schema.prisma עם מודלים, שדות וקשרים
  • מיגרציות מנוהלות עם prisma migrate dev
  • פעולות CRUD: create, findMany, findUnique, update, delete
  • ב-Server Components אפשר לגשת ישירות לפריזמה
  • ב-Server Actions פריזמה משמש לביצוע שינויים בנתונים
  • Prisma Studio מספק ממשק גרפי לניהול הנתונים
  • Seeding מאפשר מילוי נתונים ראשוניים לפיתוח