آشنایی با GraphQL Schema

GraphQL Schema

آشنایی با GraphQL Schema

GraphQL یک معماری برای طراحی API ها است اما با بزرگی خود کمتر در ایران شناخته می شود چرا که بیشتر توسعه دهندگان در ایران از معماری REST استفاده می کنند. اگر بخواهیم به زبان فنی صحبت کنیم می گوییم که GraphQL یک زبان کوئری برای API است بنابراین GraphQL توصیف می کند که یک API چطور داده هایش را در معرض نمایش قرار دهد. در واقع شما می توانید با استفاده از GraphQL چندین سیستم را در پس زمینه داشته باشید اما برای تمام آن ها یک API واحد در نظر بگیرید.

سطح و هدف مقاله

این مقاله برای افرادی در نظر گرفته شده است که با API ها کار کرده اند و مفاهیم پایه آن ها را می شناسند. به طور مثال افرادی که REST API طراحی می کنند و حالا می خواهند وارد دنیای GraphQL شوند می توانند استفاده زیادی از این مقاله ببرند. این مقاله کمک زیادی به افراد تازه کار نخواهد کرد.

توجه داشته باشید که GraphQL هیچ ارتباطی با پایگاه داده، زبان سرور، فریم ورک استفاده شده و تکنولوژی های دیگر ندارد بنابراین این مقاله روی PHP یا Node یا Java و امثال آن تمرکز نمی کند بلکه هدف اصلی ما آشنایی با ساختار خالص GraphQL است.

مزایای GraphQL

GraphQL مزایای زیادی دارد: به طور مثال در GraphQL می توانید دقیقا مشخص کنید که چه داده هایی را می خواهید در حالی که در REST تمام داده ها برگردانده می شوند که باعث هدر رفتن پهنای باند سرور می شود. مزیت دیگر آن این است که GraphQL فقط یک endpoint دارد و تمام داده ها به هر شکلی از همان تک endpoint گرفته می شود که باعث می شود نیازی به تعریف مسیرهای مختلف برای API خود نداشته باشیم. مزیت دیگر GraphQL این است که type checking و اعتبارسنجی در لحظه و قبل از ارسال درخواست انجام می شود و همه چیز برای توسعه دهندگان front-end مشخص است بنابراین اصلا نیازی به نوشتن صفحه documentation و صرف زمان برای تیم توسعه front-end نخواهید داشت. طبیعتا GraphQL مشکلات خودش مانند caching را نیز دارد و بیشتر برای برنامه های بسیار بزرگ مفید است تا اینکه به برنامه های کوچک کمکی کند.

باید در نظر داشته باشید که GraphQL یک پایگاه داده نیست بلکه لایه ای است که روی API شما سوار می شود بنابراین می توان با هر زبانی و هر پایگاه داده ای و هر فریم ورکی از آن استفاده کرد. GraphQL یک سیستم تایپ (Type System) دارد که داده ها را توصیف می کند. ساده ترین واحد سازنده این سیستم object types یا تایپ های شیئی هستند که توصیف کننده نوع شیئی هستند که می توانید از API دریافت کنید. مثلا:

type Book {

  title: String

  author: Author

}




type Author {

  name: String

  books: [Book]

}

ما بدین شکل دو تایپ ساده را به نام های Book (کتاب) و Author (نویسنده) تعریف کرده ایم. اگر توجه کرده باشید تایپ Author یک فیل به نام books دارد که آن را به صورت آرایه ای از تایپ Book تعریف کرده ایم. این یک رابطه در کوئری های GraphQL است. در واقع هر نویسنده می تواند چندین کتاب داشته باشد و هر کتاب باید متعلق به یک نویسنده باشد. با تعریف کردن این تایپ ها باعث می شویم توسعه دهندگان سمت کلاینت (front-end) دقیقا بدانند با چه داده هایی سر و کار دارند.

من می خواهم در این مقاله شما را با انواع این تایپ ها آشنا کنم.

۱. تایپ های Scalar

این دسته از تایپ ها شبیه به داده های primitive در زبان های برنامه نویسی هستند، یعنی مستقیما یک داده نشان می دهند. از داده های Scalar می توان به موارد زیر اشاره کرد:

  • Int: یک عدد صحیح ۳۲ بیتی و sign شده
  • Float: یک عدد اعشاری با دقت دو رقم اعشار و sign شده
  • String: یک رشته از کاراکتر های UTF‐8
  • Boolean: مقدار true یا false
  • ID: به شکل string سریالی شده است. این آیدی معمولا برای دریافت دوباره یک مقدار از کش استفاده می شود.

