Skip to content

Instantly share code, notes, and snippets.

@UsamaAshraf
Last active May 23, 2021 16:42
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save UsamaAshraf/95b0c8d0d64ee193148342a931c0a423 to your computer and use it in GitHub Desktop.
Save UsamaAshraf/95b0c8d0d64ee193148342a931c0a423 to your computer and use it in GitHub Desktop.
N+1 Queries, Batch Loading and Active Model Serializers
# ...
# https://github.com/exAspArk/batch-loader
gem 'batch-loader'
class PostsController < ApplicationController
def index
posts = Post.all
render json: posts
end
end
class Post
belongs_to :author, class_name: 'User'
end
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :details
belongs_to :author
end
class Post < ApplicationRecord
# ...
def get_author_lazily
BatchLoader.for(self).batch do |posts, batch_loader|
User.where(:_id.in => posts.pluck(:author_id)).each do |user|
# Modify the user through a given block, say, for serialization.
modified_user = block_given? ? yield(user) : user
batch_loader.call(posts.detect { |p| p.author_id == user._id.to_s }, modified_user)
end
end
end
# ...
end
class PostsController < ApplicationController
def index
# Can't do Post.includes(:author) beacuse the author (User object)
# is stored in an entirely different database: a MongoDB instance.
posts = Post.all
render json: posts
end
end
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :details, :author
def author
object.get_author_lazily do |author|
# Serialize the author after it has been loaded.
ActiveModelSerializers::SerializableResource.new(author).as_json[:user]
end
end
# ...
end
class User
include Mongoid::Document
include Mongoid::Timestamps
# ...
end
class UserSerializer < ActiveModel::Serializer
# ....
end
@luccasmaso
Copy link

Nice implementation! Also, good post around the subject. The application of this with AMS is really useful, since most graphql new approaches are heavy influenced by the data loader technic.

I was playing a little with your code and it works, except for a drawback with AMS. This batch behaviour to postpone the attribute will not take advantage of include directive for choosing the JSON depth/attributes and response performance.

For exemple: If the post User has a list of User as friends and I don't want to load them, I would specify:

render json: posts, include: :user

And if I want it: render json: posts, include: [user: :friends]

But with batch loader its not possible maybe because ActiveModelSerializers::SerializableResource.new(author).as_json[:user] will be constructed afterwards to apply the include? I'm thinking here what could be a solution but can't figured out if is a limitation of AMS or the BathLoader.

@UsamaAshraf
Copy link
Author

UsamaAshraf commented Apr 19, 2018

@luccasmaso yes, include works only for ORM-defined database relations, not custom attributes, which is what user has become in our case.
We can pass custom parameters and access them with @instance_options to achieve our goal:

# PostSerializer.rb
attribute :user, if: -> { @instance_options.key?(:with_user) && @instance_options[:with_user] }
# ...
render json: posts, with_user: true

You can also stick with include, use @instance_options[:include] and check if user was specified. But I'd probably not do this because it sort of goes against what the include option is supposed to be for. Also, in a way our point was to avoid include since it forced the n+1 queries to run. Having said that, there's nothing essentially wrong with using @instance_options[:include].

As far as nested associations are concerned, you can pass them to the explicit call to AMS;

ActiveModelSerializers::SerializableResource.new(user, include: [some: :nested_stuff]).as_json[:user]

Sorry for replying late. Don't know why I didn't get an email!

@Bajena
Copy link

Bajena commented Jan 5, 2019

Hey, I digged in this topic a bit and created a plugin for ActiveModelSerializers - https://github.com/Bajena/ams_lazy_relationships

It eliminates the problem that @lucasmaso mentioned in his comment :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment