"بطل تمرير البيانات الخام من قاعدة البيانات للمكونات. طبقة الوصول للبيانات هي الحل. هنا ليه ده مهم وإزاي تبنيها بشكل صح."
المشكلة اللي بتحصل في كل مشروع
تخيل ده: عندك 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>
)
}
الفرق؟ واحد بيعرض كل حاجة. التاني بيعرض بس اللي محتاج.
بناء DAL بتاعتك
الخطوة الأولى: عرّف Data Transfer Objects (DTOs)
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:
Pro Tip!
[!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: تحديث البيانات بأمان
استخدم 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 };
}
}
مثال كامل: نظام مدونة
هنا مثال كامل بيجمع كل حاجة:
الـ DAL
// 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 } });
}
الـ Server Component
// 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>
)
}
الـ Server Action
// 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 };
}
}
الـ Form Component
// 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 من اليوم الأول. نسختك المستقبلية من نفسك هتشكرك.
المصادر:
ترميز سعيد! — Ahmed Fahmy