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.
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.
classUser < ApplicationRecordhas_one:preferencehas_one:profiledefdisplay_name"#{preference.title}#{profile.name}"endendHowever, 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_nameThis partially defeats the purpose of #display_name's abstraction.
Computed solves the problem by connection the dependency-resolution to the batch loader.
classUserdefine_primary_loader:raw_userdo ... enddefine_loader:preferencedo ... enddefine_loader:profiledo ... enddependency:preference,:profilecomputeddefdisplay_name"#{preference.title}#{profile.name}"endendAdd 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
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)classUserincludeComputedModel::Modelattr_reader:iddefinitialize(raw_user)@id=raw_user.id@raw_user=raw_userenddefself.list(ids,with:)bulk_load_and_compute(Array(with),ids: ids)enddefine_primary_loader:raw_userdo |_subfields,ids:, **|
# In ActiveRecord:# raw_users = RawUser.where(id: ids).to_araw_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)}enddefine_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)}enddelegate_dependency:name,to: :raw_userdelegate_dependency:title,to: :raw_userdelegate_dependency:name_public,to: :preferencedependency:name,:name_publiccomputeddefpublic_namename_public ? name : "Anonymous"enddependency:public_name,:titlecomputeddefpublic_name_with_title"#{title}#{public_name}"endend# You can only access the field you requested ahead of timeusers=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. "]This library is distributed under MIT license.
Copyright (c) 2020 Masaki Hara
Copyright (c) 2020 Masayuki Izumi
Copyright (c) 2020 Wantedly, Inc.
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.
Bug reports and pull requests are welcome on GitHub at https://github.com/wantedly/computed_model.