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¶
prisma- כלי CLI להגדרה ומיגרציות@prisma/client- הספרייה שמשתמשים בה בקוד--datasource-provider sqlite- מגדיר SQLite כבסיס הנתונים (פשוט לפיתוח)
הפקודה יוצרת:
הגדרת חיבור¶
- ל-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¶
יצירת מיגרציה¶
- יוצר את בסיס הנתונים אם לא קיים
- יוצר את הטבלאות לפי הסכמה
- יוצר קובץ מיגרציה בתיקיית
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¶
ממשק גרפי לצפייה ועריכת הנתונים:
- נפתח בדפדפן בכתובת
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"
}
}
סיכום¶
- Prisma הוא ORM שמחבר בין TypeScript לבסיס נתונים עם type-safety מלא
- הסכמה מוגדרת בקובץ
schema.prismaעם מודלים, שדות וקשרים - מיגרציות מנוהלות עם
prisma migrate dev - פעולות CRUD:
create,findMany,findUnique,update,delete - ב-Server Components אפשר לגשת ישירות לפריזמה
- ב-Server Actions פריזמה משמש לביצוע שינויים בנתונים
- Prisma Studio מספק ממשק גרפי לניהול הנתונים
- Seeding מאפשר מילוי נתונים ראשוניים לפיתוח