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.
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 ;)