Rails GraphQL API ในปี 2026: graphql-ruby, Subscriptions และคำถามสัมภาษณ์

สร้าง GraphQL API พร้อมใช้งานจริงด้วย Rails 8 และ graphql-ruby การออกแบบ schema, mutations, subscriptions กับ ActionCable และการเตรียมตัวสัมภาษณ์

Rails GraphQL API ในปี 2026: graphql-ruby, Subscriptions และคำถามสัมภาษณ์

GraphQL กลายเป็นตัวเลือกหลักสำหรับ API ที่ต้องการการดึงข้อมูลแบบยืดหยุ่น และ graphql-ruby นำความสามารถนี้มาสู่ Rails พร้อมการผสานรวม ActiveRecord อย่างแน่นหนา Rails 8 ที่เปิดตัวปลายปี 2024 ทำงานได้ดีกับ graphql-ruby 2.4 ซึ่งแนะนำการจัดการ subscription ที่ดีขึ้นและ lazy execution ที่ปรับปรุงเพื่อป้องกัน N+1

สิ่งที่ graphql-ruby นำมาสู่ Rails

Gem graphql-ruby มอบการ implement GraphQL อย่างสมบูรณ์: การกำหนด schema ด้วย Ruby DSL, การอนุมาน type อัตโนมัติจาก ActiveRecord models, DataLoader ในตัวสำหรับ batching และการผสานรวม ActionCable สำหรับ subscriptions แบบ real-time

การตั้งค่า graphql-ruby ในแอปพลิเคชัน Rails 8

Generator ของ gem สร้างโครงสร้าง schema เริ่มต้นและ mount endpoint GraphQL เริ่มต้นด้วย gem และรัน install generator เพื่อสร้าง scaffold ไฟล์พื้นฐาน

ruby
# Gemfile
gem 'graphql', '~> 2.4'
bash
# Terminal
bundle install
rails generate graphql:install

Generator สร้างโฟลเดอร์ app/graphql/ พร้อมไฟล์ schema, base types และ directory mutations นอกจากนี้ยังเพิ่ม route ที่ /graphql และสามารถ mount GraphiQL สำหรับการพัฒนาได้

ruby
# app/graphql/sharpskill_schema.rb
class SharpskillSchema < GraphQL::Schema
  mutation(Types::MutationType)
  query(Types::QueryType)
  subscription(Types::SubscriptionType)

  # ใช้ DataLoader ในตัวเพื่อป้องกัน N+1
  use GraphQL::Dataloader

  # จำเป็นสำหรับ subscriptions
  use GraphQL::Subscriptions::ActionCableSubscriptions
end

ไฟล์ schema ทำหน้าที่เป็นจุดเข้าหลัก มันประกาศว่า types ใดจัดการ queries, mutations และ subscriptions รวมถึงลงทะเบียน middleware ใดๆ เช่น DataLoader

การกำหนด Types และ Resolvers สำหรับ Rails Models

แต่ละ ActiveRecord model ที่ปรากฏใน API ต้องการ GraphQL type ที่สอดคล้องกัน Type กำหนดว่า fields ใดที่ถูก expose และวิธีที่พวกมันถูก resolve

ruby
# app/graphql/types/user_type.rb
module Types
  class UserType < Types::BaseObject
    field :id, ID, null: false
    field :email, String, null: false
    field :created_at, GraphQL::Types::ISO8601DateTime, null: false

    # Association พร้อม batching อัตโนมัติผ่าน DataLoader
    field :posts, [Types::PostType], null: false

    def posts
      dataloader.with(Sources::ActiveRecordCollection, Post, :user_id).load(object.id)
    end
  end
end

การเรียก dataloader.with รวมการดึง posts หลายครั้งเป็น SQL query เดียว หากไม่มี DataLoader query ที่ร้องขอ 50 users พร้อม posts จะรัน 51 queries ด้วย DataLoader จะรันเพียง 2 queries

