واجهات GraphQL

العلاقات والبيانات المتداخلة

18 دقيقة الدرس 9 من 35

إدارة العلاقات في 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 } } } }

يتم حل هذا الاستعلام من خلال سلسلة من المحللات:

  1. محلل Query.user يجلب المستخدم
  2. محلل User.posts يجلب مقالات المستخدم
  3. محلل 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 } }; } } };
تمرين تطبيقي:
  1. أنشئ مخطط Prisma بنماذج User وPost وComment (User لديه العديد من Posts، Post لديه العديد من Comments)
  2. نفذ محللات GraphQL لجميع العلاقات المتداخلة
  3. أنشئ استعلامًا يجلب مستخدمًا مع مقالاته وتعليقات كل مقالة
  4. نفذ DataLoader لحل مشكلة N+1 لمؤلفي التعليقات
  5. أضف ترقيمًا إلى حقل User.posts
  6. اختبر باستخدام GraphQL Playground وتحقق من عدد استعلامات قاعدة البيانات