أهمية تصميم المخطط
مخطط GraphQL الخاص بك هو العقد بين واجهة برمجة التطبيقات الخاصة بك ومستهلكيها. المخططات المصممة جيدًا بديهية ومرنة وقابلة للصيانة. يؤدي تصميم المخطط السيئ إلى تغييرات مدمرة، وواجهات برمجة تطبيقات محيرة، ومطورين محبطين. يضمن اتباع أفضل الممارسات أن يتوسع مخططك مع تطبيقك ويظل مناسبًا للمطورين.
نهج المخطط أولاً مقابل الكود أولاً
نهجان رئيسيان لبناء مخططات GraphQL:
المخطط أولاً (SDL-First)
حدد مخططك باستخدام لغة تعريف مخطط GraphQL (SDL) أولاً، ثم نفذ المحللات:
// schema.graphql
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
}
type Query {
user(id: ID!): User
users: [User!]!
}
// resolvers.js
const resolvers = {
Query: {
user: (parent, { id }, context) => {
return context.db.getUserById(id);
},
users: (parent, args, context) => {
return context.db.getAllUsers();
}
},
User: {
posts: (user, args, context) => {
return context.db.getPostsByUserId(user.id);
}
}
};
الإيجابيات: فصل واضح، سهولة مراجعة تغييرات المخطط، رائع للفرق، مستقل عن اللغة
السلبيات: يمكن أن يصبح المخطط والكود غير متزامنين، يتطلب التحقق اليدوي
الكود أولاً (Resolver-First)
حدد المخطط باستخدام فئات TypeScript/JavaScript والمزخرفات:
import { ObjectType, Field, ID, Resolver, Query, Arg } from 'type-graphql';
@ObjectType()
class User {
@Field(type => ID)
id: string;
@Field()
name: string;
@Field()
email: string;
@Field(type => [Post])
posts: Post[];
}
@ObjectType()
class Post {
@Field(type => ID)
id: string;
@Field()
title: string;
@Field()
content: string;
@Field(type => User)
author: User;
}
@Resolver(User)
class UserResolver {
@Query(returns => User, { nullable: true })
user(@Arg('id', type => ID) id: string): Promise<User> {
return db.getUserById(id);
}
@Query(returns => [User])
users(): Promise<User[]> {
return db.getAllUsers();
}
}
الإيجابيات: أمان النوع، مخطط يتم إنشاؤه تلقائيًا، تكرار أقل، دعم IDE
السلبيات: مرتبط باللغة/الإطار، أصعب في مراجعة المخطط بشكل مستقل
التوصية: استخدم المخطط أولاً لواجهات برمجة التطبيقات العامة أو الفرق الكبيرة حيث تكون مراجعة المخطط حرجة. استخدم الكود أولاً لواجهات برمجة التطبيقات الداخلية أو عندما يكون أمان النوع أمرًا بالغ الأهمية.
اتفاقيات التسمية
تحسن التسمية المتسقة قابلية اكتشاف واجهة برمجة التطبيقات وسهولة استخدامها:
الأنواع والحقول
// ✅ جيد - تسمية واضحة ومتسقة
type User {
id: ID!
firstName: String! # camelCase للحقول
lastName: String!
emailAddress: String!
createdAt: DateTime! # أسماء واضحة وصفية
isActive: Boolean! # حقول Boolean تبدأ بـ "is/has/can"
}
type Post {
id: ID!
title: String!
publishedAt: DateTime # قابل للقيمة null للمسودات
author: User! # مفرد للكائنات الفردية
comments: [Comment!]! # جمع للقوائم
}
// ❌ سيئ - تسمية غير متسقة وغير واضحة
type user { # يجب أن تكون الأنواع PascalCase
ID: ID! # يجب أن تكون الحقول camelCase
name: String! # غامض - الاسم الأول؟ الاسم الكامل؟
email_address: String! # Snake_case غير موصى به
created: String! # نوع غامض، تنسيق غير واضح
active: Boolean! # بادئة "is" مفقودة
Author: User! # يجب أن يكون الحقل camelCase
}
الاستعلامات والطفرات
// ✅ جيد - أفعال عمل واضحة
type Query {
user(id: ID!): User # مفرد لعنصر واحد
users(limit: Int, offset: Int): [User!]! # جمع للقوائم
searchUsers(query: String!): [User!]! # فعل وصفي + اسم
}
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload!
deleteUser(id: ID!): DeleteUserPayload!
publishPost(id: ID!): PublishPostPayload! # فعل عمل واضح
}
// ❌ سيئ - تسمية غامضة أو غير متسقة
type Query {
getUser(id: ID!): User # "get" زائد في الاستعلامات
allUsers: [User!]! # غير متسق مع "user" المفرد
search(q: String!): [User!]! # عام جدًا، غير واضح ما يتم البحث عنه
}
type Mutation {
user(input: UserInput!): User! # غير واضح إذا كان إنشاء/تحديث
remove(id: ID!): Boolean! # عام جدًا، ما الذي يتم إزالته؟
post(id: ID!): Post! # إجراء غير واضح
}
أنواع الإدخال والوسائط
// ✅ جيد - أنواع إدخال واضحة قابلة لإعادة الاستخدام
input CreateUserInput {
firstName: String!
lastName: String!
email: String!
}
input UpdateUserInput {
firstName: String
lastName: String
email: String
}
input PaginationInput {
limit: Int = 10
offset: Int = 0
}
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload!
}
// ❌ سيئ - مدخلات متكررة وغير واضحة
type Mutation {
createUser(
firstName: String!,
lastName: String!,
email: String!
): User! # قوائم الوسائط الطويلة يصعب صيانتها
updateUser(
id: ID!,
data: UserData! # اسم عام، غرض غير واضح
): User!
}
أنواع الاستجابة ومعالجة الأخطاء
أرجع دائمًا أنواع حمولة منظمة بدلاً من الكائنات المباشرة:
// ✅ جيد - استجابة منظمة مع بيانات وصفية
type CreateUserPayload {
success: Boolean!
message: String
user: User
errors: [UserError!]
}
type UserError {
field: String
message: String!
code: ErrorCode!
}
enum ErrorCode {
VALIDATION_ERROR
DUPLICATE_EMAIL
INVALID_INPUT
UNAUTHORIZED
}
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
}
// الاستخدام يسمح بالنجاح الجزئي والأخطاء المفصلة
// {
// "data": {
// "createUser": {
// "success": false,
// "message": "فشل التحقق",
// "user": null,
// "errors": [
// {
// "field": "email",
// "message": "البريد الإلكتروني موجود بالفعل",
// "code": "DUPLICATE_EMAIL"
// }
// ]
// }
// }
// }
// ❌ سيئ - إرجاع كائن مباشر، أخطاء في الامتدادات
type Mutation {
createUser(input: CreateUserInput!): User!
}
// لا توجد طريقة لإرجاع بيانات جزئية أو أخطاء منظمة
// يجب أن تذهب الأخطاء إلى مصفوفة الأخطاء على المستوى الأعلى
تحذير: رمي الأخطاء في المحللات يتسبب في فشل العملية بأكملها. استخدم أنواع حمولة منظمة لإرجاع بيانات جزئية وأخطاء خاصة بالحقل.
أنماط الترقيم
نفذ ترقيمًا متسقًا عبر مخططك:
// ترقيم المؤشر بنمط Relay (موصى به لمجموعات البيانات الكبيرة)
type Query {
users(first: Int, after: String, last: Int, before: String): UserConnection!
}
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type UserEdge {
cursor: String!
node: User!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
// ترقيم الإزاحة (أبسط، جيد لمجموعات البيانات الصغيرة)
type Query {
users(limit: Int = 10, offset: Int = 0): UserList!
}
type UserList {
items: [User!]!
totalCount: Int!
limit: Int!
offset: Int!
}
إصدار المخطط وتطوره
يجب أن تتطور مخططات GraphQL دون تغييرات مدمرة:
إضافة الحقول (آمن)
// الإصدار 1
type User {
id: ID!
name: String!
}
// الإصدار 2 - إضافة حقل اختياري جديد (غير مدمر)
type User {
id: ID!
name: String!
email: String # حقل اختياري جديد
}
إهمال الحقول
type User {
id: ID!
name: String! @deprecated(reason: "استخدم firstName و lastName بدلاً من ذلك")
firstName: String!
lastName: String!
# الحقل القديم لا يزال يعمل، لكن العملاء يتم تحذيرهم
age: Int @deprecated(reason: "استخدم birthDate لحساب العمر الدقيق")
birthDate: DateTime!
}
type Query {
users: [User!]! @deprecated(reason: "استخدم searchUsers مع الترقيم")
searchUsers(
query: String,
first: Int,
after: String
): UserConnection!
}
استراتيجية الإهمال:
- أضف توجيه
@deprecated مع مسار ترحيل واضح
- راقب استخدام الحقول المهملة
- تواصل مع جدول زمني لإنهاء العمل مع مستهلكي واجهة برمجة التطبيقات
- أزل الحقول المهملة في الإصدار الرئيسي التالي
التغييرات المدمرة التي يجب تجنبها
// ❌ مدمر: إزالة حقل
type User {
id: ID!
# name: String! # تمت الإزالة - يكسر الاستعلامات الموجودة
}
// ❌ مدمر: تغيير نوع الحقل
type User {
id: ID!
age: String! # تغير من Int! إلى String! - يكسر العملاء
}
// ❌ مدمر: جعل الحقل الاختياري مطلوبًا
type User {
id: ID!
email: String! # تغير من String إلى String! - يكسر الطفرات
}
// ❌ مدمر: تغيير متطلبات الوسيطة
type Query {
user(id: ID!, email: String!): User # تمت إضافة وسيطة مطلوبة - يكسر الاستعلامات
}
// ✅ آمن: استراتيجية التطور
type User {
id: ID!
name: String! @deprecated(reason: "استخدم displayName")
displayName: String! # أضف حقلاً جديدًا أولاً
age: Int @deprecated(reason: "استخدم birthDate")
birthDate: DateTime # أضف بديلاً اختياريًا
}
تقسيم المخططات الكبيرة إلى وحدات
قسم المخططات الكبيرة إلى وحدات منطقية:
// schema/user.graphql
type User {
id: ID!
name: String!
posts: [Post!]!
}
extend type Query {
user(id: ID!): User
users: [User!]!
}
extend type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
}
// schema/post.graphql
type Post {
id: ID!
title: String!
author: User!
}
extend type Query {
post(id: ID!): Post
posts: [Post!]!
}
extend type Mutation {
createPost(input: CreatePostInput!): CreatePostPayload!
}
// schema/index.js
const { mergeTypeDefs } = require('@graphql-tools/merge');
const { loadFilesSync } = require('@graphql-tools/load-files');
const path = require('path');
const typesArray = loadFilesSync(path.join(__dirname, './'), {
extensions: ['graphql']
});
const typeDefs = mergeTypeDefs(typesArray);
module.exports = typeDefs;
توثيق المخطط
وثق مخططك بالأوصاف:
"""
يمثل مستخدمًا في النظام.
يمكن للمستخدمين إنشاء منشورات وتعليقات والتفاعل مع مستخدمين آخرين.
"""
type User {
"""معرف فريد للمستخدم"""
id: ID!
"""اسم العرض الكامل للمستخدم"""
name: String!
"""
عنوان البريد الإلكتروني للمستخدم.
هذا الحقل مرئي فقط للمستخدم نفسه والمسؤولين.
"""
email: String!
"""
جميع المنشورات التي أنشأها هذا المستخدم.
النتائج مرقمة ومرتبة حسب تاريخ الإنشاء (الأحدث أولاً).
"""
posts(first: Int = 10, after: String): PostConnection!
"""الطابع الزمني عند إنشاء حساب المستخدم"""
createdAt: DateTime!
}
"""
إدخال لإنشاء حساب مستخدم جديد.
جميع الحقول مطلوبة أثناء تسجيل المستخدم.
"""
input CreateUserInput {
"""الاسم الكامل للمستخدم (2-100 حرفًا)"""
name: String!
"""عنوان بريد إلكتروني صالح (يجب أن يكون فريدًا)"""
email: String!
"""كلمة المرور (8 أحرف على الأقل، يجب أن تتضمن أحرفًا وأرقامًا)"""
password: String!
}
نصيحة: استخدم علامات اقتباس ثلاثية (""") للأوصاف متعددة الأسطر. تظهر الأوصاف في GraphQL Playground وأدوات التوثيق والإكمال التلقائي لـ IDE.
التحقق من صحة المخطط والتحليل
استخدم الأدوات لفرض أفضل ممارسات المخطط:
// .graphql-config.yml
schema: "src/schema/**/*.graphql"
documents: "src/**/*.{graphql,js,ts}"
extensions:
validation:
rules:
- naming-convention:
types: PascalCase
fields: camelCase
arguments: camelCase
enums: UPPER_CASE
- no-deprecated
- require-description
- no-typename-prefix
// package.json
{
"scripts": {
"lint:schema": "graphql-schema-linter src/schema/**/*.graphql",
"validate:schema": "graphql-inspector validate 'src/schema/**/*.graphql'"
}
}
// تشغيل التحليل
npm run lint:schema
// المخرجات:
// ✅ جميع الأنواع تتبع PascalCase
// ❌ الحقل "User.Email" يجب أن يكون camelCase
// ❌ النوع "Post" يفتقد إلى الوصف
تمرين: صمم مخطط GraphQL كامل لمنصة تجارة إلكترونية باتباع أفضل الممارسات:
- أنشئ أنواعًا لـ Product وOrder وCustomer وReview مع تسمية مناسبة
- نفذ ترقيمًا قائمًا على المؤشر لقوائم المنتجات
- صمم أنواع حمولة منظمة لجميع الطفرات
- أضف أوصافًا على مستوى الحقل لجميع الأنواع
- قم بتضمين استراتيجية إهمال لإعادة تسمية "Customer" إلى "User"
- قسم المخطط إلى ملفات وحدة حسب المجال
- أضف رموز أخطاء شاملة وأنواع أخطاء
وثق قرارات التصميم واستراتيجية التطور الخاصة بك.