Skip to content

wantedly/computed_model

Repository files navigation

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.

日本語版README

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.

About

Batch loader with dependency resolution and computed fields

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •