GraphQL مقابل REST - برنامج تعليمي لـ GraphQL

نشرت: 2022-03-11

ربما سمعت عن الطفل الجديد حول الكتلة: GraphQL. إذا لم يكن الأمر كذلك ، فإن GraphQL هي ، باختصار ، طريقة جديدة لجلب واجهات برمجة التطبيقات ، كبديل لـ REST. لقد بدأ كمشروع داخلي في Facebook ، ومنذ أن كان مفتوح المصدر ، اكتسب الكثير من الزخم.

الهدف من هذه المقالة هو مساعدتك في إجراء انتقال سهل من REST إلى GraphQL ، سواء كنت قد اتخذت قرارك بالفعل بشأن GraphQL أو كنت ترغب فقط في تجربتها. ليست هناك حاجة إلى معرفة مسبقة بـ GraphQL ، ولكن يلزم بعض الإلمام بواجهات برمجة تطبيقات REST لفهم المقالة.

GraphQL مقابل REST - برنامج تعليمي لـ GraphQL

سيبدأ الجزء الأول من المقالة بإعطاء ثلاثة أسباب تجعلني أعتقد شخصيًا أن GraphQL تتفوق على REST. الجزء الثاني عبارة عن برنامج تعليمي حول كيفية إضافة نقطة نهاية GraphQL على الواجهة الخلفية.

Graphql مقابل REST: لماذا Drop REST؟

إذا كنت لا تزال مترددًا بشأن ما إذا كانت GraphQL مناسبة لاحتياجاتك أم لا ، فستجد هنا نظرة عامة شاملة وموضوعية عن "REST مقابل GraphQL". ومع ذلك ، لأهم ثلاثة أسباب لاستخدام GraphQL ، تابع القراءة.

السبب الأول: أداء الشبكة

لنفترض أن لديك مورد مستخدم في الواجهة الخلفية بالاسم الأول واسم العائلة والبريد الإلكتروني و 10 حقول أخرى. بالنسبة للعميل ، فأنت تحتاج عمومًا إلى اثنين فقط.

يمنحك إجراء مكالمة REST على نقطة النهاية /users جميع حقول المستخدم ، ويستخدم العميل فقط الحقول التي يحتاجها. من الواضح أن هناك بعض الهدر في نقل البيانات ، والذي قد يكون أحد الاعتبارات للعملاء المتنقلين.

تقوم GraphQL بشكل افتراضي بجلب أصغر البيانات الممكنة. إذا كنت تحتاج فقط إلى الاسم الأول والأخير للمستخدمين ، فإنك تحدد ذلك في طلب البحث.

تسمى الواجهة أدناه GraphiQL ، والتي تشبه مستكشف واجهة برمجة التطبيقات لـ GraphQL. لقد أنشأت مشروعًا صغيرًا لغرض هذه المقالة. تمت استضافة الكود على GitHub ، وسنتعمق فيه في الجزء الثاني.

في الجزء الأيمن من الواجهة يوجد الاستعلام. هنا ، نقوم بإحضار جميع المستخدمين - سنفعل GET /users مع REST - ونحصل فقط على أسمائهم الأولى والأخيرة.

استفسار

 query { users { firstname lastname } }

نتيجة

 { "data": { "users": [ { "firstname": "John", "lastname": "Doe" }, { "firstname": "Alicia", "lastname": "Smith" } ] } }

إذا أردنا الحصول على رسائل البريد الإلكتروني أيضًا ، فإن إضافة سطر "بريد إلكتروني" أسفل "اسم العائلة" سيفي بالغرض.

تقدم بعض نهايات REST الخلفية خيارات مثل /users?fields=firstname,lastname لما يستحق ، توصي Google به. ومع ذلك ، لا يتم تنفيذه افتراضيًا ، ويجعل الطلب بالكاد قابلاً للقراءة ، خاصةً عندما تقوم بإلقاء معامِلات طلب البحث الأخرى:

  • &status=active لتصفية المستخدمين النشطين
  • &sort=createdAat لفرز المستخدمين حسب تاريخ إنشائهم
  • &sortDirection=desc لأنه من الواضح أنك بحاجة إليها
  • &include=projects لتضمين مشروعات المستخدمين

معلمات الاستعلام هذه عبارة عن تصحيحات تمت إضافتها إلى REST API لتقليد لغة الاستعلام. تعتبر GraphQL قبل كل شيء لغة استعلام ، مما يجعل الطلبات موجزة ودقيقة من البداية.

السبب 2: خيار التصميم "التضمين مقابل نقطة النهاية"

لنتخيل أننا نريد بناء أداة بسيطة لإدارة المشاريع. لدينا ثلاثة موارد: المستخدمون والمشاريع والمهام. نحدد أيضًا العلاقات التالية بين الموارد:

العلاقات بين الموارد

فيما يلي بعض نقاط النهاية التي نكشفها للعالم:

نقطة النهاية وصف
GET /users قائمة بجميع المستخدمين
GET /users/:id احصل على مستخدم واحد مع معرف: معرف
GET /users/:id/projects احصل على جميع المشاريع لمستخدم واحد

نقاط النهاية بسيطة وسهلة القراءة ومنظمة جيدًا.

تصبح الأمور أكثر تعقيدًا عندما تصبح طلباتنا أكثر تعقيدًا. لنأخذ نقطة نهاية GET /users/:id/projects : لنفترض أنني أريد فقط إظهار عناوين المشاريع على الصفحة الرئيسية ، لكن المشاريع + المهام على لوحة القيادة ، دون إجراء مكالمات REST متعددة. يمكن أن أسميه:

  • GET /users/:id/projects للصفحة الرئيسية.
  • GET /users/:id/projects?include=tasks (على سبيل المثال) على صفحة لوحة القيادة بحيث تلحق النهاية الخلفية جميع المهام ذات الصلة.

من الشائع إضافة معامِلات الاستعلام ?include=... لإنجاح هذا الأمر ، بل ويوصى به أيضًا في مواصفات JSON API. معامِلات طلب البحث مثل ?include=tasks لا تزال قابلة للقراءة ، ولكن بعد فترة طويلة ، سننتهي بـ ?include=tasks,tasks.owner,tasks.comments,tasks.comments.author .

في هذه الحالة ، هل سيكون من الحكمة إنشاء نقطة نهاية /projects للقيام بذلك؟ شيء من هذا القبيل /projects?userId=:id&include=tasks ؟ أو ، في الواقع ، a /tasks?userId=:id endpoint قد تعمل أيضًا. قد يكون هذا اختيارًا صعبًا للتصميم ، بل قد يكون أكثر تعقيدًا إذا كانت لدينا علاقة أطراف بأطراف.

تستخدم GraphQL نهج include في كل مكان. هذا يجعل بناء الجملة لجلب العلاقات قويًا ومتسقًا.

فيما يلي مثال على جلب جميع المشاريع والمهام من المستخدم بالمعرف 1.

استفسار

 { user(id: 1) { projects { name tasks { description } } } }

نتيجة

 { "data": { "user": { "projects": [ { "name": "Migrate from REST to GraphQL", "tasks": [ { "description": "Read tutorial" }, { "description": "Start coding" } ] }, { "name": "Create a blog", "tasks": [ { "description": "Write draft of article" }, { "description": "Set up blog platform" } ] } ] } } }

كما ترى ، يمكن قراءة صيغة الاستعلام بسهولة. إذا أردنا التعمق في تضمين المهام والتعليقات والصور والمؤلفين ، فلن نفكر مرتين في كيفية تنظيم واجهة برمجة التطبيقات الخاصة بنا. تجعل GraphQL من السهل إحضار الكائنات المعقدة.

السبب 3: إدارة أنواع مختلفة من العملاء

عند إنشاء واجهة خلفية ، نبدأ دائمًا بمحاولة جعل واجهة برمجة التطبيقات قابلة للاستخدام على نطاق واسع من قبل جميع العملاء قدر الإمكان. ومع ذلك ، يرغب العملاء دائمًا في الاتصال بكميات أقل وجلب المزيد. مع التضمينات العميقة والموارد الجزئية والتصفية ، قد تختلف الطلبات المقدمة من عملاء الويب والجوّال كثيرًا عن بعضها البعض.

مع REST ، هناك حلان. يمكننا إنشاء نقطة نهاية مخصصة (على سبيل المثال ، نقطة نهاية مستعارة ، على سبيل المثال ، /mobile_user ) ، أو تمثيل مخصص ( Content-Type: application/vnd.rest-app-example.com+v1+mobile+json ) ، أو حتى عميل -واجهة برمجة تطبيقات محددة (مثلما فعلت Netflix مرة واحدة). كل ثلاثة منهم يتطلب جهدًا إضافيًا من فريق التطوير الخلفي.

تمنح GraphQL مزيدًا من القوة للعميل. إذا كان العميل بحاجة إلى طلبات معقدة ، فسيقوم ببناء الاستعلامات المقابلة بنفسه. يمكن لكل عميل أن يستهلك نفس واجهة برمجة التطبيقات بشكل مختلف.

كيف تبدأ مع GraphQL

في معظم المناقشات حول "GraphQL مقابل REST" اليوم ، يعتقد الناس أنه يجب عليهم اختيار أحدهما. هذا ببساطة غير صحيح.

تستخدم التطبيقات الحديثة بشكل عام العديد من الخدمات المختلفة ، والتي تعرض العديد من واجهات برمجة التطبيقات. يمكننا بالفعل التفكير في GraphQL كبوابة أو غلاف لكل هذه الخدمات. سيصل جميع العملاء إلى نقطة نهاية GraphQL ، وستصل نقطة النهاية هذه إلى طبقة قاعدة البيانات ، أو خدمة خارجية مثل ElasticSearch أو Sendgrid ، أو نقاط نهاية REST الأخرى.

مقارنات بين نقاط نهاية GraphQL و REST

الطريقة الثانية لاستخدام كليهما هي الحصول على نقطة نهاية منفصلة /graphql على واجهة برمجة تطبيقات REST الخاصة بك. يكون هذا مفيدًا بشكل خاص إذا كان لديك بالفعل العديد من العملاء الذين يصلون إلى REST API ، لكنك تريد تجربة GraphQL دون المساس بالبنية التحتية الحالية. وهذا هو الحل الذي نستكشفه اليوم.

كما ذكرنا سابقًا ، سأقوم بتوضيح هذا البرنامج التعليمي من خلال مشروع مثال صغير ، متاح على GitHub. إنها أداة مبسطة لإدارة المشاريع ، مع المستخدمين والمشاريع والمهام.

التقنيات المستخدمة لهذا المشروع هي Node.js و Express لخادم الويب ، و SQLite كقاعدة بيانات علائقية ، و Sequelize كـ ORM. يتم تحديد النماذج الثلاثة - المستخدم والمشروع والمهمة - في مجلد models . يتم عرض نقاط نهاية REST /api/users و /api/projects و /api/tasks للعالم ، ويتم تحديدها في مجلد rest .

يرجى ملاحظة أنه يمكن تثبيت GraphQL على أي نوع من أنواع البيانات الخلفية وقاعدة البيانات ، باستخدام أي لغة برمجة. يتم اختيار التقنيات المستخدمة هنا من أجل البساطة وسهولة القراءة.

هدفنا هو إنشاء نقطة نهاية /graphql دون إزالة نقاط نهاية REST. ستصل نقطة نهاية GraphQL إلى قاعدة البيانات ORM مباشرةً لجلب البيانات ، بحيث تكون مستقلة تمامًا عن منطق REST.

أنواع

يتم تمثيل نموذج البيانات في GraphQL من خلال الأنواع ، والتي يتم كتابتها بشدة. يجب أن يكون هناك تعيين 1 إلى 1 بين النماذج وأنواع GraphQL. سيكون نوع User لدينا:

 type User { id: ID! # The "!" means required firstname: String lastname: String email: String projects: [Project] # Project is another GraphQL type }

استفسارات

تحدد الاستعلامات الاستعلامات التي يمكنك تشغيلها على واجهة برمجة تطبيقات GraphQL الخاصة بك. حسب الاصطلاح ، يجب أن يكون هناك RootQuery ، والذي يحتوي على جميع الاستعلامات الموجودة. أشرت أيضًا إلى مكافئ REST لكل استعلام:

 type RootQuery { user(id: ID): User # Corresponds to GET /api/users/:id users: [User] # Corresponds to GET /api/users project(id: ID!): Project # Corresponds to GET /api/projects/:id projects: [Project] # Corresponds to GET /api/projects task(id: ID!): Task # Corresponds to GET /api/tasks/:id tasks: [Task] # Corresponds to GET /api/tasks }

الطفرات

إذا كانت الاستعلامات عبارة عن طلبات GET ، فيمكن اعتبار الطفرات على أنها طلبات POST / PATCH / PUT / DELETE (على الرغم من أنها في الحقيقة إصدارات متزامنة من الاستعلامات).

وفقًا للاتفاقية ، نضع كل RootMutation في طفرة جذرية:

 type RootMutation { createUser(input: UserInput!): User # Corresponds to POST /api/users updateUser(id: ID!, input: UserInput!): User # Corresponds to PATCH /api/users removeUser(id: ID!): User # Corresponds to DELETE /api/users createProject(input: ProjectInput!): Project updateProject(id: ID!, input: ProjectInput!): Project removeProject(id: ID!): Project createTask(input: TaskInput!): Task updateTask(id: ID!, input: TaskInput!): Task removeTask(id: ID!): Task }

لاحظ أننا قدمنا ​​أنواعًا جديدة هنا ، تسمى UserInput و ProjectInput و TaskInput . هذه ممارسة شائعة مع REST أيضًا ، لإنشاء نموذج بيانات إدخال لإنشاء الموارد وتحديثها. هنا ، نوع UserInput بنا هو نوع User الخاص بنا بدون حقول id projects ، ولاحظ input الكلمات الرئيسية بدلاً من type :

 input UserInput { firstname: String lastname: String email: String }

مخطط

باستخدام الأنواع والاستعلامات والطفرات ، نحدد مخطط GraphQL ، وهو ما تعرضه نقطة نهاية GraphQL للعالم:

 schema { query: RootQuery mutation: RootMutation }

تمت كتابة هذا المخطط بشدة وهو ما سمح لنا بالحصول على عمليات الإكمال التلقائي المفيدة في GraphiQL.

المحللون

الآن وقد أصبح لدينا المخطط العام ، فقد حان الوقت لإخبار GraphQL بما يجب القيام به عند طلب كل من هذه الاستعلامات / الطفرات. يقوم المحللون بالعمل الشاق ؛ يمكنهم ، على سبيل المثال:

  • ضرب نقطة نهاية REST داخلية
  • اتصل بالخدمة المصغرة
  • اضغط على طبقة قاعدة البيانات للقيام بعمليات CRUD

نحن نختار الخيار الثالث في مثالنا التطبيق. دعنا نلقي نظرة على ملف الحلول الخاص بنا:

 const models = sequelize.models; RootQuery: { user (root, { id }, context) { return models.User.findById(id, context); }, users (root, args, context) { return models.User.findAll({}, context); }, // Resolvers for Project and Task go here }, /* For reminder, our RootQuery type was: type RootQuery { user(id: ID): User users: [User] # Other queries }

هذا يعني أنه إذا تم طلب استعلام user(id: ID!) في GraphQL ، فسنقوم بإرجاع User.findById() ، وهي دالة Sequelize ORM ، من قاعدة البيانات.

ماذا عن الانضمام إلى نماذج أخرى في الطلب؟ حسنًا ، نحن بحاجة إلى تحديد المزيد من أدوات الحل:

 User: { projects (user) { return user.getProjects(); // getProjects is a function managed by Sequelize ORM } }, /* For reminder, our User type was: type User { projects: [Project] # We defined a resolver above for this field # ...other fields } */

لذلك عندما نطلب حقل projects في نوع User في GraphQL ، سيتم إلحاق هذه الصلة باستعلام قاعدة البيانات.

وأخيرًا ، محللات الطفرات:

 RootMutation: { createUser (root, { input }, context) { return models.User.create(input, context); }, updateUser (root, { id, input }, context) { return models.User.update(input, { ...context, where: { id } }); }, removeUser (root, { id }, context) { return models.User.destroy(input, { ...context, where: { id } }); }, // ... Resolvers for Project and Task go here }

يمكنك اللعب بهذا هنا. من أجل الحفاظ على البيانات الموجودة على الخادم نظيفة ، قمت بتعطيل محللات الطفرات ، مما يعني أن الطفرات لن تقوم بأي عمليات إنشاء أو تحديث أو حذف في قاعدة البيانات (وبالتالي ستعود null على الواجهة).

استفسار

 query getUserWithProjects { user(id: 2) { firstname lastname projects { name tasks { description } } } } mutation createProject { createProject(input: {name: "New Project", UserId: 2}) { id name } }

نتيجة

 { "data": { "user": { "firstname": "Alicia", "lastname": "Smith", "projects": [ { "name": "Email Marketing Campaign", "tasks": [ { "description": "Get list of users" }, { "description": "Write email template" } ] }, { "name": "Hire new developer", "tasks": [ { "description": "Find candidates" }, { "description": "Prepare interview" } ] } ] } } }

قد يستغرق الأمر بعض الوقت لإعادة كتابة جميع الأنواع والاستعلامات والمحللات لتطبيقك الحالي. ومع ذلك ، يوجد الكثير من الأدوات لمساعدتك. على سبيل المثال ، هناك أدوات تترجم مخطط SQL إلى مخطط GraphQL ، بما في ذلك أدوات الحل!

نضع كل شيء معًا

باستخدام مخطط ومحللات محددة جيدًا حول ما يجب القيام به في كل استعلام في المخطط ، يمكننا تركيب نقطة نهاية /graphql على النهاية الخلفية لدينا:

 // Mount GraphQL on /graphql const schema = makeExecutableSchema({ typeDefs, // Our RootQuery and RootMutation schema resolvers: resolvers() // Our resolvers }); app.use('/graphql', graphqlExpress({ schema }));

ويمكننا الحصول على واجهة GraphiQL جميلة المظهر على الواجهة الخلفية لدينا. لتقديم طلب بدون GraphiQL ، ما عليك سوى نسخ عنوان URL للطلب وتشغيله باستخدام cURL أو AJAX أو مباشرة في المتصفح. بالطبع ، هناك بعض عملاء GraphQL لمساعدتك في إنشاء هذه الاستعلامات. انظر أدناه للحصول على بعض الأمثلة.

ماذا بعد؟

تهدف هذه المقالة إلى منحك لمحة عن الشكل الذي تبدو عليه GraphQL وإظهار أنه من الممكن بالتأكيد تجربة GraphQL دون التخلص من بنية REST الأساسية. أفضل طريقة لمعرفة ما إذا كانت GraphQL تناسب احتياجاتك هي أن تجربها بنفسك. آمل أن تجعلك هذه المقالة تغوص.

هناك الكثير من الميزات التي لم نناقشها في هذه المقالة ، مثل التحديثات في الوقت الفعلي ، والتجميع من جانب الخادم ، والمصادقة ، والتفويض ، والتخزين المؤقت من جانب العميل ، وتحميل الملفات ، وما إلى ذلك. مورد ممتاز للتعرف على هذه الميزات هي كيفية GraphQL.

فيما يلي بعض الموارد المفيدة الأخرى:

أداة جانب الخادم وصف
graphql-js التنفيذ المرجعي لـ GraphQL. يمكنك استخدامه مع express-graphql لإنشاء خادم.
graphql-server خادم GraphQL الكل في واحد أنشأه فريق Apollo.
تطبيقات لمنصات أخرى Ruby ، ​​PHP ، إلخ.
أداة من جانب العميل وصف
تناوب إطار عمل لربط React بـ GraphQL.
أبولو العميل. عميل GraphQL مع روابط لـ React و Angular 2 وأطر أمامية أخرى.

في الختام ، أعتقد أن GraphQL هي أكثر من مجرد دعاية. لن تحل محل REST غدًا حتى الآن ، لكنها تقدم حلاً فعالاً لمشكلة حقيقية. إنها جديدة نسبيًا ، ولا تزال أفضل الممارسات قيد التطوير ، لكنها بالتأكيد تقنية سنسمع عنها في العامين المقبلين.