In the previous post about solving N+1 query in graphQL using graphql-batch, we discussed batching queries using the graphql-batch gem. This works great for belongs_to relationships, but it is not useful for preloading has_many associations. For example, if you wanted the comments for each article, it would again produce N+1 queries. Luckily I found a gem graphql-preload that solves this problem. The graphql-preload gem is created based on a gist by @theorygeek. This gem depends on the graphql-batch gem. so we need to keep graphql-batch settings in the schema for graphql-preload work. detailed explanation about graphql batch is given here.
Installation
Add this line to your application’s Gemfile:
Gemfile
1
gem 'graphql-preload'
Add enable_preloading to the schema file to be able to use this functionality.
mutationaddArticle{addArticle(input:{title:"GraphQL mutation",body:" This article is about graphql mutation using InputObjectType"}){article{idtitlebody}}}
For saving this we need to instantiate article object by assigning each and every input params like this.
This method become ugly if our object contain lots fields.
We can define and utilize mutation input params in the better way using InputObjectType. If we can specify article object as input_field instead of each field we can save object like below.
app/graphql/mutations/article_mutations.rb
1
article=Article.new(inputs[:article].to_h)
which is maintanable and easy to read.
Lets see how we can achive this.
The first step is to define InputObjectType and declare that input object type as input_field. Here we are going to create InputObjectType in another folder inside our graphql folder for maintainability.
Now we can declare ArticleInputObjectType as input_field inside our mutation and use declared input_field inside our mutation resolver to save the article. So final mutation definition will look like this.
That’s it, we are done, now we can save new article using addArticle mutation.
12345678910
mutationaddArticle{addArticle(input:{article:{title:"GraphQL mutation",body:" This article is about graphql mutation using InputObjectType"}}){article{idtitlebody}}}
If we want to map camel case arguments into DB fields which are in snake case, we can use as keyword.
Deccan RubyConference is a Single-Track Conference happening at Pune every year. Deccan RubyConf 2017 took place 12th Aug. 2017 at Sheraton Grand Pune. The conference covered areas like Ruby Language, Framework, and Tools by well-known speakers and also few first-time speakers from Ruby community.
I got a chance to attend This year edition as speaker. My topic was Give REST a rest, use GraphQL for your next Ruby API. In my 25 minutes session, I talked about why should we use GraphQL and also about its implementation in Ruby. After conference session few people from different companies approached me and asked about GraphQL which was really a confidence booster for me as a speaker. Few people shared the issues they faced in GraphQL which given me new ideas and thought process.
The conference started with the keynote by Tim Riley. Tim is from Australian and partner at Australian design agency Icelab. He talked about next generation Ruby web apps with dry-rb. Next talk was about leveraging the hidden powers of a database to make Rails, the swiss army knife of web development, manipulate and present data faster by Harisankar, founder of Red Panthers. Aboobacker MK from Red Panthers talked about Garbage collector in Ruby and some practical tips to improve Ruby performance.
After my session, Shaunak Pagnis from Amura talked about the Active record and beyond. After Lunch Michael Kohl from Austria, currently, serves as CTO of Lockstap Labs talked about writing maintainable Rails applications. The sixth session was a panel discussion with the panels including Ajey Gore - Group CTO of Go-Jek, Vipul - Director BigBinary, Tim Riley, Michael Kohl, Gautam Rege -Director Josh Software discussed their experience of building a start-up and hiring strategies.
Followed by Panel Discussion, there were 9 flash talks which included 5 speakers from Kerala, talked about different ruby topics from beginner level to advanced level. After flash talks, Douglas Vaz talked about HTTP/2 World. Conference session ended with Keynote by Ajey Gore. He talked about testing principles and why testing is important.
After sessions, there were Open hours which gave opportunity meet and interact with different kind of people. For me, it was a great learning experience both listening to people share their contributions and ideas which really inspired me to code better and learn better.
All the photos from the conference is available here.
One of the main highlights of GraphQl is declarative fetching, the client can define the shape of the data he needed and GraphQL server will provide data in the same shape. But an evil client can exploit this advantage to make deeper nesting and complex queries and can do DDoS attack.
GraphQL provide three confirations to avoid this issue.
Timeout:
We can avoid queries utilizing server more than specified time by setting a timeout. If a query takes more time for execute, then after time out GraphQL will return the result of query up to that time and simply reject nonexcecuted part of the query.
Maximum Depth: Another solution for prevent deeply-nested queries is setting max_deplth. If our query depth is greater than max_depth we set, the server will simply reject full query and return an error message.
Query Complexity: We can prevent complex queries by setting max_complexity in GraphQL schema. Each field in our type system has a complexity number so if our query complexity exceeds max_complexity, the server will reject query just like in the case of max_depth.
By default complexity of field is 1, but we can configure constant complexity for fields in type system and also can set complexity dynamically by passing conditions
123456789101112131415161718192021
# Constant complexity:field:comments,types[CommentType],complexity:10# Dynamic complexity:field:top_comments,types[CommentType]doargument:limit,types.Int,default_value:5complexity->(ctx,args,child_complexity){ifctx[:current_user].admin?# no limit for admin users0else# `child_complexity` is the value for selections# which were made on the items of this list.## We don't know how many items will be fetched because# we haven't run the query yet, but we can estimate by# using the `limit` argument which we defined above.args[:limit]*child_complexityend}end
We can make GraphQL Types and mutations DRY using interfaces. An Interface is an abstract type that contains a collection of types which implement some of the same fields. We can avoid specifying the same set of fields in different GraphQL Types and mutations by defining an interface and using in sharing Types and mutations.
Interfaces can have fields, defined with a field, just like a GraphQL object type. Objects which implement this field inherit field definitions from the interface. An object type can override the inherited definition by redefining that field.
For example, active record time stamps are common fields in Rails models. So we can avoid declaring these fields in all object types by declaring an interface ActiveRecordTimestamp with these fields and using it our object types.
Example for including multiple interfaces in Ruby object type.
app/graphql/types/comment_type.rb
123456789
CommentType=GraphQL::ObjectType.definedoname"Comment"# multiple interfaces included using comma.interfaces[ActiveRecordTimestamp,GraphQL::Relay::Node.interface]field:id,types.Intfield:comment,types.Stringfield:user,UserTypeend
Now, this active record time stamp will be available in both above-mentioned object types.
We can use return_interfaces to define and reuse return types in different mutation definitions. The result of the resolve block will be passed to the field definitions in the interfaces, and both interface-specific and mutation-specific fields will be available to clients.
For example, we can define a interface which will define notification of a mutation.
# encoding: utf-8CreateArticle=GraphQL::Relay::Mutation.definedo# ...return_field:title,types.Stringreturn_field:body,types.Stringreturn_interfaces[MutationResult],# clientMutationId will also be available automaticallyresolve->(obj,input,ctx){article,notice=Article.create_with_input(...){success:article.persisted?notice:noticetitle:article.titleerrors:article.errors}}end
GraphQL endpoints, we can expose errors as part of our schema. We should check errors fields to see if any errors in result data. For example if we query a field which is not existing in type system, we will get a error response. This type of errors is not supposed to be displayed to end users. It helps with debugging, error tracking etc.
123456789101112131415161718
{"errors":[{"message":"Field 'user' doesn't exist on type 'Article'","locations":[{"line":5,"column":5}],"fields":["query","article","user"]}]}
If a field’s resolve function returns an ExecutionError, the error will be inserted into the response’s errors key and the field will resolve to nil. It is often required to perform additional validation of the input parameters passed to GraphQL mutations, and provide user-friendly error messages in case validation fails or mutation cannot be completed successfully.
For example, we could add errors to ArticleType:
Then, when clients create a article, they should check the errors field to see if it was successful:
1234567
mutation{createArticle(article:{title:"GraphQL is Nice"}){idtitleerrors# in case the save failed}}
If errors are present (and id is null), the client knows that the operation was unsuccessful, and they can discover reason. If some part of our resolve function would raise an error, we can rescue it and add to the errors key by returning a GraphQL:: ExecutionError
app/graphql/mutations/article_mutations.rb
1234567891011
resolve->(obj,args,ctx){article_params=args["article"].to_hbeginarticle=Article.create!(post_params)# on success, return the article:articlerescueActiveRecord::RecordInvalid=>err# on error, return an error:GraphQL::ExecutionError.new("Invalid input for Article: #{article.errors.full_messages.join(", ")}")end}
If we don’t want to begin … rescue … end in every field, we can wrap resolve functions in error handling. For example, we could make an object that wraps another resolver:
app/graphql/resolvers/rescue_form.rb
1234567891011121314151617
# Wrap field resolver `resolve_func` with a handler for `error_superclass`.# `RescueFrom` instances are valid field resolvers too.classRescueFromdefinitialize(error_superclass,resolve_func)@error_superclass=error_superclass@resolve_func=resolve_funcenddefcall(obj,args,ctx)@resolve_func.call(obj,args,ctx)rescue@error_superclass=>err# Your error handling logic here:# - return an instance of `GraphQL::ExecutionError`# - or, return nil:nilendend
apply it to fields on an opt-in basis:
app/graphql/mutations/article_mutations.rb
1234
field:create_article,ArticleTypedo# Wrap the resolve function with `RescueFrom.new(err_class, ...)`resolveRescueFrom.new(ActiveRecord::RecordInvalid,->(obj,args,ctx){...})end
Mutation is a special type of query used to change data in the database like Creating, Editing or Deleting Records from a table or Store. These are the equivalent to the POST, PUT, PATCH and DELETE in HTTP/REST speak. Defining mutations is very similar to defining queries. The only difference is how you implement the logic inside the mutation. In mutation, we can control and specify the output data that API need to return after mutation procedure.
In this article, I am Adding a mutation query to add comments to an article which we discussed in previous example.
To add mutations to your GraphQL schema, first we need to define a mutation type in mutations folder
Like QueryType, MutationType is a root of the schema. Members of MutationType are mutation fields. For GraphQL in general, mutation fields are identical to query fields except that they have side-effects (which mutate application state, eg, update the database).
Since we created new folder for mutations, we have to tell Rails to autoload paths. Put below code in application.rb to autoload it.
Now we need to define specific mutation query. Following are the process to define a mutation - give operation a name - declare its inputs - declare its outputs - declare the mutation procedure in resolve block. resolve should return a hash with a key for each of the return_fields
In out example, we need to define CommentMutations in mutations folder.
Here input_field specify the input params we can pass in the query. In return_field, we can specify the fields returning after the update. Inside resolve block, we define the business logic. and resolve should return a hash with a key for each of the return_fields.
After defining this, we need to add the mutation’s derived field to the mutation type.
app/graphql/mutations/mutation_type.rb
12345
MutationType=GraphQL::ObjectType.definedoname"Mutation"# Add the mutation's derived field to the mutation typefield:addComment,field:CommentMutations::Create.fieldend
{"data":{"addComment":{"article":{"id":1,"comments":[{"comment":"Good article","user":{"name":"Shaiju E"}},{"comment":"Keep going","user":{"name":"Shaiju E"}},{"comment":"Another Comment","user":{"name":"David"}},{"comment":"New Comment","user":{"name":"Shaiju E"}},{"comment":"Another Comment from User 2","user":{"name":"David"}},{"comment":"Another Comment from User 1","user":{"name":"Shaiju E"}},{"comment":"TEST","user":{"name":"Shaiju E"}},{"comment":"New comment","user":{"name":"Shaiju E"}}]}}}}
We can call the same query by passing inputs using variables
$comments: AddCommentInput! will configure the variable $comments to take values from query variables section. input: $comments will pass $comments as input to mutation query.
Lets write another example for updation mutation. If we want to update a comment, we need to write UpdateComment mutation in comment_mutations.rb
# encoding: utf-8moduleCommentMutationsCreate=GraphQL::Relay::Mutation.definedoname"AddComment"# Define input parametersinput_field:articleId,!types.IDinput_field:userId,!types.IDinput_field:comment,!types.String# Define return parametersreturn_field:article,ArticleTypereturn_field:errors,types.Stringresolve->(object,inputs,ctx){article=Article.find_by_id(inputs[:articleId])return{errors:'Article not found'}ifarticle.nil?comments=article.commentsnew_comment=comments.build(user_id:inputs[:userId],comment:inputs[:comment])ifnew_comment.save{article:article}else{errors:new_comment.errors.to_a}end}endUpdate=GraphQL::Relay::Mutation.definedoname"UpdateComment"# Define input parametersinput_field:id,!types.IDinput_field:comment,types.IDinput_field:userId,types.IDinput_field:articleId,types.ID# Define return parametersreturn_field:comment,CommentTypereturn_field:errors,types.Stringresolve->(object,inputs,ctx){comment=Comment.find_by_id(inputs[:id])return{errors:'Comment not found'}ifcomment.nil?valid_inputs=ActiveSupport::HashWithIndifferentAccess.new(inputs.instance_variable_get(:@original_values).select{|k,_|comment.respond_to?"#{k}=".underscore}).except(:id)ifcomment.update_attributes(valid_inputs){comment:comment}else{errors:comment.errors.to_a}end}endend
Main defference here is, we need to create valid_inputs. This will allow us mass assignment with update attributes with valied fields which we passed.
After defining this, we need to add the mutation’s derived field to the mutation type.
app/graphql/mutations/mutation_type.rb
123456
MutationType=GraphQL::ObjectType.definedoname"Mutation"# Add the mutation's derived field to the mutation typefield:addComment,field:CommentMutations::Create.fieldfield:updateComment,field:CommentMutations::Update.fieldend
Mutation for delete a comment and return post and deleted comment ID
app/graphql/mutations/comment_mutations.rb
123456789101112131415161718192021222324252627
# encoding: utf-8moduleCommentMutationsDestroy=GraphQL::Relay::Mutation.definedoname'DestroyComment'description'Delete a comment and return post and deleted comment ID'# Define input parametersinput_field:id,!types.ID# Define return parametersreturn_field:deletedId,!types.IDreturn_field:article,ArticleTypereturn_field:errors,types.Stringresolve->(_obj,inputs,ctx){comment=Comment.find_by_id(inputs[:id])return{errors:'Comment not found'}ifcomment.nil?article=comment.articlecomment.destroy{article:article.reload,deletedId:inputs[:id]}}end# Other mutations defined here....end
app/graphql/mutations/mutation_type.rb
1234567
MutationType=GraphQL::ObjectType.definedoname"Mutation"# Add the mutation's derived field to the mutation typefield:addComment,field:CommentMutations::Create.fieldfield:updateComment,field:CommentMutations::Update.fieldfield:destroyComment,field:CommentMutations::Destroy.fieldend
GraphiQL is a graphical interactive in-browser GraphQL IDE. This is a great tool for testing GraphQL endpoints and also for Exploring documentation about GraphQL Servers. There are some limitations for default GraphiQL tool, It does not provide any option for saving queries like we have in postman and other API clients. It only saves the latest query in local storage. Another issue with GraphiQL is, it not providing any option for pass headers into GraphQL queries. It is very critical for the testing of application which needs to pass auth-token or some other headers. In this post Iam exploring few alternativs.
GraphQL IDE
GraphQL IDE is n extensive IDE for exploring GraphQL API’s. It allows manage projects, import/export, store queries, toggle query history, passing custom headers and setting environment variables for dynamic headers. Currently, it is only available for MacOS. Window / Linux version of this application is under development but can build the binary. If you have Homebrew installed on OSX, you can install it by:
GraphiQL.app is light, Electron-based wrapper around GraphiQL. This Provides a tabbed interface for editing and testing GraphQL queries/mutations with GraphiQL. It is only available for MacOS. If you have Homebrew installed on OSX, you can install it by:
1
brew cask install graphiql
GraphiQL Feen(Chrome Extension)
GraphiQL Feen is a Chrome Extension that allows you to explore and test GraphQL endpoints. It utilizes the Popular GraphiQL component. It
Features are: Save/Load Queries and variables to build a library of useful queries. * Save/Select Server Definitions so you can have different settings for different servers. * GET/POST/Multi-part GraphQL requests supported! * Authorization defined so cookies forwarded to the domain * Define Headers that will be sent with the request, Headers can even override existing Request headers: * Define the Origin: header for CORS requests to allow your server to process correctly. * Define CSRF token headers * Override all Cookies so you can pass authentication information with your requests. * EXPORT your entire application state to a JSON file * IMPORT saved state so you can restore your state
One of the most important pain points in GraphQL is the problem thats commonly referred to as N+1 SQL queries. GraphQL query fields are designed to be stand-alone functions, and resolving those fields with data from a database might result in a new database request per resolved field.
For a simple RESTful API endpoint logic, it’s easy to analyze, detect, and solve N+1 issues by enhancing the constructed SQL queries. For GraphQL dynamically resolved fields, it’s not that simple.
For example, Consider the GraphQL query to fetch article, its comments and commented user which I mentioned in previous post.
{"data":{"acticle":{"title":"A GraphQL Server","comments":[{"comment":"Good article","user":{"id":1,"name":"Shaiju E"}},{"comment":"Keep going","user":{"id":1,"name":"Shaiju E"}},{"comment":"Another Comment","user":{"id":2,"name":"David"}},{"comment":"New Comment","user":{"id":1,"name":"Shaiju E"}},{"comment":"Another Comment from User 2","user":{"id":2,"name":"David"}},{"comment":"Another Comment from User 1","user":{"id":1,"name":"Shaiju E"}},{"comment":"TEST","user":{"id":1,"name":"Shaiju E"}}]}}}
This will fire user query in for each comment. Total 7 instead of 1 for fetch 2 user. In Rest API we can solve this issue by eager loading users while fetching comments. But GraphQL query fields are designed to be stand-alone functions, and not aware of other functions.
Facebook introduced DataLoader to solve this problem in Javascript projects. Shopify created GraphQL::Batch to solve this N+1 problem in ruby. GraphQL::Batch Provides an executor for the graphql gem which allows queries to be batched. This is a flexible toolkit for lazy resolution with GraphQL.
Installation
1
gem'graphql-batch'
Now we need to define a custom loader, which is initialized with arguments that are used for grouping and a perform method for performing the batch load.
Here, resolve -> (obj, args, ctx) {RecordLoader.for(User).load(obj.user_id) } will make user fetching lazy loading there by solve N+1 query problem.
Before:
After
More information about GraphQL::Batch is available in Gem Documentation
You can see sample code here.
One of the main advantages of GraphQL is, we can create queries with a different combination of datasets which will avoid multiple requests to fetch required data, thereby eliminating overheads. In this post, I am discussing how we can create nested datasets in GraphQL.
In the previous post, I mentioned about Article model and query to fetch an article. Here I’m going to fetch comments under the article. Assuming we have comments and user model with corresponding associations.
Here comments are an array of objects so we need to specify CommentType using types keyword. We can see in comment_type.rb we are not specifying types for UserType, as it is returning a single object. Since we defined association in, This will fetch all comments of the article by executing article.comments.
Put below code in application.rb to autoload graphql and types folder like so: