GraphQL مقابل REST
GraphQL و REST هما نهجان مختلفان لبناء واجهات API. بينما كانت REST هي المعيار السائد لأكثر من عقد من الزمان، ظهرت GraphQL كبديل قوي. فهم متى تستخدم كل منهما—أو حتى دمجهما—أمر بالغ الأهمية لتصميم API الحديث.
فهم REST
REST (نقل الحالة التمثيلية) هو نمط معماري يستخدم طرق HTTP وعناوين URL لإجراء عمليات CRUD على الموارد.
خصائص REST:
- قائم على الموارد: كل شيء هو مورد (المستخدمون، المنشورات، الطلبات)
- نقاط نهاية متعددة: كل مورد له نقطة نهاية خاصة به
- طرق HTTP: GET, POST, PUT, PATCH, DELETE
- استجابات ثابتة: الخادم يحدد بنية الاستجابة
- عديم الحالة: كل طلب يحتوي على جميع المعلومات الضرورية
// أمثلة REST API
// الحصول على جميع المستخدمين
GET /api/v1/users
// الحصول على مستخدم محدد
GET /api/v1/users/123
// الحصول على منشورات المستخدم
GET /api/v1/users/123/posts
// الحصول على منشور محدد
GET /api/v1/posts/456
// الحصول على تعليقات المنشور
GET /api/v1/posts/456/comments
// الاستجابة: بنية ثابتة يحددها الخادم
{
"data": {
"id": 123,
"type": "users",
"attributes": {
"name": "John Doe",
"email": "john@example.com",
"created_at": "2024-01-01T00:00:00Z",
"bio": "Developer",
"avatar_url": "https://...",
"followers_count": 150,
"following_count": 75
}
}
}
فهم GraphQL
GraphQL هي لغة استعلام لواجهات API تسمح للعملاء بطلب البيانات التي يحتاجونها بالضبط، لا أكثر ولا أقل.
خصائص GraphQL:
- نقطة نهاية واحدة: عادة نقطة نهاية واحدة (مثل /graphql)
- مدفوعة من العميل: العميل يحدد متطلبات البيانات بالضبط
- مطبوعة بقوة: المخطط يحدد جميع العمليات المتاحة
- لا جلب زائد/ناقص: احصل على ما تطلبه بالضبط
- الاستبطان: المخطط ذاتي التوثيق
# أمثلة استعلام GraphQL
# الحصول على حقول مستخدم محددة فقط
query {
user(id: 123) {
name
email
}
}
# الاستجابة: الحقول المطلوبة فقط
{
"data": {
"user": {
"name": "John Doe",
"email": "john@example.com"
}
}
}
# الحصول على مستخدم مع علاقات متداخلة في طلب واحد
query {
user(id: 123) {
name
email
posts(limit: 5) {
id
title
createdAt
comments(limit: 3) {
id
text
author {
name
}
}
}
}
}
# الطفرات (عمليات الكتابة)
mutation {
createPost(input: {
title: "منشوري الجديد"
content: "محتوى المنشور هنا"
}) {
id
title
createdAt
}
}
إعداد GraphQL في Laravel
# تثبيت Lighthouse (مكتبة Laravel GraphQL)
composer require nuwave/lighthouse
# نشر التكوين
php artisan vendor:publish --tag=lighthouse-config
# نشر المخطط الافتراضي
php artisan vendor:publish --tag=lighthouse-schema
# graphql/schema.graphql
"تعريف نوع المستخدم"
type User {
id: ID!
name: String!
email: String!
posts: [Post!]! @hasMany
createdAt: DateTime!
}
"تعريف نوع المنشور"
type Post {
id: ID!
title: String!
content: String!
author: User! @belongsTo
comments: [Comment!]! @hasMany
createdAt: DateTime!
updatedAt: DateTime!
}
"تعريف نوع التعليق"
type Comment {
id: ID!
text: String!
author: User! @belongsTo
post: Post! @belongsTo
createdAt: DateTime!
}
"نوع الاستعلام الجذر"
type Query {
"الحصول على جميع المستخدمين مع تصفية اختيارية"
users(
name: String @where(operator: "like")
orderBy: [OrderByClause!] @orderBy
): [User!]! @paginate(defaultCount: 10)
"الحصول على مستخدم واحد بواسطة المعرف"
user(id: ID! @eq): User @find
"الحصول على المستخدم المصادق عليه"
me: User @auth
"الحصول على جميع المنشورات"
posts(
title: String @where(operator: "like")
orderBy: [OrderByClause!] @orderBy
): [Post!]! @paginate(defaultCount: 20)
"الحصول على منشور واحد"
post(id: ID! @eq): Post @find
}
"نوع الطفرة الجذر"
type Mutation {
"إنشاء منشور جديد"
createPost(input: CreatePostInput! @spread): Post
@create
@inject(context: "user.id", name: "user_id")
"تحديث منشور موجود"
updatePost(id: ID!, input: UpdatePostInput! @spread): Post
@update
"حذف منشور"
deletePost(id: ID! @eq): Post @delete
"تسجيل دخول المستخدم"
login(email: String!, password: String!): AuthPayload!
}
"إدخال لإنشاء المنشورات"
input CreatePostInput {
title: String! @rules(apply: ["required", "max:255"])
content: String! @rules(apply: ["required"])
}
"إدخال لتحديث المنشورات"
input UpdatePostInput {
title: String @rules(apply: ["max:255"])
content: String
}
"حمولة المصادقة"
type AuthPayload {
token: String!
user: User!
}
"معلومات الترقيم"
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
<?php
// app/GraphQL/Mutations/Login.php
namespace App\GraphQL\Mutations;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use GraphQL\Error\Error;
class Login
{
public function __invoke($rootValue, array $args): array
{
$user = User::where('email', $args['email'])->first();
if (!$user || !Hash::check($args['password'], $user->password)) {
throw new Error('بيانات اعتماد غير صالحة');
}
$token = $user->createToken('api-token')->plainTextToken;
return [
'token' => $token,
'user' => $user
];
}
}
الاختلافات الرئيسية: REST مقابل GraphQL
1. جلب البيانات
// REST: طلبات متعددة للبيانات المتداخلة
// الطلب 1: الحصول على المستخدم
GET /api/v1/users/123
// الاستجابة: كائن المستخدم الكامل مع العديد من الحقول غير المستخدمة
// الطلب 2: الحصول على منشورات المستخدم
GET /api/v1/users/123/posts
// الاستجابة: مصفوفة المنشورات مع جميع الحقول
// الطلب 3: الحصول على تعليقات لكل منشور
GET /api/v1/posts/1/comments
GET /api/v1/posts/2/comments
GET /api/v1/posts/3/comments
// المجموع: 5+ طلبات HTTP، الكثير من البيانات غير الضرورية
// ---
// GraphQL: طلب واحد للبيانات المتداخلة
query {
user(id: 123) {
name
email
posts(limit: 3) {
title
comments(limit: 2) {
text
author {
name
}
}
}
}
}
// المجموع: طلب HTTP واحد، الحقول المطلوبة فقط
2. الجلب الزائد والناقص
مشاكل REST:
- الجلب الزائد: الحصول على بيانات أكثر من المطلوب (مثل كائن المستخدم الكامل عندما تحتاج الاسم فقط)
- الجلب الناقص: عدم الحصول على بيانات كافية، مما يتطلب طلبات إضافية
حل GraphQL:
العميل يطلب بالضبط ما يحتاجه—لا أكثر ولا أقل.
3. الإصدار
// REST: عادةً يستخدم إصدار URL
GET /api/v1/users/123
GET /api/v2/users/123 // إصدار جديد مع استجابة مختلفة
// المشكلة: الحفاظ على إصدارات متعددة
// - /api/v1/users
// - /api/v2/users
// - /api/v3/users
// ---
// GraphQL: تطور المخطط بدون إصدار
type User {
id: ID!
name: String!
email: String!
fullName: String! @deprecated(reason: "استخدم 'name' بدلاً من ذلك")
posts: [Post!]!
articles: [Post!]! @deprecated(reason: "استخدم 'posts' بدلاً من ذلك")
}
// إضافة حقول جديدة دون كسر الاستعلامات الموجودة
// إهمال الحقول القديمة بلطف
// نقطة نهاية واحدة تتطور مع مرور الوقت
4. معالجة الأخطاء
// REST: رموز حالة HTTP
// 200 OK, 201 Created, 400 Bad Request, 401 Unauthorized, 404 Not Found, 500 Server Error
{
"errors": [{
"status": "422",
"title": "خطأ التحقق",
"detail": "البريد الإلكتروني مطلوب",
"source": { "pointer": "/data/attributes/email" }
}]
}
// ---
// GraphQL: يعيد دائماً 200، الأخطاء في نص الاستجابة
{
"data": {
"createPost": null
},
"errors": [
{
"message": "خطأ التحقق",
"extensions": {
"validation": {
"title": ["حقل العنوان مطلوب"],
"content": ["حقل المحتوى مطلوب"]
}
},
"path": ["createPost"]
}
]
}
// يمكن إرجاع بيانات جزئية مع أخطاء
{
"data": {
"user": {
"name": "John Doe",
"posts": null // فشل التحميل
}
},
"errors": [
{
"message": "فشل تحميل المنشورات",
"path": ["user", "posts"]
}
]
}
متى تستخدم REST
REST أفضل عندما:
- عمليات CRUD البسيطة: إدارة موارد مباشرة
- التخزين المؤقت أمر بالغ الأهمية: التخزين المؤقت HTTP يعمل مباشرة
- تحميل/تنزيل الملفات: REST يتعامل مع البيانات الثنائية بشكل أكثر طبيعية
- معرفة الفريق: الفريق يعرف REST جيداً
- التكاملات مع طرف ثالث: العديد من الخدمات تتوقع REST
- واجهات API العامة: REST مفهوم على نطاق أوسع
- الخدمات الصغيرة: الاتصال الداخلي بين الخدمات
متى تستخدم GraphQL
GraphQL أفضل عندما:
- متطلبات البيانات المعقدة: العلاقات المتداخلة والاستعلامات المرنة
- تطبيقات الجوال: تقليل النطاق الترددي والطلبات
- التكرار السريع: يمكن للواجهة الأمامية طلب حقول جديدة دون تغييرات في الخلفية
- عملاء متعددون: عملاء مختلفون يحتاجون أشكال بيانات مختلفة
- التحديثات في الوقت الفعلي: اشتراكات GraphQL للبيانات المباشرة
- تجربة المطور: مخطط ذاتي التوثيق، أدوات ممتازة
- التجميع: دمج البيانات من مصادر متعددة
النهج الهجين
لا يتعين عليك الاختيار بين أحدهما أو الآخر. تستخدم العديد من الشركات كلاهما:
<?php
// API هجين: REST + GraphQL
// routes/api.php
// نقاط نهاية REST للعمليات البسيطة
Route::prefix('v1')->group(function () {
Route::apiResource('users', UserController::class);
Route::post('login', [AuthController::class, 'login']);
Route::post('logout', [AuthController::class, 'logout']);
// تحميل الملفات عبر REST
Route::post('upload', [UploadController::class, 'store']);
});
// نقطة نهاية GraphQL للاستعلامات المعقدة
Route::post('/graphql', [GraphQLController::class, 'handle']);
// ملعب GraphQL في التطوير
if (app()->environment('local')) {
Route::get('/graphql-playground', [GraphQLController::class, 'playground']);
}
استراتيجيات الترحيل
الترحيل من REST إلى GraphQL
<?php
// نهج الترحيل التدريجي
// المرحلة 1: إضافة GraphQL جنباً إلى جنب مع REST
// - احتفظ بجميع نقاط نهاية REST
// - أضف نقطة نهاية GraphQL
// - دع العملاء يختارون
// المرحلة 2: تنفيذ محللات GraphQL باستخدام المنطق الموجود
class UserResolver
{
public function __invoke($root, array $args)
{
// إعادة استخدام منطق متحكم REST الموجود
return app(UserController::class)->show($args['id']);
}
}
// المرحلة 3: إهمال نقاط نهاية REST تدريجياً
Route::prefix('v1')->group(function () {
// وضع علامة كمهمل في التوثيق
Route::get('users/{id}', [UserController::class, 'show'])
->middleware(AddDeprecationHeader::class);
});
// المرحلة 4: مراقبة الاستخدام وإزالة النقاط غير المستخدمة
// - تتبع مقاييس استخدام API
// - إخطار العملاء بالجدول الزمني للإهمال
// - إزالة النقاط بدون استخدام
إضافة REST إلى GraphQL
<?php
// كشف استعلامات GraphQL كنقاط نهاية REST
Route::get('/api/users/{id}', function ($id) {
$query = '''
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
posts {
id
title
}
}
}
''';
$result = GraphQL::query($query, ['id' => $id]);
return response()->json($result);
});
// هذا يمنحك نقاط نهاية تشبه REST مع قوة GraphQL تحتها
اعتبارات الأداء
مشكلة N+1 في GraphQL:
يمكن أن تتسبب GraphQL في استعلامات N+1 إذا لم تكن حذراً:
# استعلام يسبب مشكلة N+1
query {
posts { # استعلام واحد: SELECT * FROM posts
id
title
author { # N استعلامات: SELECT * FROM users WHERE id = ?
name # (استعلام واحد لكل منشور!)
}
}
}
# الحل 1: DataLoader (التجميع)
# الحل 2: التحميل المسبق في المحللات
<?php
// استخدم التحميل المسبق في Laravel
class PostResolver
{
public function __invoke()
{
return Post::with('author')->get(); // استعلام واحد مع JOIN
}
}
// أو استخدم توجيه @with في Lighthouse
type Query {
posts: [Post!]! @all @with(relation: "author")
}
ملخص المقارنة
┌─────────────────────┬──────────────────────────┬──────────────────────────┐
│ الميزة │ REST │ GraphQL │
├─────────────────────┼──────────────────────────┼──────────────────────────┤
│ نقاط النهاية │ متعددة │ واحدة │
│ جلب البيانات │ ثابت من الخادم │ محدد من العميل │
│ الجلب الزائد │ شائع │ لا يوجد │
│ الجلب الناقص │ شائع │ لا يوجد │
│ الإصدار │ على أساس URL (v1, v2) │ تطور المخطط │
│ التخزين المؤقت │ مدمج في HTTP │ يتطلب التنفيذ │
│ منحنى التعلم │ منخفض │ متوسط-مرتفع │
│ الأدوات │ ناضجة │ ممتازة ونامية │
│ تحميل الملفات │ طبيعي │ أكثر تعقيداً │
│ الوقت الفعلي │ WebSockets/SSE │ الاشتراكات │
│ القابلية للاكتشاف │ يحتاج توثيق │ الاستبطان مدمج │
│ صديق للجوال │ يمكن أن يكون غير فعال │ فعال جداً │
└─────────────────────┴──────────────────────────┴──────────────────────────┘
تمرين:
- ابنِ نفس ميزة API باستخدام كل من REST و GraphQL
- قارن عدد الطلبات المطلوبة لجلب البيانات المتداخلة
- نفذ مخطط GraphQL مع العلاقات
- أضف المصادقة إلى نقاط نهاية REST و GraphQL
- قس اختلافات الأداء بين النهجين
- صمم استراتيجية ترحيل من REST إلى GraphQL لمشروع موجود
- نفذ DataLoader لحل مشكلة N+1 في GraphQL
الحكم النهائي:
لا REST ولا GraphQL أفضل عالمياً. الاختيار الصحيح يعتمد على حالة استخدامك المحددة:
- استخدم REST لواجهات API البسيطة القابلة للتخزين المؤقت مع موارد محددة جيداً
- استخدم GraphQL لمتطلبات البيانات المعقدة وأنواع العملاء المتعددة
- استخدم كلاهما إذا كنت بحاجة إلى نقاط قوة كل منهما لحالات استخدام مختلفة
تستخدم العديد من الشركات الناجحة نهج هجين، بالاستفادة من REST للعمليات البسيطة و GraphQL للاستعلامات المعقدة.