Batch Loading

A batch loader loads data for all serialized objects at once, so serializing 100 users runs one query, not 100.

class UserSerializer < Serega
  batch :followers_count, ->(users) { Follow.where(user: users).group(:user_id).count }

  attribute :name
  attribute :followers_count, batch: true
end

UserSerializer.to_h(users)
# SELECT COUNT(*), user_id FROM follows WHERE user_id IN (1, 2, ...) GROUP BY user_id
# => [{name: "Bruce", followers_count: 7}, {name: "Alfred", followers_count: 3}]

A loader receives the objects and returns a Hash of values by object id. The attribute value is batches[:followers_count][user.id], nil for a missing id.

A loader runs once per serialization for all objects of a relation: for users(posts(comments_count)), the comments_count loader runs once for the posts of all users. A loader of an attribute that is not serialized does not run.

Defining loaders

batch defines a named loader. Child serializers inherit it.

class UserSerializer < Serega
  batch :likes_count, ->(users) { Like.where(user: users).group(:user_id).count }
  batch :views_count, ViewsCountLoader                          # responds to #call
  batch(:badges) { |users| Badge.for_users(users) }               # block
  batch :rank, ->(users, ctx) { Rank.for(users, ctx[:season]) }   # with context
  batch :rank, ->(users, ctx:) { Rank.for(users, ctx[:season]) }  # :ctx keyword
end

Using loaders

class UserSerializer < Serega
  # The loader named after the attribute. It must be defined before the attribute.
  attribute :likes_count, batch: true

  # A loader with another name
  attribute :likes, batch: :likes_count

  # A loader that belongs to this attribute
  attribute :followers_count, batch: ->(users) { Follow.where(user: users).group(:user_id).count }
end

batch: true and batch: :name raise Serega::SeregaError when no loader with that name is defined.

Building the value

:value builds the value from the loaded batches, for example to set a default or combine several loaders. A loader given directly to the attribute is found under the attribute name.

attribute :followers_count, batch: true,
  value: proc { |user, batches:| batches[:followers_count].fetch(user.id, 0) }

attribute :likes_count, batch: {use: [:facebook_likes, :x_likes]},
  value: proc { |user, batches:| batches[:facebook_likes][user.id] + batches[:x_likes][user.id] }

Several loaders in :use require :value. For a simple fallback, default: also works: batch: true, default: 0.

Object ids

Values are looked up by object.id. The :id option or config.batch_id_option choose another method:

attribute :likes_count, batch: {use: :likes_count, id: :uuid}

class AppSerializer < Serega
  config.batch_id_option = :uuid
end

Relations

A loader can return records for a relation serializer:

class UserSerializer < Serega
  batch :posts, ->(users) { Post.where(user: users).group_by(&:user_id) }

  attribute :posts, batch: true, serializer: PostSerializer
end

For ActiveRecord associations, preloads are usually simpler.