إدارة العلاقات في GraphQL
إحدى أقوى ميزات GraphQL هي قدرته على التعامل مع العلاقات المعقدة بين البيانات. في هذا الدرس، سنستكشف أنواعًا مختلفة من العلاقات وكيفية الاستعلام عن البيانات المتداخلة بكفاءة.
أنواع العلاقات
هناك ثلاثة أنواع رئيسية من العلاقات في قواعد البيانات:
1. علاقة واحد لواحد (One-to-One)
كل سجل في جدول واحد مرتبط بسجل واحد بالضبط في جدول آخر.
// مخطط Prisma
model User {
id Int @id @default(autoincrement())
email String @unique
profile Profile?
}
model Profile {
id Int @id @default(autoincrement())
bio String
userId Int @unique
user User @relation(fields: [userId], references: [id])
}
// مخطط GraphQL
type User {
id: Int!
email: String!
profile: Profile
}
type Profile {
id: Int!
bio: String!
user: User!
}
// المحللات
const resolvers = {
User: {
profile: async (parent, args, { prisma }) => {
return await prisma.profile.findUnique({
where: { userId: parent.id }
});
}
},
Profile: {
user: async (parent, args, { prisma }) => {
return await prisma.user.findUnique({
where: { id: parent.userId }
});
}
}
};
2. علاقة واحد لكثير (One-to-Many)
كل سجل في جدول واحد يمكن أن يكون مرتبطًا بسجلات متعددة في جدول آخر.
// مخطط Prisma
model User {
id Int @id @default(autoincrement())
email String @unique
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
authorId Int
author User @relation(fields: [authorId], references: [id])
}
// مخطط GraphQL
type User {
id: Int!
email: String!
posts: [Post!]!
}
type Post {
id: Int!
title: String!
author: User!
}
// المحللات
const resolvers = {
User: {
posts: async (parent, args, { prisma }) => {
return await prisma.post.findMany({
where: { authorId: parent.id }
});
}
},
Post: {
author: async (parent, args, { prisma }) => {
return await prisma.user.findUnique({
where: { id: parent.authorId }
});
}
}
};
3. علاقة كثير لكثير (Many-to-Many)
سجلات متعددة في جدول واحد يمكن أن تكون مرتبطة بسجلات متعددة في جدول آخر.
// مخطط Prisma
model Post {
id Int @id @default(autoincrement())
title String
categories Category[]
}
model Category {
id Int @id @default(autoincrement())
name String
posts Post[]
}
// مخطط GraphQL
type Post {
id: Int!
title: String!
categories: [Category!]!
}
type Category {
id: Int!
name: String!
posts: [Post!]!
}
// المحللات
const resolvers = {
Post: {
categories: async (parent, args, { prisma }) => {
const post = await prisma.post.findUnique({
where: { id: parent.id },
include: { categories: true }
});
return post.categories;
}
},
Category: {
posts: async (parent, args, { prisma }) => {
const category = await prisma.category.findUnique({
where: { id: parent.id },
include: { posts: true }
});
return category.posts;
}
}
};
يتعامل Prisma تلقائيًا مع علاقات كثير لكثير من خلال إنشاء جدول ربط خلف الكواليس. لا تحتاج إلى تعريف الجدول الوسيط يدويًا.
المحللات المتداخلة
يسمح GraphQL للعملاء بالاستعلام عن العلاقات المتداخلة في طلب واحد:
query {
user(id: 1) {
id
email
posts {
id
title
categories {
id
name
}
}
}
}
يتم حل هذا الاستعلام من خلال سلسلة من المحللات:
- محلل
Query.user يجلب المستخدم
- محلل
User.posts يجلب مقالات المستخدم
- محلل
Post.categories يجلب فئات كل مقالة
تحذير الأداء: يمكن أن تؤدي المحللات المتداخلة إلى مشكلة N+1، حيث تقوم بعمل N استعلامات إضافية لـ N عنصر مرتبط. يمكن أن يؤثر هذا بشدة على الأداء.
مشكلة N+1
ضع في اعتبارك هذا الاستعلام الذي يجلب 10 مقالات ومؤلفيها:
query {
posts {
id
title
author {
name
}
}
}
بدون تحسين، هذا ينفذ:
- استعلام واحد لجلب المقالات
- 10 استعلامات لجلب مؤلف كل مقالة (مشكلة N+1)
حل N+1 باستخدام DataLoader
يجمع DataLoader طلبات قاعدة البيانات ويخزنها مؤقتًا في دورة طلب واحدة:
npm install dataloader
const DataLoader = require('dataloader');
// إنشاء محمل يجمع عمليات البحث عن المستخدمين
const createUserLoader = (prisma) => {
return new DataLoader(async (userIds) => {
// جلب دُفعة جميع المستخدمين في استعلام واحد
const users = await prisma.user.findMany({
where: { id: { in: userIds } }
});
// إرجاع المستخدمين بنفس ترتيب المعرفات المطلوبة
return userIds.map(id =>
users.find(user => user.id === id)
);
});
};
// إضافة إلى السياق
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
return {
prisma,
loaders: {
user: createUserLoader(prisma)
}
};
}
});
// الاستخدام في المحلل
const resolvers = {
Post: {
author: async (parent, args, { loaders }) => {
// سيتم دمج هذا مع عمليات البحث الأخرى عن المؤلفين
return await loaders.user.load(parent.authorId);
}
}
};
يجمع DataLoader تلقائيًا استدعاءات load() المتعددة التي تحدث في نفس دورة حلقة الحدث، مما يقلل استعلامات قاعدة البيانات من N+1 إلى 2 فقط.
استعلامات متداخلة فعالة مع Prisma
يتيح لك خيار include في Prisma جلب البيانات المتداخلة في استعلام واحد:
const resolvers = {
Query: {
posts: async (parent, args, { prisma }) => {
return await prisma.post.findMany({
include: {
author: true, // جلب المؤلف في نفس الاستعلام
categories: true // جلب الفئات في نفس الاستعلام
}
});
}
},
Post: {
// لا حاجة لمحللات منفصلة - البيانات محملة بالفعل
author: (parent) => parent.author,
categories: (parent) => parent.categories
}
};
الترقيم للعلاقات الكبيرة
عند التعامل مع قوائم كبيرة، قم دائمًا بالترقيم:
type User {
id: Int!
email: String!
posts(skip: Int, take: Int): [Post!]!
}
type Query {
user(id: Int!): User
}
const resolvers = {
User: {
posts: async (parent, { skip = 0, take = 10 }, { prisma }) => {
return await prisma.post.findMany({
where: { authorId: parent.id },
skip,
take,
orderBy: { createdAt: 'desc' }
});
}
}
};
query {
user(id: 1) {
email
posts(skip: 0, take: 5) {
title
}
}
}
الترقيم القائم على المؤشر (نمط Relay)
للتمرير اللانهائي، يكون الترقيم القائم على المؤشر أكثر كفاءة:
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
}
type PostEdge {
node: Post!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
endCursor: String
}
type User {
posts(first: Int!, after: String): PostConnection!
}
const resolvers = {
User: {
posts: async (parent, { first, after }, { prisma }) => {
const posts = await prisma.post.findMany({
where: { authorId: parent.id },
take: first + 1, // جلب واحد إضافي للتحقق من وجود المزيد
...(after && { cursor: { id: parseInt(after) }, skip: 1 }),
orderBy: { id: 'asc' }
});
const hasNextPage = posts.length > first;
const edges = posts.slice(0, first).map(post => ({
node: post,
cursor: post.id.toString()
}));
return {
edges,
pageInfo: {
hasNextPage,
endCursor: edges[edges.length - 1]?.cursor
}
};
}
}
};
تمرين تطبيقي:
- أنشئ مخطط Prisma بنماذج User وPost وComment (User لديه العديد من Posts، Post لديه العديد من Comments)
- نفذ محللات GraphQL لجميع العلاقات المتداخلة
- أنشئ استعلامًا يجلب مستخدمًا مع مقالاته وتعليقات كل مقالة
- نفذ DataLoader لحل مشكلة N+1 لمؤلفي التعليقات
- أضف ترقيمًا إلى حقل User.posts
- اختبر باستخدام GraphQL Playground وتحقق من عدد استعلامات قاعدة البيانات