ruby
# app/graphql/sources/active_record_collection.rb
class Sources::ActiveRecordCollection < GraphQL::Dataloader::Source
  def initialize(model, foreign_key)
    @model = model
    @foreign_key = foreign_key
  end

  def fetch(ids)
    records = @model.where(@foreign_key => ids).group_by(&@foreign_key)
    ids.map { |id| records[id] || [] }
  end
end

รูปแบบนี้ปรากฏในทุกโปรเจค graphql-ruby ที่จริงจัง เอกสารทางการของ DataLoader ครอบคลุม use cases เพิ่มเติม เช่น caching และ nested sources

การสร้าง Queries ด้วย Arguments และ Filtering

QueryType กำหนด root fields ที่พร้อมใช้งานสำหรับ clients Arguments อนุญาตให้ทำ filtering และ pagination

ruby
# app/graphql/types/query_type.rb
module Types
  class QueryType < Types::BaseObject
    field :users, [Types::UserType], null: false do
      argument :email_contains, String, required: false
      argument :limit, Integer, required: false, default_value: 20
    end

    def users(email_contains: nil, limit:)
      scope = User.all
      scope = scope.where('email ILIKE ?', "%#{email_contains}%") if email_contains
      scope.limit(limit)
    end

    field :user, Types::UserType, null: true do
      argument :id, ID, required: true
    end

    def user(id:)
      User.find_by(id: id)
    end
  end
end

Resolver methods รับ keyword arguments ที่ตรงกับ field definition การ return nil จาก nullable field เป็นสิ่งที่ถูกต้อง การ throw error จาก non-null field จะทำให้เกิด GraphQL error response

พร้อมที่จะพิชิตการสัมภาษณ์ Ruby on Rails แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

Mutations: การสร้างและอัปเดต Records

Mutations ปฏิบัติตามข้อตกลงที่เข้มงวดกว่า queries แต่ละ mutation อยู่ใน class แยกและ return payload type ที่รวมทั้งผลลัพธ์และ errors

ruby
# app/graphql/mutations/create_post.rb
module Mutations
  class CreatePost < Mutations::BaseMutation
    argument :title, String, required: true
    argument :body, String, required: true

    field :post, Types::PostType, null: true
    field :errors, [String], null: false

    def resolve(title:, body:)
      post = context[:current_user].posts.build(title: title, body: body)

      if post.save
        { post: post, errors: [] }
      else
        { post: nil, errors: post.errors.full_messages }
      end
    end
  end
end

context[:current_user] มาจาก controller การ authenticate เกิดขึ้นก่อน layer GraphQL โดยทั่วไปใช้ Devise หรือ JWT library

ruby
# app/controllers/graphql_controller.rb
class GraphqlController < ApplicationController
  def execute
    context = {
      current_user: current_user,
      request: request
    }
    result = SharpskillSchema.execute(
      params[:query],
      variables: params[:variables],
      context: context,
      operation_name: params[:operationName]
    )
    render json: result
  end
end

การควบคุมการเข้าถึงภายใน resolvers ตรวจสอบ context[:current_user] และ throw GraphQL::ExecutionError เมื่อไม่ได้รับอนุญาต คู่มือ authorization ของ graphql-ruby อธิบายรูปแบบ authorization ระดับ field และระดับ type

การอัปเดตแบบ Real-Time ด้วย GraphQL Subscriptions และ ActionCable

Subscriptions อนุญาตให้ clients รับการอัปเดตเมื่อเกิด events ฝั่ง server Rails ActionCable จัดการ WebSocket connection และ graphql-ruby ผสานรวมผ่าน GraphQL::Subscriptions::ActionCableSubscriptions

ruby
# app/graphql/types/subscription_type.rb
module Types
  class SubscriptionType < Types::BaseObject
    field :post_created, Types::PostType, null: false do
      argument :user_id, ID, required: false
    end

    def post_created(user_id: nil)
      object # Object ที่ส่งผ่านจาก trigger
    end
  end
end

