Navigation Menu

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
@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