Gem graphql-ruby
Bài đăng này đã không được cập nhật trong 4 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-rubyvàoGemfile# Gemfile gem "graphql" - Chạy
bundle installđể install gem vàrails generate graphql:installđể generate ra các file cần thiết choGraphQLbundle install rails generate graphql:install - Generator sẽ install thêm gem
graphiql-railsvào trongdevelopmentgroup - Gem
graphiql-railscung 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
GraphqlControllervới actionexecutenơ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 APIthì sẽ được thay thế bằngqueryởGraphQL - Sau khi chạy generator thì gem
graphqlsẽ 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
camelCasevớiGraphQLthay vìsnake_casenhư ruby - Response trả về sẽ có dạng:
{ "data": { "testField": "Hello World!" } } - Trong đó
datalà node mặc định củaGraphQLtestFieldlà 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
testFieldvớigraphiql![]()
iii. app/graphql/types/mutation_type.rb
- Ứng với các request dạng
POST,PUT,PATCH,PATCHởREST APIthì sẽ được thay thế bằngmutationởGraphQL - Sau khi chạy generator thì gem
graphqlsẽ 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
camelCasevớiGraphQLthay vìsnake_casenhư ruby - Response trả về sẽ có dạng:
{ "data": { "testField": "Hello World!" } } - Trong đó
datalà node mặc định củaGraphQLtestFieldlà 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
testFieldvớigraphiql![]()
2. Query với model
- Sau khi đã hiểu đc cách
GraphQLhoạt động thông qua examplequeryvàmutation - Ta có thể bắt đầu viết các
queryvàmutationcủ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
Posttrướcrails g graphql:object Post - Sau khi chạy generator ta sẽ nhận file
post_type.rbcó 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 đó
fieldlà từ khóa để khai báo field của type id,title,content,created_at,updated_atlà các field name, chúng map với các column của tablepostsID,String,GraphQL::Types::ISO8601DateTimelà các kiểu dữ liệu của gemgraphqlnull: true / falsecho ta biết field này có chập nhận trả về giá trinullhay không
b. Khai báo query
- Chúng ta sẽ bắt đầu với action
posts#indexbênRESTmapping với querypostsbênGraphQL# app/graphql/types/query_type.rb field :posts, [Types::PostType], null: false - Trong đó
postslà tên của query [Types::PostType]là kiểu dữ liệu trả vềnull: falselà 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
biovớ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 :biovàoPostTypevà 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_fieldtrongquery_type.rbhoặ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
biomì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::PostTypesẽ được initialize vớiobjectlà 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, :titleta không cần khai báo các hàmid,titletương ứng trongTypes::PostTypevì 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
commentsvàoPostTypenhư 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
postschúng ra đang queryPost.allvà 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 đó
argumentlà từ khóa để khai báo params cho query :page,:perlà tên của paramsIntlà kiểu dữ liệu của paramsrequiredlà 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
postsdướ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
poststrongquery_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
postvớ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_fromblock 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_fromvà raiseGraphQL::ExecutionError - Block
rescue_fromnhận các parmas err: error được raise ởquery_typeobjobject xảy ra lỗiargsparmas mà query lỗi nhân vàoctxcontext màgraphql_controllertruyền vàoquery_type- Với
GraphQL::ExecutionErrorngoà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
resolvervớimutation
- Khai báo mutation ở file
- Let's start, tạo 1 cái mutation
create_postnha 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ề
GraphQLnhư sau Subscriptionsđể theo dõi sự kiên (có thể hiểu như 1 dạng callback)Input Objecttrong 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 argumentValidationcung 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