این دسته از تایپ ها، بخش اعظم اکثر پروژه ها را تشکیل می دهند.

۲. تایپ های Object

اکثر تایپ های تعریف شده در GraphQL از نوع object type هستند. هر فیلد از فیلدهای این دسته تایپ می توانند یک scalar type یا یک object type دیگر باشد. قبلا مثالی را در این زمینه مشاهده کردید:

type Book {

  title: String

  author: Author

}




type Author {

  name: String

  books: [Book]

}

۳. تایپ های Query

تایپ های کوئری خودشان از نوع object type هستند اما همیشه Query نام دارند و وظیفه شان تعریف کردن تمام نقاط ورودی سطح بالا است که درخواست کلاینت به سمت آن ها ارسال می شود. این دسته از تایپ ها نام و نوع داده برگردانده شده از یک نقطه ورودی را تعریف می کنند. مثلا اگر دو Object type بالا را در نظر بگیرید، Query Type آن ها به شکل زیر خواهد بود:

type Query {

  books: [Book]

  authors: [Author]

}

یعنی بالاترین تایپ برای این نقطه ورودی دو فیلد به نام های books و authors دارد و نوع آن مجموعه ای از تایپ های Book و Author است. اگر از REST API استفاده می کردیم، برای چنین کاری باید دو مسیر جداگانه تعریف می کردیم:

  • api/books/
  • api/authors/

اما در GraphQL همه چیز در یک endpoint قرار دارد.

ساخت یک کوئری برای دریافت داده

حالا که با تایپ ها آشنا شدید باید با نحوه کوئری زدن به چنین سیستمی آشنا شویم. زمانی که برنامه نویس front-end شما می خواهد به سرور یک کوئری بزند، ساختار این کوئری دقیقا مانند همان تایپ هایی است که تعریف کرده ایم. به طور مثال اگر بخواهیم از همان مثال book و author استفاده کنیم، کوئری زیر را می نویسیم:

query GetBooksAndAuthors {

  books {

    title

  }




  authors {

    name

  }

}

در ابتدا کلمه query را آورده و سپس نامی را برایش انتخاب می کنیم (GetBooksAndAuthors). این کوئری تمام کتاب ها و تمام نویسنده ها را برایمان برمی گرداند. زمانی که این کوئری به سمت سرور ما ارسال شود، داده های زیر برای کلاینت برگردانده می شوند:

{

  "data": {

    "books": [

      {

        "title": "City of Glass"

      },

      ...

    ],

    "authors": [

      {

        "name": "Paul Auster"

      },

      ...

    ]

  }

}

همانطور که می بینید این نتیجه شبیه نتیجه REST API ها است چرا که تمام داده ها یکجا برگردانده شده است. حالا اگر کلاینت بخواهد لیستی از کتاب ها را دریافت کند و نویسنده هر کتاب نیز همراه آن کتاب باشد چطور؟ آنگاه می توانیم کوئری بالا را به شکل زیر ویرایش کنیم:

query GetBooks {

  books {

    title

    author {

      name

    }

  }

}

این بار سرور ما بدین شکل پاسخ می دهد:

{

  "data": {

    "books": [

      {

        "title": "City of Glass",

        "author": {

          "name": "Paul Auster"

        }

      },

      ...

    ]

  }

}

همانطور که می بینید در GraphQL می توانیم دقیقا فیلدهای مورد نظرمان را دریافت کنیم.

۴. تایپ های Mutation

تایپ های Mutation دقیقا مانند تایپ های Query هستند البته یک تفاوت بسیار بزرگ وجود دارد. تایپ های Query فقط برای خواندن (Read) داده هستند (مثلا دریافت کتاب ها) در حالی که تایپ Mutation برای نوشتن (Write) داده ها در سرور است (مثلا ثبت نام یک کاربر جدید).

به طور مثال اگر بخواهیم یک کتاب جدید را در سیستم خودمان ثبت کنیم باید بدین شکل عمل کنیم:

type Mutation {

  addBook(title: String, author: String): Book

}

یعنی addBook یک عنوان و یک نویسنده را می گیرد که هر دو رشته ای هستند و سپس یک کتاب را برمی گرداند. تا اینجا برنامه ما فقط یک mutation به نام addBook دارد. چنین کدی در سمت سرور نوشته می شود و خود ما روی آن کنترل داریم اما اگر کلاینت بخواهد به سرور کوئری زده و یک کتاب جدید را ثبت کند چطور؟ آنگاه کلاینت باید چنین کوئری را به سمت سرور ارسال کند:

mutation CreateBook {

  addBook(title: "Fox in Socks", author: "Dr. Seuss") {

    title

    author {

      name

    }

  }

}

ما در این کوئری یک کتاب جدید را ثبت کرده ایم و سپس فیلدهای title (عنوان کتاب) و author (نویسنده آن) را نیز تقاضا کرده ایم. دقت کنید که از فیلد author فقط نام نویسنده را می خواهیم چرا که کتاب را خودمان ثبت کرده ایم و می دانیم دقیقا چه کتابی است بنابراین با علامت های {} آن را مشخص کرده ایم. با اجرای این کوئری، سرور چنین پاسخی را به شما می دهد:

{

  "data": {

    "addBook": {

      "title": "Fox in Socks",

      "author": {

        "name": "Dr. Seuss"

      }

    }

  }

}

البته کلاینت می تواند چندین mutation را به صورت همزمان درخواست بدهد. این mutation ها به صورت سریالی و به ترتیبی که نوشته شده اند انجام خواهند شد.

۵. تایپ های Subscription

این دسته از تایپ ها در واقع یک نوع عملیات read (یک تایپ query) طولانی هستند که در هنگام رخ دادن یک رویداد خاص، نتایج را به روز رسانی می کنند. معمولا زمانی از این تایپ ها استفاده می کنیم که بخواهیم با به روز رسانی یک داده در سمت پایگاه داده از این موضوع با خبر شویم. مثلا اگر یک برنامه پیامرسان داشته باشیم، می خواهیم با دریافت داده جدید (یک پیام جدید، تماس جدید، کامنت جدید و الی آخر) آن داده را به گیرنده ارسال کنیم.

برای استفاده از این تایپ ها معمولا از پروتکل WebSocket استفاده می شود. مثلا ما می توانیم در سمت سرور چنین تایپی را تعریف کنیم:

type Subscription {

  postCreated: Post

}

یعنی هر گاه پستی تغییر کرد، داده های جدید به کلاینت ارسال می شوند. از طرف دیگر کلاینت نیز می تواند به شکل زیر به این تایپ خاص subscribe کند (به آن متصل بماند تا تغییرات جدید برایش ارسال شود):

subscription PostFeed {

  postCreated {

    author

    comment

  }

}

۶. تایپ های Input

این دسته از تایپ ها، در اصل object type های خاصی می باشند که به شما اجازه می دهند در Query ها و Mutation ها یک شیء را به عنوان آرگومان پاس بدهید. در حالت عادی زمانی که از object type ها استفاده می کنید فقط می توانستیم تایپ های scalar را به عنوان آرگومان پاس بدهیم. به این مثال توجه کنید:

type Mutation {

  createPost(title: String, body: String, mediaUrls: [String]): Post

}

این mutation یک پست وبلاگ را ایجاد می کند. ما می توانیم به جای پاس دادن چندین آرگومان مختلف به شکل بالا یک شیء را ه عنوان آرگومان پاس بدهیم. به طور مثال به این کد توجه کنید:

type Mutation {

  createPost(post: PostAndMediaInput): Post

}




input PostAndMediaInput {

  title: String

  body: String

  mediaUrls: [String]

}

انجام این کار باعث می شود نیازی به پاس دادن انواع متغیرها نداشته باشید و کارتان را راحت تر می کند. همانطور که می بینید من تایپ را از نوع input تعریف کرده ام. اگر از تایپ های input استفاده کنید می توانید با قرار دادن یک رشته بالای هر فیلد آن را توضیح بدهید (چیزی شبیه به typeDoc در جاوا اسکریپت):

input PostAndMediaInput {

  "A main title for the post"

  title: String

  "The text body of the post."

  body: String

  "A list of URLs to render in the post."

  mediaUrls: [String]

}

نکته: از یک تایپ input برای تعیین تایپ mutation ها و query ها استفاده نکنید چرا که در بسیاری از مواقع داده ها برای mutation اجباری هستند اما برای query ها اختیاری هستند.

۷. تایپ Enum

این دسته از تایپ ها دقیقا مانندscalar ها هستند با این تفاوت که تمام مقادیر مجاز آن ها باید از همان ابتدا مشخص شوند. به مثال زیر توجه کنید:

enum AllowedColor {

  RED

  GREEN

  BLUE

}

