"بطل تمرير البيانات الخام من قاعدة البيانات للمكونات. طبقة الوصول للبيانات هي الحل. هنا ليه ده مهم وإزاي تبنيها بشكل صح."
ترميز سعيد! — Ahmed Fahmy
تخيل ده: عندك Server Component، بتجيب بيانات من قاعدة البيانات، وبتمررها مباشرة للمكون. بسيط، صح؟ غلط.
// ❌ ده غلط
export default async function UserProfile({ userId }: { userId: string }) {
const user = await db.users.findUnique({
where: { id: userId }
})
// بتمرر كل حاجة—الـ password، الـ tokens، كل حاجة
return <Profile user={user} />
}
شوف إيه اللي بيحصل هنا؟ بتمرر كل الحقول. الـ password hash. الـ API tokens. البيانات الحساسة. كل حاجة. لو المكون بتاعتك Client Component، كل دي بتروح للعميل. وبعدين بتروح للـ JavaScript bundle. وبعدين بتروح للـ browser. وبعدين... أنت فهمت الفكرة.
لهذا السبب توجد طبقة الوصول إلى البيانات. ولهذا السبب هي مهمة.
طبقة الوصول للبيانات (DAL) هي مكتبة داخلية بتتحكم في:
إزاي بتجيب البيانات
متى بتجيبها
إيه اللي بتمرره للمكونات
ده مش حاجة معقدة. ده مجرد مكان واحد بتحط فيه كل منطق جلب البيانات.
// lib/dal.ts - طبقة الوصول للبيانات
import { db } from "@/lib/db";
// تحقق من الصلاحيات
async function verifyUserAccess(userId: string, requestingUserId: string) {
if (userId !== requestingUserId) {
throw new Error("غير مصرح");
}
return true;
}
// جيب البيانات بشكل آمن
export async function getUserProfile(userId: string, requestingUserId: string) {
// تحقق من الصلاحيات أولاً
await verifyUserAccess(userId, requestingUserId);
// جيب البيانات
const user = await db.users.findUnique({
where: { id: userId },
});
if (!user) {
throw new Error("المستخدم غير موجود");
}
// مرر بيانات آمنة بس
return {
id: user.id,
name: user.name,
email: user.email,
avatar: user.avatar,
// ما تمرر: password, tokens, internalIds, etc.
};
}
ده كل اللي فيها. مكان واحد. منطق واحد. أمان واحد.
تخيل أنك تعمل في شركة كبيرة. لديك مئات المكونات. كل مكون يجلب البيانات بطريقة مختلفة. بعضها يتحقق من الأذونات، وبعضها لا. بعضها يمرر بيانات حساسة، وبعضها لا. فوضى عارمة.
ثم تحدث مشكلة أمنية. تتساءل "من أين بدأت هذه المشكلة؟" وتضطر للبحث في مئات الملفات.
مع طبقة الوصول إلى البيانات (DAL)؟ مكان واحد. تحقق من الأذونات مرة واحدة. أعد البيانات الآمنة مرة واحدة. انتهى الأمر.
// ❌ ده غلط
export default async function Dashboard() {
const users = await db.users.findMany()
return (
<div>
{users.map(user => (
<div key={user.id}>
<p>{user.name}</p>
<p>{user.email}</p>
<p>{user.passwordHash}</p> {/* 🚨 بتعرض الـ password! */}
<p>{user.apiToken}</p> {/* 🚨 بتعرض الـ token! */}
</div>
))}
</div>
)
}
// ✅ ده صح
import { getAllUsers } from '@/lib/dal'
export default async function Dashboard() {
const users = await getAllUsers()
return (
<div>
{users.map(user => (
<div key={user.id}>
<p>{user.name}</p>
<p>{user.email}</p>
{/* ما في حاجات حساسة */}
</div>
))}
</div>
)
}
الفرق؟ واحد بيعرض كل حاجة. التاني بيعرض بس اللي محتاج.
DTOs هي أشكال البيانات اللي بتمررها للمكونات. بتحتوي على البيانات الآمنة بس.
// lib/dal/types.ts
export interface UserDTO {
id: string;
name: string;
email: string;
avatar: string | null;
}
export interface PostDTO {
id: string;
title: string;
content: string;
authorId: string;
createdAt: Date;
}
export interface CommentDTO {
id: string;
text: string;
authorId: string;
postId: string;
createdAt: Date;
}
// lib/dal/users.ts
import { db } from "@/lib/db";
import type { UserDTO } from "./types";
export async function getUserById(userId: string): Promise<UserDTO> {
const user = await db.users.findUnique({
where: { id: userId },
});
if (!user) {
throw new Error("المستخدم غير موجود");
}
return {
id: user.id,
name: user.name,
email: user.email,
avatar: user.avatar,
};
}
export async function getUserByEmail(email: string): Promise<UserDTO | null> {
const user = await db.users.findUnique({
where: { email },
});
if (!user) {
return null;
}
return {
id: user.id,
name: user.name,
email: user.email,
avatar: user.avatar,
};
}
export async function getAllUsers(): Promise<UserDTO[]> {
const users = await db.users.findMany();
return users.map((user) => ({
id: user.id,
name: user.name,
email: user.email,
avatar: user.avatar,
}));
}
// app/users/page.tsx - قائمة ثابتة
import { getAllUsers } from '@/lib/dal/users'
export default async function UsersPage() {
const users = await getAllUsers()
return (
<div>
<h1>المستخدمين</h1>
<ul>
{users.map(user => (
<li key={user.id}>
<p>{user.name}</p>
<p>{user.email}</p>
</li>
))}
</ul>
</div>
)
}
// app/users/[id]/page.tsx - تفاصيل ديناميكية (Next.js 15+)
import { getUserById } from '@/lib/dal/users'
export default async function UserPage({
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const user = await getUserById(id)
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
)
}
DAL هي المكان الوحيد اللي بتتحقق فيه من الصلاحيات. ده مهم جداً.
// lib/dal/posts.ts
import { db } from "@/lib/db";
import { getCurrentUser } from "@/lib/auth";
import type { PostDTO } from "./types";
export async function getPostById(postId: string): Promise<PostDTO> {
const post = await db.posts.findUnique({
where: { id: postId },
});
if (!post) {
throw new Error("المقالة غير موجودة");
}
return {
id: post.id,
title: post.title,
content: post.content,
authorId: post.authorId,
createdAt: post.createdAt,
};
}
export async function getUserPosts(userId: string): Promise<PostDTO[]> {
// تحقق من الصلاحيات
const currentUser = await getCurrentUser();
if (!currentUser) {
throw new Error("غير مصرح");
}
// لو كان يطلب مقالات مستخدم تاني، تحقق من الصلاحيات
if (userId !== currentUser.id && !currentUser.isAdmin) {
throw new Error("ما عندك صلاحية");
}
const posts = await db.posts.findMany({
where: { authorId: userId },
});
return posts.map((post) => ({
id: post.id,
title: post.title,
content: post.content,
authorId: post.authorId,
createdAt: post.createdAt,
}));
}
export async function deletePost(postId: string): Promise<void> {
// تحقق من الصلاحيات
const currentUser = await getCurrentUser();
if (!currentUser) {
throw new Error("غير مصرح");
}
const post = await db.posts.findUnique({
where: { id: postId },
});
if (!post) {
throw new Error("المقالة غير موجودة");
}
// بس الـ admin أو صاحب المقالة يقدر يحذفها
if (post.authorId !== currentUser.id && !currentUser.isAdmin) {
throw new Error("ما عندك صلاحية");
}
await db.posts.delete({
where: { id: postId },
});
}
DAL هي المكان المثالي للتخزين المؤقت. في Next.js 16+، استخدم موجه (directive) 'use cache' مع cacheLife و cacheTag:
[!NOTE] عشان تستخدم 'use cache'، لازم تفعل cacheComponents: true في ملف next.config.ts.
// lib/dal/posts.ts
import { cacheLife, cacheTag } from "next/cache";
import { db } from "@/lib/db";
export async function getCachedPosts() {
'use cache';
cacheLife('hours');
cacheTag('posts');
return await db.posts.findMany({
select: {
id: true,
title: true,
content: true,
authorId: true,
createdAt: true,
},
});
}
export async function getCachedPostById(postId: string) {
'use cache';
cacheLife('hours');
cacheTag(`post-${postId}`);
const post = await db.posts.findUnique({
where: { id: postId },
});
if (!post) {
throw new Error("المقالة غير موجودة");
}
return {
id: post.id,
title: post.title,
content: post.content,
authorId: post.authorId,
createdAt: post.createdAt,
};
}
استخدم Server Actions مع DAL عشان تحدث البيانات:
// app/actions.ts
"use server";
import { revalidateTag, unstable_rethrow } from "next/cache";
import { createPost, updatePost, deletePost } from "@/lib/dal/posts";
export async function createPostAction(formData: FormData) {
try {
const title = formData.get("title") as string;
const content = formData.get("content") as string;
if (!title || !content) {
throw new Error("العنوان والمحتوى مطلوبان");
}
const post = await createPost(title, content);
// ألغِ الـ cache
revalidateTag("posts");
return { success: true, data: post };
} catch (error) {
unstable_rethrow(error);
return { success: false, error: error.message };
}
}
export async function updatePostAction(postId: string, formData: FormData) {
try {
const title = formData.get("title") as string;
const content = formData.get("content") as string;
const post = await updatePost(postId, title, content);
// ألغِ الـ cache
revalidateTag("posts");
revalidateTag(`post-${postId}`);
return { success: true, data: post };
} catch (error) {
unstable_rethrow(error);
return { success: false, error: error.message };
}
}
export async function deletePostAction(postId: string) {
try {
await deletePost(postId);
// ألغِ الـ cache
revalidateTag("posts");
revalidateTag(`post-${postId}`);
return { success: true };
} catch (error) {
unstable_rethrow(error);
return { success: false, error: error.message };
}
}
هنا مثال كامل بيجمع كل حاجة:
// lib/dal/index.ts
import { db } from "@/lib/db";
import { cacheLife, cacheTag } from "next/cache";
import { getCurrentUser } from "@/lib/auth";
// Types
export interface PostDTO {
id: string;
title: string;
content: string;
authorId: string;
createdAt: Date;
}
// Cached queries
export async function getCachedPosts() {
'use cache';
cacheLife('hours');
cacheTag('posts');
const posts = await db.posts.findMany({
orderBy: { createdAt: "desc" },
});
return posts.map((post) => ({
id: post.id,
title: post.title,
content: post.content,
authorId: post.authorId,
createdAt: post.createdAt,
}));
}
// Mutations
export async function createPost(
title: string,
content: string,
): Promise<PostDTO> {
const user = await getCurrentUser();
if (!user) throw new Error("غير مصرح");
const post = await db.posts.create({
data: {
title,
content,
authorId: user.id,
},
});
return {
id: post.id,
title: post.title,
content: post.content,
authorId: post.authorId,
createdAt: post.createdAt,
};
}
export async function updatePost(
postId: string,
title: string,
content: string,
): Promise<PostDTO> {
const user = await getCurrentUser();
if (!user) throw new Error("غير مصرح");
const post = await db.posts.findUnique({ where: { id: postId } });
if (!post) throw new Error("المقالة غير موجودة");
if (post.authorId !== user.id && !user.isAdmin) {
throw new Error("ما عندك صلاحية");
}
const updated = await db.posts.update({
where: { id: postId },
data: { title, content },
});
return {
id: updated.id,
title: updated.title,
content: updated.content,
authorId: updated.authorId,
createdAt: updated.createdAt,
};
}
export async function deletePost(postId: string): Promise<void> {
const user = await getCurrentUser();
if (!user) throw new Error("غير مصرح");
const post = await db.posts.findUnique({ where: { id: postId } });
if (!post) throw new Error("المقالة غير موجودة");
if (post.authorId !== user.id && !user.isAdmin) {
throw new Error("ما عندك صلاحية");
}
await db.posts.delete({ where: { id: postId } });
}
// app/blog/page.tsx
import { getCachedPosts } from '@/lib/dal'
import PostCard from '@/app/components/post-card'
import CreatePostForm from '@/app/components/create-post-form'
export default async function BlogPage() {
const posts = await getCachedPosts()
return (
<div>
<h1>المدونة</h1>
<CreatePostForm />
<div>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
</div>
)
}
// app/actions.ts
"use server";
import { revalidateTag } from "next/cache";
import { createPost } from "@/lib/dal";
export async function createPostAction(formData: FormData) {
try {
const title = formData.get("title") as string;
const content = formData.get("content") as string;
const post = await createPost(title, content);
revalidateTag("posts");
return { success: true, data: post };
} catch (error) {
return { success: false, error: error.message };
}
}
// app/components/create-post-form.tsx
'use client'
import { createPostAction } from '@/app/actions'
import { useState } from 'react'
export default function CreatePostForm() {
const [loading, setLoading] = useState(false)
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
setLoading(true)
const formData = new FormData(e.currentTarget)
const result = await createPostAction(formData)
if (result.success) {
e.currentTarget.reset()
} else {
alert(`خطأ: ${result.error}`)
}
setLoading(false)
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
name="title"
placeholder="عنوان المقالة"
required
/>
<textarea
name="content"
placeholder="محتوى المقالة"
required
/>
<button type="submit" disabled={loading}>
{loading ? 'جاري الإنشاء...' : 'إنشاء مقالة'}
</button>
</form>
)
}
مكان واحد للصلاحيات - كل التحقق من الصلاحيات في DAL
DTOs دائماً - ما تمرر بيانات قاعدة البيانات مباشرة
معالجة الأخطاء - رمي أخطاء واضحة؛ استخدم unstable_rethrow في الـ actions
التخزين المؤقت - استخدم موجه 'use cache' الحديث
الـ Invalidation - استخدم revalidateTag أو updateTag عند التحديثات
TypeScript - عرّف أنواع واضحة للـ DTOs
طبقة الوصول للبيانات ليست حاجة معقدة. ده مجرد مكان واحد بتحط فيه كل منطق جلب البيانات والتحقق من الصلاحيات. ده بيخليك:
أكتر أماناً
أكتر وضوح
أسهل في الصيانة
أسهل في الاختبار
ابدأ بـ DAL من اليوم الأول. نسختك المستقبلية من نفسك هتشكرك.
المصادر: