Sunday, 28 December 2014

API Development in Ruby On Rails, Nested Entities

There are times when there is one-to-many association amongst the entities. And the mobile developer needs to show all of them in one screen. In these cases it is very easy to fell into the bad habit to execute n+1 queries from mobile side toward the server or from the server to the sql-server.

Here are the entities:

class DetailedGameEntity < Grape::Entity
  ...
  expose :reviews, using: ReviewEntity do |game, options|
    game.reviews
  end
end

class ReviewPresenter < Grape::Entity
  expose :user_id, as: :owner_id
  expose :comment
  expose :rating

  expose :image_urls do |review, options|
    review.images.map{|image| image.image}
  end
end

They are designed to return everything the mobile screen needs. The games contains their reviews. And the reviews contain their images. So in one query the whole mobile screen can be populated.

Let's see how a programmer in a hurry develops all this API:

games = Game.actual.limit(500)
present games, with: DetailedGameEntity

By developing the API this way the call will result in these queries:
...
  Review Load (0.5ms)  SELECT "reviews".* FROM "reviews"  WHERE "reviews"."reviewable_id" = $1 AND "reviews"."reviewable_type" = $2  [["reviewable_id", 2359], ["reviewable_type", "Game"]]
  Review Load (0.4ms)  SELECT "reviews".* FROM "reviews"  WHERE "reviews"."reviewable_id" = $1 AND "reviews"."reviewable_type" = $2  [["reviewable_id", 2358], ["reviewable_type", "Game"]]
...

As you can see for each returned game game another select is executed to fetch the belonging reviews. The same is done to fetch the belonging user and to fetch the images belonging to the review. This is a very ineffective way because as the review count grows more and more queries need to be executed. This problem is called n+1 query problem.

I do this in my code in these cases:

games = Game.actual.limit(500)
games = games.includes(reviews: [:images, :user] )
present games, with: DetailedGameEntity

The above code will do this sql query:

 Review Load (1.0ms)  SELECT "reviews".* FROM "reviews"  WHERE "reviews"."reviewable_type" = 'Schedule' AND "reviews"."reviewable_id" IN (2195, 2198, 2197, 9567, 9572, 9573, 9574, 9575, 9576, 9571, 9570, 9569, 9568, 2196, 2204, 2210, 2211, ...

So, the underlying Rails code will execute only one query per entity.

It is nice, isn't it?

Please use it ;)