ComputedModel
ComputedModel is a universal batch loader which comes with a dependency-resolution algorithm.
- Thanks to the dependency resolution, it allows you to the following trifecta at once, without breaking abstraction.
- Process information gathered from datasources (such as ActiveRecord) and return the derived one.
- Prevent N+1 problem via batch loading.
- Load only necessary data.
- Can load data from multiple datasources.
- Designed to be universal and datasource-independent. For example, you can gather data from both HTTP and ActiveRecord and return the derived one.
Problems to solve
As models grow, they cannot simply return the database columns as-is. Instead, we want to process information obtained from the database and return the derived value.
class User < ApplicationRecord
has_one :preference
has_one :profile
def display_name
"#{preference.title} #{profile.name}"
end
end
However, it can lead to N+1 without care.
# N+1 problem!
User.where(id: friend_ids).map(&:display_name)
To solve the N+1 problem, we need to enumerate dependencies of #display_name
and preload them.
User.where(id: friend_ids).preload(:preference, :profile).map(&:display_name)
# ^^^^^^^^^^^^^^^^^^^^^ breaks abstraction of display_name
This partially defeats the purpose of #display_name
's abstraction.
Computed solves the problem by connection the dependency-resolution to the batch loader.
class User
define_primary_loader :raw_user do ... end
define_loader :preference do ... end
define_loader :profile do ... end
dependency :preference, :profile
computed def display_name
"#{preference.title} #{profile.name}"
end
end
Installation
Add this line to your application's Gemfile:
gem 'computed_model', '~> 0.3.0'
And then execute:
$ bundle
Or install it yourself as:
$ gem install computed_model
Working example
require 'computed_model'
# Consider them external sources (ActiveRecord or resources obtained via HTTP)
RawUser = Struct.new(:id, :name, :title)
Preference = Struct.new(:user_id, :name_public)
class User
include ComputedModel::Model
attr_reader :id
def initialize(raw_user)
@id = raw_user.id
@raw_user = raw_user
end
def self.list(ids, with:)
bulk_load_and_compute(Array(with), ids: ids)
end
define_primary_loader :raw_user do |_subfields, ids:, **|
# In ActiveRecord:
# raw_users = RawUser.where(id: ids).to_a
raw_users = [
RawUser.new(1, "Tanaka Taro", "Mr. "),
RawUser.new(2, "Yamada Hanako", "Dr. "),
].filter { |u| ids.include?(u.id) }
raw_users.map { |u| User.new(u) }
end
define_loader :preference, key: -> { id } do |user_ids, _subfields, **|
# In ActiveRecord:
# Preference.where(user_id: user_ids).index_by(&:user_id)
{
1 => Preference.new(1, true),
2 => Preference.new(2, false),
}.filter { |k, _v| user_ids.include?(k) }
end
delegate_dependency :name, to: :raw_user
delegate_dependency :title, to: :raw_user
delegate_dependency :name_public, to: :preference
dependency :name, :name_public
computed def public_name
name_public ? name : "Anonymous"
end
dependency :public_name, :title
computed def public_name_with_title
"#{title}#{public_name}"
end
end
# You can only access the field you requested ahead of time
users = User.list([1, 2], with: [:public_name_with_title])
users.map(&:public_name_with_title) # => ["Mr. Tanaka Taro", "Dr. Anonymous"]
users.map(&:public_name) # => error (ForbiddenDependency)
users = User.list([1, 2], with: [:public_name_with_title, :public_name])
users.map(&:public_name_with_title) # => ["Mr. Tanaka Taro", "Dr. Anonymous"]
users.map(&:public_name) # => ["Tanaka Taro", "Anonymous"]
# In this case, preference will not be loaded.
users = User.list([1, 2], with: [:title])
users.map(&:title) # => ["Mr. ", "Dr. "]
Next read
License
This library is distributed under MIT license.
Copyright (c) 2020 Masaki Hara
Copyright (c) 2020 Masayuki Izumi
Copyright (c) 2020 Wantedly, Inc.
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/wantedly/computed_model.