فهم الأنواع المجردة
يوفر GraphQL ميزتين قويتين للتعامل مع البيانات متعددة الأشكال: الواجهات وأنواع الاتحاد. تسمح هذه الأنواع المجردة بتحديد حقول يمكن أن ترجع أنواع كائنات مختلفة متعددة، مما يجعل مخططك أكثر مرونة وتعبيرًا. إنها ضرورية لنمذجة سيناريوهات العالم الحقيقي حيث تشترك الكيانات في خصائص مشتركة أو يمكن أن تكون من أنواع مختلفة.
واجهات GraphQL
الواجهة هي نوع مجرد يحدد مجموعة من الحقول التي يجب أن تتضمنها الأنواع المنفذة:
interface Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
}
type User implements Node {
# الحقول المطلوبة من واجهة Node
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
# حقول إضافية خاصة بـ User
name: String!
email: String!
posts: [Post!]!
}
type Post implements Node {
# الحقول المطلوبة من واجهة Node
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
# حقول إضافية خاصة بـ Post
title: String!
content: String!
author: User!
}
type Comment implements Node {
# الحقول المطلوبة من واجهة Node
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
# حقول إضافية خاصة بـ Comment
text: String!
author: User!
post: Post!
}
type Query {
# إرجاع أي نوع ينفذ Node
node(id: ID!): Node
# البحث عبر جميع أنواع Node
search(query: String!): [Node!]!
}
ملاحظة: يجب أن يتضمن أي نوع ينفذ واجهة جميع الحقول المحددة في الواجهة بنفس الأنواع. يمكن للأنواع المنفذة إضافة حقول إضافية بما يتجاوز ما تتطلبه الواجهة.
الاستعلام عن أنواع الواجهة
استخدم الأجزاء المضمنة للاستعلام عن الحقول الخاصة بالنوع:
query SearchContent {
search(query: "GraphQL") {
# الحقول المشتركة المتاحة على جميع أنواع Node
id
createdAt
updatedAt
# استخدم __typename لتحديد النوع المحدد
__typename
# أجزاء مضمنة للحقول الخاصة بالنوع
... on User {
name
email
}
... on Post {
title
content
author {
name
}
}
... on Comment {
text
author {
name
}
post {
title
}
}
}
}
# الاستجابة:
{
"data": {
"search": [
{
"id": "1",
"createdAt": "2026-01-15T10:00:00Z",
"updatedAt": "2026-01-15T10:00:00Z",
"__typename": "Post",
"title": "مقدمة إلى GraphQL",
"content": "GraphQL هي لغة استعلام...",
"author": { "name": "جون دو" }
},
{
"id": "2",
"createdAt": "2026-01-16T11:30:00Z",
"updatedAt": "2026-01-16T11:30:00Z",
"__typename": "User",
"name": "جين سميث",
"email": "jane@example.com"
}
]
}
}
تنفيذ واجهات متعددة
يمكن للنوع تنفيذ واجهات متعددة:
interface Node {
id: ID!
}
interface Timestamped {
createdAt: DateTime!
updatedAt: DateTime!
}
interface Authored {
author: User!
publishedAt: DateTime
}
# Post ينفذ جميع الواجهات الثلاث
type Post implements Node & Timestamped & Authored {
# من Node
id: ID!
# من Timestamped
createdAt: DateTime!
updatedAt: DateTime!
# من Authored
author: User!
publishedAt: DateTime
# حقول خاصة بـ Post
title: String!
content: String!
tags: [String!]!
}
# Comment ينفذ Node و Timestamped
type Comment implements Node & Timestamped {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
text: String!
author: User!
}
type Query {
# إرجاع أي محتوى مؤلف
authoredContent(authorId: ID!): [Authored!]!
# إرجاع أي كيان موقوت
recentActivity: [Timestamped!]!
}
نصيحة: استخدم واجهات متعددة لتكوين السلوكيات المشتركة. هذا يتبع مبدأ فصل الواجهة ويجعل مخططك أكثر قابلية للصيانة.
أنواع الاتحاد
يمثل نوع الاتحاد كائنًا يمكن أن يكون أحد عدة أنواع، دون الحاجة إلى حقول مشتركة:
type User {
id: ID!
name: String!
email: String!
}
type Post {
id: ID!
title: String!
content: String!
}
type Comment {
id: ID!
text: String!
}
# SearchResult يمكن أن يكون User أو Post أو Comment
union SearchResult = User | Post | Comment
type Query {
search(query: String!): [SearchResult!]!
}
# المحللات
const resolvers = {
Query: {
search: async (parent, { query }, context) => {
// البحث عبر مجموعات متعددة
const users = await context.db.users.find({ name: query });
const posts = await context.db.posts.find({ title: query });
const comments = await context.db.comments.find({ text: query });
// إرجاع مصفوفة مختلطة من أنواع مختلفة
return [...users, ...posts, ...comments];
}
},
SearchResult: {
__resolveType(obj) {
// تحديد النوع بناءً على خصائص الكائن
if (obj.email) return 'User';
if (obj.title) return 'Post';
if (obj.text) return 'Comment';
return null;
}
}
};
الاستعلام عن أنواع الاتحاد
استخدم الأجزاء المضمنة للوصول إلى الحقول في أنواع أعضاء الاتحاد المحددة:
query SearchAll {
search(query: "GraphQL") {
__typename
... on User {
id
name
email
}
... on Post {
id
title
content
}
... on Comment {
id
text
}
}
}
# استجابة مع أنواع مختلطة:
{
"data": {
"search": [
{
"__typename": "User",
"id": "1",
"name": "خبير GraphQL",
"email": "expert@example.com"
},
{
"__typename": "Post",
"id": "10",
"title": "أفضل ممارسات GraphQL",
"content": "إليك بعض النصائح..."
},
{
"__typename": "Comment",
"id": "50",
"text": "شرح رائع لـ GraphQL!"
}
]
}
}
تحذير: لا تشترك أنواع الاتحاد في أي حقول مشتركة. يجب عليك استخدام أجزاء مضمنة للوصول إلى أي حقول. إذا كان يجب أن تشترك الأنواع في حقول، فكر في استخدام الواجهات بدلاً من ذلك.
الواجهات مقابل أنواع الاتحاد
اختر النوع المجرد المناسب لحالتك الاستخدامية:
# ✅ استخدم الواجهة عندما تشترك الأنواع في حقول مشتركة
interface Media {
id: ID!
title: String!
url: String!
uploadedAt: DateTime!
}
type Image implements Media {
id: ID!
title: String!
url: String!
uploadedAt: DateTime!
width: Int!
height: Int!
format: String!
}
type Video implements Media {
id: ID!
title: String!
url: String!
uploadedAt: DateTime!
duration: Int!
resolution: String!
}
type Query {
media(id: ID!): Media
allMedia: [Media!]!
}
# يمكن للاستعلام الوصول إلى الحقول المشتركة بدون أجزاء
query GetAllMedia {
allMedia {
id
title
url
uploadedAt
# الحقول الخاصة بالنوع تحتاج إلى أجزاء
... on Image {
width
height
}
... on Video {
duration
}
}
}
# ✅ استخدم الاتحاد عندما ترتبط الأنواع مفاهيميًا لكنها لا تشترك في حقول
type TextPost {
id: ID!
text: String!
}
type ImagePost {
id: ID!
imageUrl: String!
caption: String!
}
type VideoPost {
id: ID!
videoUrl: String!
thumbnail: String!
}
union FeedItem = TextPost | ImagePost | VideoPost
type Query {
feed: [FeedItem!]!
}
# يتطلب الاستعلام أجزاء لجميع الحقول
query GetFeed {
feed {
__typename
... on TextPost {
id
text
}
... on ImagePost {
id
imageUrl
caption
}
... on VideoPost {
id
videoUrl
thumbnail
}
}
}
حقل __typename
يُرجع حقل __typename الوصفي اسم نوع الكائن:
query GetContent {
search(query: "tutorial") {
__typename # متاح دائمًا على أي نوع كائن
... on Post {
id
title
}
... on Video {
id
duration
}
}
}
# المعالجة من جانب العميل بناءً على __typename
function renderSearchResult(result) {
switch (result.__typename) {
case 'Post':
return <PostCard post={result} />;
case 'Video':
return <VideoPlayer video={result} />;
case 'User':
return <UserProfile user={result} />;
default:
return null;
}
}
// Apollo Client يتضمن تلقائيًا __typename
// يتم استخدامه لتطبيع التخزين المؤقت وتحديد النوع
const { data } = useQuery(SEARCH_QUERY);
data.search.forEach(result => {
console.log(result.__typename); // 'Post' أو 'Video' أو 'User'
});
حل النوع
نفذ __resolveType لتحديد الأنواع الملموسة:
// للواجهات
const resolvers = {
Node: {
__resolveType(obj, context, info) {
// تحقق من حقل المميز
if (obj.type === 'USER') return 'User';
if (obj.type === 'POST') return 'Post';
if (obj.type === 'COMMENT') return 'Comment';
// أو تحقق من الحقول الفريدة
if (obj.email) return 'User';
if (obj.title && obj.content) return 'Post';
if (obj.text && obj.postId) return 'Comment';
// أو استخدم instanceof (إذا كنت تستخدم فئات)
if (obj instanceof User) return 'User';
if (obj instanceof Post) return 'Post';
return null; // نوع غير معروف
}
}
};
// لأنواع الاتحاد
const resolvers = {
SearchResult: {
__resolveType(obj) {
// استخدم حقل النوع الصريح إذا كان متاحًا
if (obj.__typename) return obj.__typename;
// خلاف ذلك استنتج من الهيكل
if (obj.email && obj.posts) return 'User';
if (obj.title && obj.author) return 'Post';
if (obj.text && obj.post) return 'Comment';
throw new Error('تعذر حل النوع');
}
}
};
// بديل: إرجاع كائنات مع __typename
const resolvers = {
Query: {
search: async (parent, { query }, context) => {
const users = await context.db.users.find({ name: query });
const posts = await context.db.posts.find({ title: query });
return [
...users.map(u => ({ ...u, __typename: 'User' })),
...posts.map(p => ({ ...p, __typename: 'Post' }))
];
}
}
};
أفضل ممارسة: قم بتضمين حقل مميز __typename أو type في نماذج قاعدة البيانات الخاصة بك. هذا يجعل حل النوع بسيطًا وصريحًا.
الاستعلامات متعددة الأشكال
مثال من العالم الحقيقي: موجز النشاط مع أنواع محتوى متعددة:
interface Activity {
id: ID!
timestamp: DateTime!
actor: User!
}
type PostCreated implements Activity {
id: ID!
timestamp: DateTime!
actor: User!
post: Post!
}
type CommentAdded implements Activity {
id: ID!
timestamp: DateTime!
actor: User!
comment: Comment!
post: Post!
}
type UserFollowed implements Activity {
id: ID!
timestamp: DateTime!
actor: User!
followedUser: User!
}
type LikeReceived implements Activity {
id: ID!
timestamp: DateTime!
actor: User!
likedContent: LikeableContent!
}
union LikeableContent = Post | Comment
type Query {
# موجز النشاط يعرض جميع الأنواع
activityFeed(limit: Int = 20): [Activity!]!
}
# الاستعلام مع الأجزاء
query GetActivityFeed {
activityFeed(limit: 10) {
# الحقول المشتركة
id
timestamp
actor {
name
avatar
}
# الحقول الخاصة بالنوع
... on PostCreated {
post {
title
excerpt
}
}
... on CommentAdded {
comment {
text
}
post {
title
}
}
... on UserFollowed {
followedUser {
name
avatar
}
}
... on LikeReceived {
likedContent {
__typename
... on Post {
title
}
... on Comment {
text
}
}
}
}
}
# المحللات
const resolvers = {
Query: {
activityFeed: async (parent, { limit }, { userId, db }) => {
// جلب ودمج الأنشطة من جداول مختلفة
const activities = await db.activities
.find({ userId })
.sort({ timestamp: -1 })
.limit(limit);
return activities; // يتضمن بالفعل __typename من قاعدة البيانات
}
},
Activity: {
__resolveType(obj) {
return obj.__typename; // 'PostCreated', 'CommentAdded', إلخ.
}
},
LikeableContent: {
__resolveType(obj) {
return obj.__typename; // 'Post' أو 'Comment'
}
}
};
تركيب الأجزاء
أعد استخدام الأجزاء لاستعلامات أنظف:
# حدد أجزاء قابلة لإعادة الاستخدام
fragment UserInfo on User {
id
name
avatar
email
}
fragment PostPreview on Post {
id
title
excerpt
publishedAt
}
fragment CommentPreview on Comment {
id
text
createdAt
}
# الاستخدام في استعلام متعدد الأشكال
query GetSearchResults {
search(query: "GraphQL") {
__typename
... on User {
...UserInfo
posts {
...PostPreview
}
}
... on Post {
...PostPreview
author {
...UserInfo
}
comments {
...CommentPreview
}
}
... on Comment {
...CommentPreview
author {
...UserInfo
}
}
}
}
نصيحة: استخدم تركيب الأجزاء للحفاظ على قابلية قراءة وصيانة الاستعلامات متعددة الأشكال. حدد الأجزاء مرة واحدة وأعد استخدامها عبر استعلامات متعددة.
امتدادات الواجهة
قم بتوسيع الواجهات لإضافة أنواع منفذة جديدة:
# المخطط الأساسي
interface Node {
id: ID!
}
type User implements Node {
id: ID!
name: String!
}
# الامتداد في وحدة أخرى
extend interface Node {
createdAt: DateTime! # إضافة حقل إلى الواجهة
}
# يجب أن تتضمن جميع الأنواع المنفذة الآن createdAt
type User implements Node {
id: ID!
name: String!
createdAt: DateTime! # مطلوب بعد امتداد الواجهة
}
# إضافة نوع جديد ينفذ واجهة موجودة
type Organization implements Node {
id: ID!
createdAt: DateTime!
name: String!
members: [User!]!
}
تمرين: صمم مخطط نظام إدارة محتوى باستخدام الواجهات والاتحادات:
- أنشئ واجهة
Content مع حقول مشتركة (id, title, createdAt, author)
- نفذ أنواع Article وVideo وPodcast مع حقول خاصة بالنوع
- أنشئ اتحاد
MediaAsset لأنواع Image وAudio وFile
- صمم واجهة
Notification لـ ContentPublished وCommentReceived وMentionReceived
- نفذ محللات
__resolveType لجميع الأنواع المجردة
- اكتب استعلامات باستخدام أجزاء مضمنة و
__typename
- أنشئ أجزاء قابلة لإعادة الاستخدام للأنماط الشائعة
اختبر مخططك باستعلامات متعددة الأشكال معقدة عبر مستويات متعددة من التجريد.