computed_model 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +3 -0
- data/README.md +40 -15
- data/computed_model.gemspec +2 -2
- data/lib/computed_model.rb +80 -11
- data/lib/computed_model/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 827856e649d8af3b892979456fb1acbcfc9ab746934713422903e68342872371
|
4
|
+
data.tar.gz: 68b14dcca2f7e31880ac54b26e0f3c92bfeebf90ca9387be741f158d7fbeb5ee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7a5ea72e1d06f357866dfde5a453591d43bad5a9ebd178cad88db09e351fc6ae3830d380514ee00ce582f45cb80339980e90c363298b910bfb2d2ccf3765008f
|
7
|
+
data.tar.gz: b2a953f74000b6b40f76ae43920e516c39c24856a2c29d89f63ee8253878c1c0ba847342b7915f134de6df3caa620f520f076951e10fed445726f793098153de
|
data/CHANGELOG.md
CHANGED
@@ -1,4 +1,7 @@
|
|
1
1
|
## Unreleased
|
2
|
+
- **BREAKING CHANGE** Make define_loader more concise interface like GraphQL's DataLoader.
|
3
|
+
- Introduce primary loader through `#define_primary_loader`.
|
4
|
+
- **BREAKING CHANGE** Change `#bulk_load_and_compute` signature to support primary loader.
|
2
5
|
|
3
6
|
## 0.1.1
|
4
7
|
|
data/README.md
CHANGED
@@ -42,25 +42,54 @@ end
|
|
42
42
|
They your model class will be able to define the two kinds of special attributes:
|
43
43
|
|
44
44
|
- **Loaded attributes** for external data. You define batch loading strategies for loaded attributes.
|
45
|
+
- Among them, there is a special **primary model** for listing up the models from certain criteria.
|
45
46
|
- **Computed attributes** for data derived from loaded attributes or other computed attributes.
|
46
47
|
You define a usual `def` with special dependency annotations.
|
47
48
|
|
48
49
|
## Loaded attributes
|
49
50
|
|
50
|
-
Use `ComputedModel::ClassMethods#
|
51
|
+
Use `ComputedModel::ClassMethods#define_primary_loader`
|
52
|
+
or `ComputedModel::ClassMethods#define_loader` to define loaded attributes.
|
51
53
|
|
52
54
|
```ruby
|
55
|
+
# Create a User instance
|
56
|
+
def initialize(raw_user)
|
57
|
+
@id = raw_user.id
|
58
|
+
@raw_user = raw_user
|
59
|
+
end
|
60
|
+
|
53
61
|
# Example: pulling data from ActiveRecord
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
62
|
+
define_primary_loader :raw_user do |subdeps, ids:, **options|
|
63
|
+
RawUser.where(id: ids).preload(subdeps).map { |raw_user| User.new(raw_user) }
|
64
|
+
end
|
65
|
+
|
66
|
+
# Example: pulling auxiliary data from ActiveRecord
|
67
|
+
define_loader :user_aux_data, key: -> { id } do |user_ids, subdeps, **options|
|
68
|
+
UserAuxData.where(user_id: user_ids).preload(subdeps).group_by(&:id)
|
61
69
|
end
|
62
70
|
```
|
63
71
|
|
72
|
+
### `define_primary_loader`
|
73
|
+
|
74
|
+
At most one primary loader can be defined on a model class.
|
75
|
+
|
76
|
+
The primary loader's job is to list up models from user-defined criteria, along with
|
77
|
+
requested data loaded to the primary attribute.
|
78
|
+
|
79
|
+
Search criteria can be passed as a keyword argument to `bulk_load_and_compute`
|
80
|
+
and it will be passed to the loader as-is.
|
81
|
+
|
82
|
+
Most typically you receive `ids`, an array of integers, and use it like
|
83
|
+
`.where(id: ids)`. Instead you may want to accept a non-primary-key criterion
|
84
|
+
such as `group_ids` and `.where(group_id: group_ids)`.
|
85
|
+
|
86
|
+
The loader must return an array of instances of the model being defined.
|
87
|
+
Each instance must have the primary attribute assigned at that time.
|
88
|
+
In the example above, the block for `define_primary_loader :raw_user`
|
89
|
+
must return an array of `User`s, each of which already have `@raw_user`.
|
90
|
+
|
91
|
+
### `define_loader`
|
92
|
+
|
64
93
|
The first argument to the block is an array of the model instances.
|
65
94
|
The loader's job is to assign something to the corresponding field of each instance.
|
66
95
|
|
@@ -94,13 +123,7 @@ Typically you need to create a wrapper for the batch loader like:
|
|
94
123
|
|
95
124
|
```ruby
|
96
125
|
def self.list(ids, with:)
|
97
|
-
|
98
|
-
objs = ids.map { |id| User.new(id) }
|
99
|
-
# Batch-load attributes into the objects.
|
100
|
-
bulk_load_and_compute(objs, Array(with) + [:raw_user])
|
101
|
-
# Reject objects without primary model.
|
102
|
-
objs.reject! { |u| u.raw_user.nil? }
|
103
|
-
objs
|
126
|
+
bulk_load_and_compute(Array(with), ids: ids)
|
104
127
|
end
|
105
128
|
```
|
106
129
|
|
@@ -116,6 +139,8 @@ This library is distributed under MIT license.
|
|
116
139
|
|
117
140
|
Copyright (c) 2020 Masaki Hara
|
118
141
|
|
142
|
+
Copyright (c) 2020 Masayuki Izumi
|
143
|
+
|
119
144
|
Copyright (c) 2020 Wantedly, Inc.
|
120
145
|
|
121
146
|
## Development
|
data/computed_model.gemspec
CHANGED
@@ -7,8 +7,8 @@ require "computed_model/version"
|
|
7
7
|
Gem::Specification.new do |spec|
|
8
8
|
spec.name = "computed_model"
|
9
9
|
spec.version = ComputedModel::VERSION
|
10
|
-
spec.authors = ["Masaki Hara", "Wantedly, Inc."]
|
11
|
-
spec.email = ["ackie.h.gmai@gmail.com", "dev@wantedly.com"]
|
10
|
+
spec.authors = ["Masaki Hara", "Masayuki Izumi", "Wantedly, Inc."]
|
11
|
+
spec.email = ["ackie.h.gmai@gmail.com", "m@izum.in", "dev@wantedly.com"]
|
12
12
|
|
13
13
|
spec.summary = %q{Batch loader with dependency resolution and computed fields}
|
14
14
|
spec.description = <<~DSC
|
data/lib/computed_model.rb
CHANGED
@@ -27,6 +27,11 @@ module ComputedModel
|
|
27
27
|
# A return value from {ComputedModel::ClassMethods#computing_plan}.
|
28
28
|
Plan = Struct.new(:load_order, :subdeps_hash)
|
29
29
|
|
30
|
+
# An object for storing procs for loaded attributes.
|
31
|
+
Loader = Struct.new(:key_proc, :load_proc)
|
32
|
+
|
33
|
+
private_constant :Loader
|
34
|
+
|
30
35
|
# A set of class methods for {ComputedModel}. Automatically included to the
|
31
36
|
# singleton class when you include {ComputedModel}.
|
32
37
|
module ClassMethods
|
@@ -136,7 +141,7 @@ module ComputedModel
|
|
136
141
|
end
|
137
142
|
end
|
138
143
|
|
139
|
-
# Declares a loaded attribute. See {#dependency} too.
|
144
|
+
# Declares a loaded attribute. See {#dependency} and {#define_primary_loader} too.
|
140
145
|
#
|
141
146
|
# `define_loader :foo do ... end` generates a reader `foo` and a writer `foo=`.
|
142
147
|
# The writer is only meant to be used in the loader.
|
@@ -145,9 +150,45 @@ module ComputedModel
|
|
145
150
|
# or set `computed_model_error` otherwise.
|
146
151
|
#
|
147
152
|
# @param meth_name [Symbol] the name of the loaded attribute.
|
153
|
+
# @param key [Proc] The proc to collect keys.
|
154
|
+
# @return [void]
|
155
|
+
# @yield [keys, subdeps, **options]
|
156
|
+
# @yieldparam objects [Array] The ids of the loaded attributes.
|
157
|
+
# @yieldparam subdeps [Hash] sub-dependencies
|
158
|
+
# @yieldparam options [Hash] A verbatim copy of what is passed to {#bulk_load_and_compute}.
|
159
|
+
# @yieldreturn [Hash]
|
160
|
+
#
|
161
|
+
# @example define a loader for ActiveRecord-based models
|
162
|
+
# define_loader :user_aux_data, key: -> { id } do |user_ids, subdeps, **options|
|
163
|
+
# UserAuxData.where(user_id: user_ids).preload(subdeps).group_by(&:id)
|
164
|
+
# end
|
165
|
+
def define_loader(meth_name, key:, &block)
|
166
|
+
raise ArgumentError, "No block given" unless block
|
167
|
+
|
168
|
+
var_name = :"@#{meth_name}"
|
169
|
+
|
170
|
+
@__computed_model_loaders[meth_name] = Loader.new(key, block)
|
171
|
+
|
172
|
+
define_method(meth_name) do
|
173
|
+
raise NotLoaded, "the field #{meth_name} is not loaded" unless instance_variable_defined?(var_name)
|
174
|
+
instance_variable_get(var_name)
|
175
|
+
end
|
176
|
+
attr_writer meth_name
|
177
|
+
end
|
178
|
+
|
179
|
+
# Declares a primary attribute. See {#define_loader} and {#dependency} too.
|
180
|
+
#
|
181
|
+
# `define_primary_loader :foo do ... end` generates a reader `foo` and
|
182
|
+
# a writer `foo=`.
|
183
|
+
# The writer is only meant to be used in the loader.
|
184
|
+
#
|
185
|
+
# The responsibility of the primary loader is to list up all the relevant
|
186
|
+
# primary models, and initialize instances of the subclass of ComputedModel
|
187
|
+
# with `@foo` set to the primary model which is just being found.
|
188
|
+
#
|
189
|
+
# @param meth_name [Symbol] the name of the loaded attribute.
|
148
190
|
# @return [void]
|
149
|
-
# @yield [
|
150
|
-
# @yieldparam objects [Array] The objects to preload the attribute into.
|
191
|
+
# @yield [**options]
|
151
192
|
# @yieldparam options [Hash] A verbatim copy of what is passed to {#bulk_load_and_compute}.
|
152
193
|
# @yieldreturn [void]
|
153
194
|
#
|
@@ -160,12 +201,14 @@ module ComputedModel
|
|
160
201
|
# user.raw_user = raw_users[user.id]
|
161
202
|
# end
|
162
203
|
# end
|
163
|
-
def
|
204
|
+
def define_primary_loader(meth_name, &block)
|
164
205
|
raise ArgumentError, "No block given" unless block
|
206
|
+
raise ArgumentError, "Primary loader has already been defined" if @__computed_model_primary_attribute
|
165
207
|
|
166
208
|
var_name = :"@#{meth_name}"
|
167
209
|
|
168
|
-
@
|
210
|
+
@__computed_model_primary_loader = block
|
211
|
+
@__computed_model_primary_attribute = meth_name
|
169
212
|
|
170
213
|
define_method(meth_name) do
|
171
214
|
raise NotLoaded, "the field #{meth_name} is not loaded" unless instance_variable_defined?(var_name)
|
@@ -176,26 +219,44 @@ module ComputedModel
|
|
176
219
|
|
177
220
|
# The core routine for batch-loading.
|
178
221
|
#
|
179
|
-
# @param objs [Array] The objects to preload attributes into.
|
180
222
|
# @param deps [Array<Symbol, Hash{Symbol=>Array}>] A set of dependencies.
|
181
223
|
# @param options [Hash] An arbitrary hash to pass to loaders
|
182
224
|
# defined by {#define_loader}.
|
183
|
-
# @return [
|
184
|
-
|
185
|
-
|
225
|
+
# @return [Array<Object>] The array of the requested models.
|
226
|
+
# Based on what the primary loader returns.
|
227
|
+
def bulk_load_and_compute(deps, **options)
|
228
|
+
unless @__computed_model_primary_attribute
|
229
|
+
raise ArgumentError, "No primary loader defined"
|
230
|
+
end
|
231
|
+
|
232
|
+
objs = orig_objs = nil
|
186
233
|
plan = computing_plan(deps)
|
187
234
|
plan.load_order.each do |dep_name|
|
188
|
-
if @
|
235
|
+
if @__computed_model_primary_attribute == dep_name
|
236
|
+
orig_objs = @__computed_model_primary_loader.call(plan.subdeps_hash[dep_name], **options)
|
237
|
+
objs = orig_objs.dup
|
238
|
+
elsif @__computed_model_dependencies.key?(dep_name)
|
239
|
+
raise "Bug: objs is nil" if objs.nil?
|
240
|
+
|
189
241
|
objs.each do |obj|
|
190
242
|
obj.send(:"compute_#{dep_name}")
|
191
243
|
end
|
192
244
|
elsif @__computed_model_loaders.key?(dep_name)
|
193
|
-
|
245
|
+
raise "Bug: objs is nil" if objs.nil?
|
246
|
+
|
247
|
+
l = @__computed_model_loaders[dep_name]
|
248
|
+
keys = objs.map { |o| o.instance_exec(&(l.key_proc)) }
|
249
|
+
subobj_by_key = l.load_proc.call(keys, plan.subdeps_hash[dep_name], **options)
|
250
|
+
objs.zip(keys) do |obj, key|
|
251
|
+
obj.send(:"#{dep_name}=", subobj_by_key[key])
|
252
|
+
end
|
194
253
|
else
|
195
254
|
raise "No dependency info for #{self}##{dep_name}"
|
196
255
|
end
|
197
256
|
objs.reject! { |obj| !obj.computed_model_error.nil? }
|
198
257
|
end
|
258
|
+
|
259
|
+
orig_objs
|
199
260
|
end
|
200
261
|
|
201
262
|
# @param deps [Array]
|
@@ -206,6 +267,12 @@ module ComputedModel
|
|
206
267
|
subdeps_hash = {}
|
207
268
|
visiting = Set[]
|
208
269
|
visited = Set[]
|
270
|
+
if @__computed_model_primary_attribute
|
271
|
+
load_order << @__computed_model_primary_attribute
|
272
|
+
visiting.add @__computed_model_primary_attribute
|
273
|
+
visited.add @__computed_model_primary_attribute
|
274
|
+
subdeps_hash[@__computed_model_primary_attribute] ||= []
|
275
|
+
end
|
209
276
|
normalized.each do |dep_name, dep_subdeps|
|
210
277
|
computing_plan_dfs(dep_name, dep_subdeps, load_order, subdeps_hash, visiting, visited)
|
211
278
|
end
|
@@ -271,5 +338,7 @@ module ComputedModel
|
|
271
338
|
klass.extend ClassMethods
|
272
339
|
klass.instance_variable_set(:@__computed_model_dependencies, {})
|
273
340
|
klass.instance_variable_set(:@__computed_model_loaders, {})
|
341
|
+
klass.instance_variable_set(:@__computed_model_primary_loader, nil)
|
342
|
+
klass.instance_variable_set(:@__computed_model_primary_attribute, nil)
|
274
343
|
end
|
275
344
|
end
|
metadata
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: computed_model
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Masaki Hara
|
8
|
+
- Masayuki Izumi
|
8
9
|
- Wantedly, Inc.
|
9
10
|
autorequire:
|
10
11
|
bindir: exe
|
11
12
|
cert_chain: []
|
12
|
-
date: 2020-03-
|
13
|
+
date: 2020-03-26 00:00:00.000000000 Z
|
13
14
|
dependencies:
|
14
15
|
- !ruby/object:Gem::Dependency
|
15
16
|
name: bundler
|
@@ -62,6 +63,7 @@ description: |
|
|
62
63
|
ActiveRecord and remote server (such as ActiveResource).
|
63
64
|
email:
|
64
65
|
- ackie.h.gmai@gmail.com
|
66
|
+
- m@izum.in
|
65
67
|
- dev@wantedly.com
|
66
68
|
executables: []
|
67
69
|
extensions: []
|