Gem graphql-ruby
Bài đăng này đã không được cập nhật trong 3 năm
1. Installation
a. Gem graphql
- Để impelment GraphQL với Rails ta có thể sử dụng gem
graphql-ruby
. - Thêm
gem graphql-ruby
vàoGemfile
# Gemfile gem "graphql"
- Chạy
bundle install
để install gem vàrails generate graphql:install
để generate ra các file cần thiết choGraphQL
bundle install rails generate graphql:install
- Generator sẽ install thêm gem
graphiql-rails
vào trongdevelopment
group - Gem
graphiql-rails
cung cấp giao diện để bạn có thể test GraphQL API của bạn ở route /graphiql# config/routes.rb if Rails.env.development? mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql" end
- Đối với Rails 6 (mà mình đang làm demo), bạn cần thêm asset của graphiql vào
manifest.js
# app/assets/config/manifest.js ... //= link graphiql/rails/application.css //= link graphiql/rails/application.js
- Giao diện
grapiql
b. Các file cần chú ý
- Sau khi chạy generator thí có 1 số file cần chú ý sau
i. app/controllers/graphql_controller.rb
- Đây là file impelment
GraphqlController
với actionexecute
nơi sẽ nhận request từ client, thực hiện request và trả về response cho người dùng. - Gem
graphql
đã thêm route cho GraphQL vàoroutes.rb
# config/routes.rb post "/graphql", to: "graphql#execute"
- Ở file này bạn cần uncomment
protect_from_forgery with: :null_session
để tránh các lỗi 422 liên quan đến token# app/controllers/graphql_controller.rb class GraphqlController < ApplicationController protect_from_forgery with: :null_session def execute # ... end end
ii. app/graphql/types/query_type.rb
- Ứng với các request dạng
GET
ởREST API
thì sẽ được thay thế bằngquery
ởGraphQL
- Sau khi chạy generator thì gem
graphql
sẽ generate ra 1 example query là# app/graphql/types/query_type.rb field :test_field, String, null: false, description: "An example field added by the generator" def test_field "Hello World!" end
- Bạn có thể sử dụng POST MAN để gửi POST request lên /graphql với params query là
query { testField }
- Bạn cần chú ý sử dụng
camelCase
vớiGraphQL
thay vìsnake_case
như ruby - Response trả về sẽ có dạng:
{ "data": { "testField": "Hello World!" } }
- Trong đó
data
là node mặc định củaGraphQL
testField
là field mà bạn đã truy vấnHello World!
là giá trị được trả về của fieldtestField
được khai báo trong hàmtest_field
ởquery_type.rb
- Query
testField
vớigraphiql
iii. app/graphql/types/mutation_type.rb
- Ứng với các request dạng
POST
,PUT
,PATCH
,PATCH
ởREST API
thì sẽ được thay thế bằngmutation
ởGraphQL
- Sau khi chạy generator thì gem
graphql
sẽ generate ra 1 example mutation là# app/graphql/types/mutation_type.rb field :test_field, String, null: false, description: "An example field added by the generator" def test_field "Hello World" end
- Bạn có thể sử dụng POST MAN để gửi POST request lên /graphql với params query là
mutation { testField }
- Bạn cần chú ý sử dụng
camelCase
vớiGraphQL
thay vìsnake_case
như ruby - Response trả về sẽ có dạng:
{ "data": { "testField": "Hello World!" } }
- Trong đó
data
là node mặc định củaGraphQL
testField
là field mà bạn đã truy vấnHello World!
là giá trị được trả về của fieldtestField
được khai báo trong hàmtest_field
ởquery_type.rb
- Mutation
testField
vớigraphiql
2. Query với model
- Sau khi đã hiểu đc cách
GraphQL
hoạt động thông qua examplequery
vàmutation
- Ta có thể bắt đầu viết các
query
vàmutation
của mình - Các bạn tạo 1 model Post gồm id, title, content để test nha =))
rails g model post title content:text rails db:migrate
- Install thêm gem
ffaker
để tạo seed data nữa nha# Gemfile gem "ffaker" # db/seeds.rb 100.times do Post.create title: FFaker::Lorem.sentence, content: FFaker::Lorem.paragraph end
- Chạy seed
rails db:seed
- Done bước chuẩn bị =))
a. Tạo type ứng với model
- Ta cần tạo type cho model
Post
trướcrails g graphql:object Post
- Sau khi chạy generator ta sẽ nhận file
post_type.rb
có nội dung sau# app/graphql/types/post_type.rb module Types class PostType < Types::BaseObject field :id, ID, null: false field :title, String, null: true field :content, String, null: true field :created_at, GraphQL::Types::ISO8601DateTime, null: false field :updated_at, GraphQL::Types::ISO8601DateTime, null: false end end
- Trong đó
field
là từ khóa để khai báo field của type id
,title
,content
,created_at
,updated_at
là các field name, chúng map với các column của tableposts
ID
,String
,GraphQL::Types::ISO8601DateTime
là các kiểu dữ liệu của gemgraphql
null: true / false
cho ta biết field này có chập nhận trả về giá trinull
hay không
b. Khai báo query
- Chúng ta sẽ bắt đầu với action
posts#index
bênREST
mapping với queryposts
bênGraphQL
# app/graphql/types/query_type.rb field :posts, [Types::PostType], null: false
- Trong đó
posts
là tên của query [Types::PostType]
là kiểu dữ liệu trả vềnull: false
là không cháp nhận dữ liệu trả về lànull
c. Implement logic cho query
- Khai báo method
posts
ứng với querypost
- Method này cần trả về 1 mảng
Post
# app/graphql/types/query_type.rb field :posts, [Types::PostType], null: false def posts Post.all end
d. Test query
- Gửi request lên graphql với query có dạng
query { posts { id } }
- Response trả về có dạng
{ "data": { "posts": [ { "id": "1" }, { "id": "2" }, ... { id: "100" } ] } }
- Gửi request lên graphql với query các field khác của post
query { posts { id title content createdAt updatedAt } }
- Response trả về có dạng
{ "data": { "posts": [ { "id": "1", "title": "Id unde minus iure temporibus.", "content": "Numquam autem inventore beatae", "createdAt": "2021-05-20T12:27:46Z", "updatedAt": "2021-05-20T12:27:46Z" }, ... ] } }
- Thành công bước đấu, ăn cái bánh uống ly nước được rồi
- Vậy giả sử muốn query 1 field không có trong DB của Post thì làm sao ? sang phần tiếp theo nha =))
e. Custom field
- Giả sử 1 ngày đẹp trời KH muốn query thêm field
bio
với post nhưng field này không được lưu trong database (ví dụ muốn GET bio của post phải gọi API của bên thứ 3 hoặc logic gì khác) - Ta cần thêm
field :bio
vàoPostType
và khai báo hàmbio
để trả về giá trị cho field bio - Tương tự như hàm
test_field
để trả về giá trị cho fieldtest_field
trongquery_type.rb
hoặcmutation_type.rb
# app/graphql/types/post_type.rb module Types class PostType < Types::BaseObject ... field :bio, String, null: false def bio # Example call PostAPI to get post's bio # PostAPI.find(object.id).bio FFaker::Lorem.sentence end end end
- Trong method
bio
mình đang gọiobject
- Object này chính là post mà
PostType
đang sử dụng để trả về giá trị cho các file - Bạn có thể nhìn lại đoạn code trong
QueryType
để hiểu hơn# app/graphql/types/query_type.rb module Types class QueryType < Types::BaseObject .... field :posts, [Types::PostType], null: false def posts Post.all end end end
- Mỗi
Types::PostType
sẽ được initialize vớiobject
là post được trả về từPost.all
- Gửi request lên graphql với query có dạng
query { posts { id bio } }
- Response trả về có dạng
{ "data": { "posts": [ { "id": "1", "bio": "Eaque recusandae aspernatur perferendis vero in nobis aperiam." }, { "id": "2", "bio": "Quod itaque corporis debitis inventore iste laborum sequi illum." }, ... { id: "100", "bio": "Similique iusto explicabo voluptate reiciendis quisquam expedita atque optio." } ] } }
- Các
field :id, :title
ta không cần khai báo các hàmid
,title
tương ứng trongTypes::PostType
vì các hàm này sẽ được delegate đến các hàmid
,title
# app/graphql/types/post_type.rb module Types class PostType < Types::BaseObject field :id, ID, null: false # có 1 hàm id và delegate tới hàm id của post # def id # object.ì # end end end
- Áp dụng tương tự ta có thể move hàm bio vào model Post
# app/graphql/types/post_type.rb module Types class PostType < Types::BaseObject field :id, ID, null: false field :title, String, null: true field :content, String, null: true field :created_at, GraphQL::Types::ISO8601DateTime, null: false field :updated_at, GraphQL::Types::ISO8601DateTime, null: false field :bio, String, null: false end end
f. Association field
- Ngoài các field có trong database, custom field như các ví dụ trên, ta còn có thể trả về field ứng với các association của record
- Ví dụ khi query post ra cần query thêm comments của post
- Let's start
- Tạo model Comment
rails g model Comment post:references content:text
- Update code vào các file cần thiết
# app/models/post.rb class Post < ApplicationRecord has_many :comments end # app/models/post.rb class Comment < ApplicationRecord belongs_to :post end # db/seeds.rb 100.times do post = Post.create title: FFaker::Lorem.sentence, content: FFaker::Lorem.paragraph 3.times { post.comments.create! content: FFaker::Lorem.sentence } end
- Run your code =))
rails db:migrate rails db:seed
- Chạy graph generator với Comment
rails g graphql:object Comment
- Generator sinh ra class
CommentType
# app/graphql/types/comment_type.rb module Types class CommentType < Types::BaseObject field :id, ID, null: false field :post_id, Integer, null: false field :content, String, null: true field :created_at, GraphQL::Types::ISO8601DateTime, null: false field :updated_at, GraphQL::Types::ISO8601DateTime, null: false end end
- Thêm field
comments
vàoPostType
như sau# app/graphql/types/post_type.rb module Types class PostType < Types::BaseObject ... field :comments, [Types::CommentType], null: false end end
- Gửi request lên graphql với query có dạng
query { posts { id bio comments { id content } } }
- Response trả về có dạng
{ "data": { "posts": [ { "id": "1", "bio": "Eaque recusandae aspernatur perferendis vero in nobis aperiam.", "comments": [ { "id": "1", "content": "Dignissimos perspiciatis nam qui minus velit." }, { "id": "2", "content": "Provident unde sed eaque modi eius reiciendis." }, { "id": "3", "content": "Tempora repellendus nisi incidunt quasi adipisci quod molestiae." } ] }, { "id": "2", "bio": "Quod itaque corporis debitis inventore iste laborum sequi illum.", "comments": [ { "id": "4", "content": "Facilis exercitationem accusamus ipsa nostrum sint vitae at maiores." }, { "id": "5", "content": "Aut aspernatur maiores fugit reiciendis aperiam." }, { "id": "6", "content": "Dignissimos cumque recusandae eum magnam." } ] }, ... { id: "100", "bio": "Similique iusto explicabo voluptate reiciendis quisquam expedita atque optio.", "comments": [] } ] } }
g. Thêm params vào query
- Hiện tại với query
posts
chúng ra đang queryPost.all
và trả về tất cả post - Dự án thực tế sẽ không ai làm như vậy cả =))
- Chúng ra cần truyển thêm 1 số thông tin như
page
,per
để thực hiện pagination =)) - Ví dụ
query { posts(page: 1, per: 10) { id title content bio } }
- Let's start =))
- Để thêm params vào query chúng ra sử dụng keywork
argument
- Với ví dụ trên ta sẽ update lại file code như sau
# app/graphql/types/query_type.rb module Types class QueryType < Types::BaseObject ... field :posts, [Types::PostType], null: false do argument :page, Int, required: true argument :per, Int, required: true end def posts(page:, per: ) Post.page(page).per(per) end end end
- Trong đó
argument
là từ khóa để khai báo params cho query :page
,:per
là tên của paramsInt
là kiểu dữ liệu của paramsrequired
là params có bắt buộc hay không- Hàm
posts
đang nhận 1 tham số dạng hash với 2 key là page và per - Ta có thể khai báo hàm
posts
dưới 1 dạng khác như saudef posts(**args) Post.page(args[:page]).per(args[:per]) end
- Gửi request lên graphql với query có dạng
query { posts(page: 1, per: 2) { id title } }
- Response trả về có dạng
{ "data": { "posts": [ { "id": "1", "title": "Nam repudiandae vitae fugit facere placeat ea.", }, { "id": "2", "title": "Voluptatum ullam nisi inventore placeat quisquam reiciendis.", } ] } }
h. Resolver cho query
- Hiện tại mình đang khai báo và implement logic của query
posts
trongquery_type.rb
- Giả sử số lượng query nhiều và logic query trở nên phức tạp hơn thì sẽ rất khó để maintain và develop
- Để giải quyết vấn đề trên ta có thể implement logic query trong resolver
- Ví dụ
# app/graphql/types/query_type.rb module Types class QueryType < Types::BaseObject ... field :posts, resolver: Resolvers::Posts::IndexResolver end end # app/graphql/resolvers/base_resolver.rb module Resolvers class BaseResolver < GraphQL::Schema::Resolver end end # app/graphql/resolvers/posts/index_resolver.rb module Resolvers class Posts::IndexResolver < Resolvers::BaseResolver type [Types::PostType], null: false argument :page, Int, required: true argument :per, Int, required: true def resolve(page:, per:) Post.page(page).per(per) end end end
- Gửi request lên graphql với query có dạng
query { posts(page: 1, per: 2) { id title comments { id content } } }
- Response trả về có dạng
{ "data": { "posts": [ { "id": "1", "title": "Nam repudiandae vitae fugit facere placeat ea.", "comments": [ { "id": "1", "content": "Dignissimos perspiciatis nam qui minus velit." }, { "id": "2", "content": "Provident unde sed eaque modi eius reiciendis." }, { "id": "3", "content": "Tempora repellendus nisi incidunt quasi adipisci quod molestiae." } ] }, { "id": "2", "title": "Voluptatum ullam nisi inventore placeat quisquam reiciendis.", "comments": [ { "id": "4", "content": "Facilis exercitationem accusamus ipsa nostrum sint vitae at maiores." }, { "id": "5", "content": "Aut aspernatur maiores fugit reiciendis aperiam." }, { "id": "6", "content": "Dignissimos cumque recusandae eum magnam." } ] } ] } }
- Done query cơ bản =))))))))
i. Handle error
- Chúng ta sẽ minh họa phần này bằng API
post
với id là -1 - implement các file cần thiết
# app/graphql/types/query_type.rb module Types class QueryType < Types::BaseObject ... field :post, resolver: Resolvers::Posts::ShowResolver end end # app/graphql/resolvers/posts/show_resolver.rb module Resolvers class Posts::ShowResolver < Resolvers::BaseResolver type Types::PostType, null: false argument :id, Int, required: true def resolve(id:) Post.find(id) end end end
- Gửi request lên graphql với query có dạng
query { post(id: 1) { id title comments { id content } } }
- Response trả về có dạng
{ "data": { "post": { "id": "1", "title": "Nam repudiandae vitae fugit facere placeat ea.", "comments": [ { "id": "1", "content": "Dignissimos perspiciatis nam qui minus velit." }, { "id": "2", "content": "Provident unde sed eaque modi eius reiciendis." }, { "id": "3", "content": "Tempora repellendus nisi incidunt quasi adipisci quod molestiae." } ] } } }
- Gửi request lên graphql với case lỗi =))
query { post(id: -1) { id title comments { id content } } }
- Response trả về có dạng
{ "errors": [ { "message": "Couldn't find Post with 'id'=-1", "backtrace": [ "/home/le.tan.thanh/.rvm/gems/ruby-3.0.1/gems/activerecord-6.1.3.2/lib/active_record/core.rb:338:in `find'", ... "/home/le.tan.thanh/.rvm/gems/ruby-3.0.1/gems/puma-5.3.1/lib/puma/thread_pool.rb:145:in `block in spawn_thread'" ] } ], "data": {} }
- Đấy là ở môi trường development, lỗi được handle để response JSON cho developer fix
# app/controllers/graphql_controller.rb class GraphqlController < ApplicationController # If accessing from outside this domain, nullify the session # This allows for outside API access while preventing CSRF attacks, # but you'll have to authenticate your user separately protect_from_forgery with: :null_session def execute ... rescue StandardError => e raise e unless Rails.env.development? handle_error_in_development(e) end end
- Ở môi trường production sẽ trả về HTML của page 500
- Toang thật sự =))
- Để handle lỗi ở graphql ruby bạn thêm
rescue_from
block vào schema của graphql - Ở ví dụ của mình là
VibloGraphqlRubySchema
# app/graphql/viblo_graphql_ruby_schema.rb class VibloGraphqlRubySchema < GraphQL::Schema ... rescue_from(ActiveRecord::RecordNotFound) do |err, obj, args, ctx, field| raise GraphQL::ExecutionError.new err.message, extensions: { err: err, obj: obj, args: args, ctx: ctx } end
- Để trả về response lỗi, ta sử dụng
rescue_from
và raiseGraphQL::ExecutionError
- Block
rescue_from
nhận các parmas err
: error được raise ởquery_type
obj
object xảy ra lỗiargs
parmas mà query lỗi nhân vàoctx
context màgraphql_controller
truyền vàoquery_type
- Với
GraphQL::ExecutionError
ngoài message lỗi ta có thể truyền thêm 1 params là extensions để custom thêm các field trả về - Gửi request lên graphql với case lỗi =))
query { post(id: -1) { id title comments { id content } } }
- Response trả về có dạng
{ "data": null, "errors": [ { "message": "Couldn't find Post with 'id'=-1", "locations": [ { "line": 2, "column": 7 } ], "path": [ "post" ], "extensions": { "err": "Couldn't find Post with 'id'=-1", "obj": null, "args": { "id": -1 }, "ctx": {} } } ] }
- Done phần handle lỗi, khá giống
rescue_from
để handle lỗi ở controller nhỉ =))
3. Mutation với model
- Các kiến thức đổi với mutation cũng tương tự như với query, bạn chỉ cần chú ý
- Khai báo mutation ở file
mutation_type.rb
- Thay
resolver
vớimutation
- Khai báo mutation ở file
- Let's start, tạo 1 cái mutation
create_post
nha các bạn# app/graphql/types/mutation_type.rb module Types class MutationType < Types::BaseObject field :create_post, mutation: Mutations::Posts::CreateMutation end end # app/graphql/mutations/posts/create_mutation.rb module Mutations class Posts::CreateMutation < Mutations::BaseMutation argument :title, String, required: true argument :content, String, required: true field :post, Types::PostType, null: false def resolve(title:, content:) { post: Post.create!(title: title, content: content) } end end end
- Gửi request lên graphql
mutation { createPost(input: {title: "title", content: ""}) { id title content comments { id content } } }
- Response trả về có dạng
{ "data": { "createPost": { "post": { "id": "110", "title": "title", "content": "content", "comments": [] } } } }
- Bạn cũng có thể bổ sung thêm
field :erors
để chứa message lỗi trong trường hợp create post lỗi# app/graphql/mutations/posts/create_mutation.rb module Mutations class Posts::CreateMutation < Mutations::BaseMutation ... field :post, Types::PostType, null: false field :errors, [String], null: false ... end end
4. Advance
- Ngoài ra cón có 1 số topic khá hay về
GraphQL
như sau Subscriptions
để theo dõi sự kiên (có thể hiểu như 1 dạng callback)Input Object
trong các ví dụ trên các argument đều là các kiểu có sẵn của GraphQL, sử dụng input object để tạp thêm kiểu dữ liệu mới cho argumentValidation
cung cấp các validate cho field tương tự như với model
5. Link tham khảo
- graphql: https://graphql.org/
- graphql-ruby: https://graphql-ruby.org/getting_started
- Sample code: https://github.com/thanhlt-1007/viblo_graphql_ruby
- Happy coding
All rights reserved