การ trigger subscription เกิดขึ้นจากที่ใดก็ได้ในแอปพลิเคชัน โดยทั่วไปใน model callback หรือ service object

ruby
# app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user

  after_create_commit :notify_subscribers

  private

  def notify_subscribers
    SharpskillSchema.subscriptions.trigger(
      :post_created,
      { user_id: user_id },
      self
    )
  end
end

ActionCable channel ที่จัดการ GraphQL subscriptions มาพร้อมกับ gem

ruby
# app/channels/graphql_channel.rb
class GraphqlChannel < ApplicationCable::Channel
  def subscribed
    @subscription_ids = []
  end

  def execute(data)
    result = SharpskillSchema.execute(
      data['query'],
      variables: data['variables'],
      context: { current_user: current_user, channel: self },
      operation_name: data['operationName']
    )

    payload = { result: result.to_h, more: result.subscription? }

    @subscription_ids << result.context[:subscription_id] if result.subscription?

    transmit(payload)
  end

  def unsubscribed
    @subscription_ids.each do |sid|
      SharpskillSchema.subscriptions.delete_subscription(sid)
    end
  end
end

ฝั่ง client libraries เช่น Apollo Client หรือ urql เชื่อมต่อกับ ActionCable WebSocket และจัดการ subscription state สำหรับการเตรียมสัมภาษณ์เกี่ยวกับกลไก ActionCable ดู module ActionCable & WebSockets

การทดสอบ GraphQL API ด้วย RSpec

การทดสอบ GraphQL API ต้องรัน queries กับ schema และ assert โครงสร้างและข้อมูลของ response

ruby
# spec/graphql/queries/users_spec.rb
RSpec.describe 'Users query' do
  let!(:user) { create(:user, email: 'test@example.com') }

  let(:query) do
    <<~GRAPHQL
      query {
        users(emailContains: "test") {
          id
          email
        }
      }
    GRAPHQL
  end

  it 'returns users matching the filter' do
    result = SharpskillSchema.execute(query)
    users = result.dig('data', 'users')

    expect(users.length).to eq(1)
    expect(users.first['email']).to eq('test@example.com')
  end
end

การทดสอบ mutation ตรวจสอบทั้งเส้นทางสำเร็จและ validation errors

ruby
# spec/graphql/mutations/create_post_spec.rb
RSpec.describe Mutations::CreatePost do
  let(:user) { create(:user) }
  let(:context) { { current_user: user } }

  let(:mutation) do
    <<~GRAPHQL
      mutation($title: String!, $body: String!) {
        createPost(input: { title: $title, body: $body }) {
          post { id title }
          errors
        }
      }
    GRAPHQL
  end

  it 'creates a post when valid' do
    result = SharpskillSchema.execute(
      mutation,
      variables: { title: 'Hello', body: 'World' },
      context: context
    )

    data = result.dig('data', 'createPost')
    expect(data['errors']).to be_empty
    expect(data['post']['title']).to eq('Hello')
  end

  it 'returns errors when invalid' do
    result = SharpskillSchema.execute(
      mutation,
      variables: { title: '', body: 'World' },
      context: context
    )

    data = result.dig('data', 'createPost')
    expect(data['errors']).to include("Title can't be blank")
  end
end

สำหรับรูปแบบการทดสอบ Rails เพิ่มเติม ดู module Testing ด้วย RSpec

คำถามสัมภาษณ์ GraphQL ทั่วไปสำหรับ Rails Developers

ผู้สัมภาษณ์ที่ทดสอบความรู้ GraphQL ในบริบท Rails มักเน้นที่ปัญหาการ implement จริงมากกว่าทฤษฎี type ที่เป็นนามธรรม

จะป้องกัน N+1 queries ใน graphql-ruby ได้อย่างไร?

ใช้ DataLoader กับ custom sources กำหนด source class ที่รวม database calls และเรียก dataloader.with(SourceClass, args).load(id) ใน resolvers DataLoader รวบรวม IDs ทั้งหมดที่ร้องขอในการรัน GraphQL ครั้งเดียวและดึงข้อมูลใน query เดียว

อะไรคือความแตกต่างระหว่าง query และ mutation?

ความหมาย: queries อ่านข้อมูลและไม่ควรมี side effects mutations แก้ไขข้อมูลและอาจมี side effects GraphQL รับประกันการรันแบบขนานสำหรับ query fields และการรันแบบลำดับสำหรับ mutation fields ดังนั้นสอง mutations ใน request เดียวกันจะรันตามลำดับ

Subscriptions ทำงานอย่างไรใน Rails?

Subscriptions ใช้ ActionCable สำหรับ WebSocket connections client ส่ง subscription query ผ่าน WebSocket server เก็บ subscription และเมื่อ trigger ถูกเรียก จะ push ผลลัพธ์ไปยัง clients ที่ตรงกันทั้งหมด subscription ID อนุญาตให้ clients unsubscribe ได้

เมื่อใดควรเลือก GraphQL แทน REST สำหรับ Rails API?

GraphQL ลด over-fetching เมื่อ clients ต้องการ subsets ของข้อมูลที่แตกต่างกัน ซึ่งพบบ่อยในแอป mobile ที่มีขนาดหน้าจอต่างกัน มันยังไม่ต้องการ REST endpoints หลายตัวเมื่อ view เดียวต้องการข้อมูลจากหลาย models REST ยังคงง่ายกว่าสำหรับ APIs ที่เน้น CRUD พร้อมรูปแบบข้อมูลที่สม่ำเสมอและสำหรับ public APIs ที่ caching สำคัญ

จะจัดการ authentication และ authorization ใน graphql-ruby อย่างไร?

Authentication เกิดขึ้นใน controller ก่อนการรัน GraphQL โดยทั่วไปผ่าน Devise, Warden หรือการตรวจสอบ JWT user ที่ authenticated ถูกส่งผ่านใน context Authorization เกิดขึ้นใน resolvers หรือผ่าน hook authorized? ของ graphql-ruby บน types และ mutations โดย throw GraphQL::ExecutionError เมื่อถูกปฏิเสธการเข้าถึง

สำหรับ การเตรียมสัมภาษณ์ Ruby on Rails เพิ่มเติม ดู module Rails API Mode ซึ่งครอบคลุมรูปแบบการออกแบบ REST API ที่เสริมความรู้ GraphQL

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

การสร้าง Production GraphQL APIs ด้วย Rails: ประเด็นสำคัญ

  • ติดตั้ง graphql-ruby ด้วย rails generate graphql:install เพื่อสร้าง scaffold schema, types และ routing
  • ใช้ DataLoader sources สำหรับ associations ทั้งหมดเพื่อรวม database queries และป้องกันปัญหาประสิทธิภาพ N+1
  • กำหนด mutations ใน classes แยกพร้อม error fields ที่ชัดเจนใน return type
  • ผสานรวม subscriptions ผ่าน ActionCable สำหรับการอัปเดตแบบ real-time โดย trigger จาก model callbacks หรือ service objects
  • ทดสอบ queries และ mutations โดยรันโดยตรงกับ schema ใน RSpec
  • Authentication อยู่ใน controller; การตรวจสอบ authorization เกิดขึ้นภายใน resolvers โดยใช้ context[:current_user]
  • เลือก GraphQL เมื่อ clients ต้องการการดึงข้อมูลที่ยืดหยุ่น; ใช้ REST สำหรับ CRUD APIs อย่างง่ายพร้อม responses ที่สม่ำเสมอ
ชาเลนจ์ประจำวัน

คุณหาบั๊กใน Ruby on Rails เจอไหม

โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

Anthony Fillion-Maillet

เขียนโดย

Anthony Fillion-Maillet

ผู้ก่อตั้ง SharpSkill

เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่

อัปเดตเมื่อ 19 กันยายน 2569

แชร์

บทความที่เกี่ยวข้อง