با این حساب تایپ بالا فقط می تواند یکی از مقادیر RED یا GREEN یا BLUE را داشته باشد. مثال در هنگام تعریف Query:

type Query {

  favoriteColor: AllowedColor # enum return value

  avatar(borderColor: AllowedColor): String # enum argument

}

مثال در هنگام استفاده می توانیم بدین شکل عمل کنیم:

query GetAvatar {

  avatar(borderColor: RED)

}

۸. تایپ های Union و Interface

تایپ های union از اجتماع دو یا چند object type تشکیل می شوند. به طور مثال:

union Media = Book | Movie

یعنی تایپ Media یا تایپ Book یا تایپ Movie خواهد بود (هر دو مقبول هستند). سپس در هنگام استفاده از آن می توان بدین شکل عمل کرد:

type Query {

  allMedia: [Media] # This list can include both Books and Movies

}

در نظر داشته باشید که نمی توانید با تایپ های Scalar یا امثال آن Union بسازید بلکه Union Type ها فقط با object type ها ساخته می شوند.

از طرف دیگر تایپ های interface را داریم که مشخص کننده ساختار تایپ های دیگر هستند. مثلا:

interface Book {

  title: String

  author: Author

}

در اینجا یک interface به نام Book را داریم که دو فیلد را مشخص کرده است. هر تایپی که بخواهد از این interface پیروی کند باید حتما این دو فیلد را داشته باشد:

type Textbook implements Book {

  title: String # فیلد اجباری

  author: Author # فیلد اجباری

  courses: [Course]

}

این مسئله بسیار شبیه به extend شدن یک کلاس توسط کلاس فرزند دارد بنابراین درک آن باید برایتان بسیار ساده باشد.

ابزارهای پیاده سازی

حالا که با کلیت schema در GraphQL آشنا شدیم سوالی باقی می ماند و آن بحث ابزارهای پیاده سازی GraphQL است. چطور می توانیم API خود را به شکل GraphQL بسازیم؟ پاسخ به این سوال به شدت بستگی به تکنولوژی های مورد استفاده شما دارد. اگر به این صفحه از وب سایت GraphQL سری بزنید متوجه می شوید که GraphQL در انواع زبان ها و ابزارهای مختلف برای شما موجود است.

به طور مثال اگر با زبان جاوا اسکریپت و Node.js کار می کنید، یکی از محبوب ترین روش های راه اندازی سرور GraphQL استفاده از پکیج apollo-server است. این پکیج یک سرور GraphQL را با کمک فریم ورک مورد نظر شما (مثلا Express.js) راه اندازی می کند و استفاده از آن بسیار آسان است. به مثال زیر توجه کنید:

const { ApolloServer, gql } = require('apollo-server');




// The GraphQL schema

const typeDefs = gql`

  type Query {

    "A simple type for getting started!"

    hello: String

  }

`;




// A map of functions which return data for the schema.

const resolvers = {

  Query: {

    hello: () => 'world',

  },

};




const server = new ApolloServer({

  typeDefs,

  resolvers,

});




server.listen().then(({ url }) => {

  console.log(`🚀 Server ready at ${url}`);

});

ما به همین راحتی یک سرور را راه اندازی کرده ایم.

البته پکیج های دیگری مانند express-graphql و graphql-yoga نیز وجود دارند که از نظر محبوبیت از apollo-server کم ندارند. یک مثال ساده از ساخت سرور با graphql-yoga را می توانید در این کد ببینید:

import { GraphQLServer } from 'graphql-yoga'

// ... or using `require()`

// const { GraphQLServer } = require('graphql-yoga')




const typeDefs = `

  type Query {

    hello(name: String): String!

  }

`




const resolvers = {

  Query: {

    hello: (_, { name }) => `Hello ${name || 'World'}`,

  },

}




const server = new GraphQLServer({ typeDefs, resolvers })

server.start(() => console.log('Server is running on localhost:4000'))

اگر از زبان Python استفاده می کنید پکیج graphene مشهور ترین پکیج برای ساخت سرور GraphQL است.

تکنولوژی های موجود در این زمینه و تمام این ابزارها مناسب هستند و نهایتا می توانید بر اساس سلیقه خودتان از آن ها استفاده کنید. هر کدام از آن ها ویژگی خاصی دارند و طبیعتا بسته به نیاز های شما و محیط توسعه شما مناسب یا نامناسب خواهند بود. در آینده نزدیک پروژه ای را با Apollo-server با هم خواهیم نوشت!

نویسنده شوید

دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.