computed_model 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +18 -0
- data/.github/workflows/test.yml +24 -0
- data/.yardopts +6 -0
- data/CHANGELOG.md +126 -0
- data/CONCEPTS.ja.md +324 -0
- data/CONCEPTS.md +330 -0
- data/Migration-0.3.md +343 -0
- data/README.ja.md +168 -0
- data/README.md +112 -70
- data/Rakefile +14 -0
- data/computed_model.gemspec +10 -2
- data/lib/computed_model.rb +78 -153
- data/lib/computed_model/dep_graph.rb +245 -0
- data/lib/computed_model/model.rb +447 -0
- data/lib/computed_model/plan.rb +48 -0
- data/lib/computed_model/version.rb +1 -1
- metadata +102 -7
- data/.travis.yml +0 -6
data/CONCEPTS.md
ADDED
@@ -0,0 +1,330 @@
|
|
1
|
+
# Basic concepts and features
|
2
|
+
|
3
|
+
[日本語版](CONCEPTS.ja.md)
|
4
|
+
|
5
|
+
## Wrapping classes
|
6
|
+
|
7
|
+
We don't (yet) support directly including `ComputedModel::Model` into ActiveRecord classes or similar ones.
|
8
|
+
In that case, we recommend creating a wrapper class and reference the original class via the primary loader
|
9
|
+
(described later).
|
10
|
+
|
11
|
+
## Fields
|
12
|
+
|
13
|
+
**Field** are certain attributes managed by ComputedModel. It's a unit of dependency resolution and
|
14
|
+
there are three kinds of fields:
|
15
|
+
|
16
|
+
- computed fields
|
17
|
+
- loaded fields
|
18
|
+
- primary fields
|
19
|
+
|
20
|
+
### computed fields
|
21
|
+
|
22
|
+
A computed field is a field in which it's value is derived from other fields.
|
23
|
+
It's calculated independently per record.
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
class User
|
27
|
+
dependency :preference, :profile
|
28
|
+
computed def display_name
|
29
|
+
"#{preference.title} #{profile.name}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
```
|
33
|
+
|
34
|
+
### loaded fields
|
35
|
+
|
36
|
+
A loaded field is a field in which we obtain values in batches.
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
class User
|
40
|
+
define_loader :preference, key: -> { id } do |ids, _subfields, **|
|
41
|
+
Preference.where(user_id: ids).index_by(&:user_id)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
```
|
45
|
+
|
46
|
+
### primary fields
|
47
|
+
|
48
|
+
A primary field is responsible in searching/enumerating the whole records,
|
49
|
+
in addition to the usual responsibility of loaded fields.
|
50
|
+
|
51
|
+
Consider a hypothetical `User` class for example. In this case you might want to inquiry somewhere (a data source)
|
52
|
+
whether a user with a certain id exists.
|
53
|
+
|
54
|
+
If it were a hypothetical ActiveRecord class `RawUser`, the primary field would be defined as follows:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
class User
|
58
|
+
def initialize(raw_user)
|
59
|
+
@raw_user = raw_user
|
60
|
+
end
|
61
|
+
|
62
|
+
define_primary_loader :raw_user do |_subfields, ids:, **|
|
63
|
+
# You need to set @raw_user in User#initialize.
|
64
|
+
RawUser.where(id: ids).map { |u| User.new(u) }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
## When computation is done
|
70
|
+
|
71
|
+
All necessary fields are computed eagerly when ComputedModel's `bulk_load_and_compute` is called.
|
72
|
+
|
73
|
+
It doesn't (yet) provide lazy loading functionality.
|
74
|
+
|
75
|
+
## Dependency
|
76
|
+
|
77
|
+
You can declare dependencies on a field.
|
78
|
+
As an exception, the primary field cannot have a dependency (but it can have dependents, of course).
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
class User
|
82
|
+
dependency :preference, :profile
|
83
|
+
computed def display_name
|
84
|
+
"#{preference.title} #{profile.name}"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
```
|
88
|
+
|
89
|
+
In `computed def` or `define_loader`, among all fields, you can only read values of explicitly declared dependencies.
|
90
|
+
You cannot read other fields even if it happens to be present (such as indirect dependencies).
|
91
|
+
|
92
|
+
## `bulk_load_and_compute`
|
93
|
+
|
94
|
+
`bulk_load_and_compute` is the very method you need to load ComputedModel records.
|
95
|
+
We recommend wrapping the method in each model class.
|
96
|
+
This is mostly because there is a lot of freedom in the format of the batch-loading parameters (described later)
|
97
|
+
and it will likely cause mistakes if used directly.
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
class User
|
101
|
+
# You can specify an array of fields like [:display_name, :title] in the `with` parameter.
|
102
|
+
def self.list(ids, with:)
|
103
|
+
bulk_load_and_compute(with, ids: ids)
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.get(id, with:)
|
107
|
+
list([id], with: with).first
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.get!(id, with:)
|
111
|
+
get(id, with: with) || (raise User::NotFound)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
116
|
+
There is no such method as load a single record. You can easily implement it by wrapping `bulk_load_and_compute`.
|
117
|
+
If you want a certain optimization for single-record cases, you may want to write conditionals in `define_loader` or `define_primary_loader`.
|
118
|
+
|
119
|
+
## Subfield selectors
|
120
|
+
|
121
|
+
Subfield selectors (or subdependencies) are additional information attached to a dependency.
|
122
|
+
|
123
|
+
Implementation-wise they're just arbitrary messages sent from a field to its dependency.
|
124
|
+
Nonetheless we expect them to be used to request "subfields" as the name suggests.
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
class User
|
128
|
+
define_loader :profile, key: -> { id } do |ids, subfields, **|
|
129
|
+
Profile.preload(subfields).where(user_id: ids).index_by(&:user_id)
|
130
|
+
end
|
131
|
+
|
132
|
+
# [:contact_phones] will be passed to the loader of `profile`.
|
133
|
+
dependency profile: :contact_phones
|
134
|
+
computed def contact_phones
|
135
|
+
profile.contact_phones
|
136
|
+
end
|
137
|
+
end
|
138
|
+
```
|
139
|
+
|
140
|
+
You can also receive subfield selectors in a computed field. See the "Dynamic dependencies" section later.
|
141
|
+
|
142
|
+
## Batch-loading parameters
|
143
|
+
|
144
|
+
The keyword parameters given to `bulk_load_and_compute` is passed through to the blocks of `define_primary_loader` or `define_loader`.
|
145
|
+
You can use it for various purposes, some of which we present below:
|
146
|
+
|
147
|
+
### Searching records by conditions other than id
|
148
|
+
|
149
|
+
You can pass multiple different search conditions through the keyword parameters.
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
class User
|
153
|
+
def self.list(ids, with:)
|
154
|
+
bulk_load_and_compute(with, ids: ids, emails: nil)
|
155
|
+
end
|
156
|
+
|
157
|
+
def self.list_by_emails(emails, with:)
|
158
|
+
bulk_load_and_compute(with, ids: nil, emails: emails)
|
159
|
+
end
|
160
|
+
|
161
|
+
define_primary_loader :raw_user do |_subfields, ids:, emails:, **|
|
162
|
+
s = User.all
|
163
|
+
s = s.where(id: ids) if ids
|
164
|
+
s = s.where(email: emails) if emails
|
165
|
+
s.map { |u| User.new(u) }
|
166
|
+
end
|
167
|
+
end
|
168
|
+
```
|
169
|
+
|
170
|
+
### Current user
|
171
|
+
|
172
|
+
Consider a situation where we want to present different information depending on whom the user is logging in as.
|
173
|
+
You can implement it by including the current user in the keyword parameters.
|
174
|
+
|
175
|
+
```ruby
|
176
|
+
class User
|
177
|
+
def initialize(raw_user, current_user_id)
|
178
|
+
@raw_user = raw_user
|
179
|
+
@current_user_id = current_user_id
|
180
|
+
end
|
181
|
+
|
182
|
+
define_primary_loader :raw_user do |_subfields, current_user_id:, ids:, **|
|
183
|
+
# ...
|
184
|
+
end
|
185
|
+
|
186
|
+
define_loader :profile, key: -> { id } do |ids, _subfields, current_user_id:, **|
|
187
|
+
# ...
|
188
|
+
end
|
189
|
+
end
|
190
|
+
```
|
191
|
+
|
192
|
+
## Dynamic dependencies
|
193
|
+
|
194
|
+
You can configure dynamic dependencies by specifying Procs as subfield selectors.
|
195
|
+
|
196
|
+
### Conditional dependencies
|
197
|
+
|
198
|
+
Dependencies which are conditionally enabled based on incoming subfield selectors:
|
199
|
+
|
200
|
+
```ruby
|
201
|
+
|
202
|
+
class User
|
203
|
+
dependency(
|
204
|
+
:blog_articles,
|
205
|
+
# Load image_permissions only when it receives `image` subfield selector.
|
206
|
+
image_permissions: -> (subfields) { subfields.normalized[:image].any? }
|
207
|
+
)
|
208
|
+
computed def filtered_blog_articles
|
209
|
+
if current_subfields.normalized[:image].any?
|
210
|
+
# ...
|
211
|
+
end
|
212
|
+
# ...
|
213
|
+
end
|
214
|
+
end
|
215
|
+
```
|
216
|
+
|
217
|
+
### Subfield selectors passthrough
|
218
|
+
|
219
|
+
Passing through incoming subfield selectors to another field:
|
220
|
+
|
221
|
+
```ruby
|
222
|
+
|
223
|
+
class User
|
224
|
+
dependency(
|
225
|
+
blog_articles: -> (subfields) { subfields }
|
226
|
+
)
|
227
|
+
computed def filtered_blog_articles
|
228
|
+
if current_subfields.normalized[:image].any?
|
229
|
+
# ...
|
230
|
+
end
|
231
|
+
# ...
|
232
|
+
end
|
233
|
+
end
|
234
|
+
```
|
235
|
+
|
236
|
+
### Subfield selectors mapping
|
237
|
+
|
238
|
+
Processing incoming subfield selectors and pass the result as outgoing subfield selectors to another field:
|
239
|
+
|
240
|
+
```ruby
|
241
|
+
class User
|
242
|
+
dependency(
|
243
|
+
# Always load blog_articles, but
|
244
|
+
# if the incoming subfield selectors contain `blog_articles`, pass them down to the dependency.
|
245
|
+
blog_articles: [true, -> (subfields) { subfields.normalized[:blog_articles] }],
|
246
|
+
# Always load wiki_articles, but
|
247
|
+
# if the incoming subfield selectors contain `wiki_articles`, pass them down to the dependency.
|
248
|
+
wiki_articles: [true, -> (subfields) { subfields.normalized[:wiki_articles] }]
|
249
|
+
)
|
250
|
+
computed def articles
|
251
|
+
(blog_articles + wiki_articles).sort_by { |article| article.created_at }.reverse
|
252
|
+
end
|
253
|
+
end
|
254
|
+
```
|
255
|
+
|
256
|
+
### Detailed dependency format
|
257
|
+
|
258
|
+
You can pass 0 or more arguments to `dependency`.
|
259
|
+
They're pushed into an internal array and will be consumed by the next `computed def` or `define_loader`.
|
260
|
+
So they have the same meaning:
|
261
|
+
|
262
|
+
```ruby
|
263
|
+
dependency :profile
|
264
|
+
dependency :preference
|
265
|
+
computed def display_name; ...; end
|
266
|
+
```
|
267
|
+
|
268
|
+
```ruby
|
269
|
+
dependency :profile, :preference
|
270
|
+
computed def display_name; ...; end
|
271
|
+
```
|
272
|
+
|
273
|
+
The resulting array will be normalized as a hash by `ComputedModel.normalize_dependencies`. The rules are:
|
274
|
+
|
275
|
+
- If it's a Symbol, convert it to a singleton hash containing the key. (`:foo` → `{ foo: [true] }`)
|
276
|
+
- If it's a Hash, convert the values as follows:
|
277
|
+
- If the value is an empty array, replace it with `[true]`. (`{ foo: [] }` → `{ foo: [true] }`)
|
278
|
+
- If the value is not an array, convert it to the singleton array. (`{ foo: :bar }` → `{ foo: [:bar] }`)
|
279
|
+
- If the value is a non-empty array, keep it as-is.
|
280
|
+
- If it's an Array, convert each element following the rules above and merge the keys of the hashes. Hash values are always arrays and will be simply concatenated.
|
281
|
+
- `[:foo, :bar]` → `{ foo: [true], bar: [true] }`
|
282
|
+
- `[{ foo: :foo }, { foo: :bar }]` → `{ foo: [:foo, :bar] }`
|
283
|
+
|
284
|
+
We interpret the resulting hash as a dictionary from a field name (dependency names) and it's subfield selectors.
|
285
|
+
|
286
|
+
Each subfield selector is interpreted as below:
|
287
|
+
|
288
|
+
- If it contains `#call`able objects (such as Proc), call them with `subfields` (incoming subfield selectors) as their argument.
|
289
|
+
- Expand the result if it's an Array. (`{ foo: [-> { [:bar, :baz] }] }` → `{ foo: [:bar, :baz] }`)
|
290
|
+
- Otherwise push the result. (`{ foo: [-> { :bar }] }` → `{ foo: [:bar] }`)
|
291
|
+
- After Proc substitution, check if it contains any truthy value (value other than `nil` or `false`).
|
292
|
+
- If no truthy value is found, we don't use the dependency as the condition is not met.
|
293
|
+
- Otherwise (if truthy value is found), use the dependency. Subfield selectors (after substitution) are sent to the dependency as-is.
|
294
|
+
|
295
|
+
For that reason, in most cases subfield selectors contain `true`. As a special case we remove them in the following cases:
|
296
|
+
|
297
|
+
- We'll remove `nil`, `false`, `true` from the subfield selectors before passed to a `define_loader` or `define_primary_loader` block.
|
298
|
+
- In certain cases you can use `subfields.normalize` to get a hash from the subfield selectors array. This is basically `ComputedModel.normalize_dependencies` but `nil`, `false`, `true` will be removed as part of preprocessing.
|
299
|
+
|
300
|
+
## Inheritance
|
301
|
+
|
302
|
+
You can also define partial ComputedModel class/module. You can then inherit/include it in a different class and complete the definition.
|
303
|
+
|
304
|
+
```ruby
|
305
|
+
module UserLikeConcern
|
306
|
+
extends ActiveSupport::Concern
|
307
|
+
include ComputedModel::Model
|
308
|
+
|
309
|
+
dependency :preference, :profile
|
310
|
+
computed def display_name
|
311
|
+
"#{preference.title} #{profile.name}"
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
class User
|
316
|
+
include UserLikeConcern
|
317
|
+
|
318
|
+
define_loader :preference, key: -> { id } do ... end
|
319
|
+
define_loader :profile, key: -> { id } do ... end
|
320
|
+
end
|
321
|
+
|
322
|
+
class Admin
|
323
|
+
include UserLikeConcern
|
324
|
+
|
325
|
+
define_loader :preference, key: -> { id } do ... end
|
326
|
+
define_loader :profile, key: -> { id } do ... end
|
327
|
+
end
|
328
|
+
```
|
329
|
+
|
330
|
+
Note that in certain cases overriding might work incorrectly (because `computed def` internally renames the given method)
|
data/Migration-0.3.md
ADDED
@@ -0,0 +1,343 @@
|
|
1
|
+
# v0.3.0 migration guide
|
2
|
+
|
3
|
+
computed_model 0.3.0 comes with a number of breaking changes.
|
4
|
+
This guide will help you upgrade the library,
|
5
|
+
but please test your program before deploying to production.
|
6
|
+
|
7
|
+
## Major breaking: `ComputedModel` is now `ComputedModel::Model`
|
8
|
+
|
9
|
+
https://github.com/wantedly/computed_model/pull/17
|
10
|
+
|
11
|
+
Before:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
class User
|
15
|
+
include ComputedModel
|
16
|
+
end
|
17
|
+
```
|
18
|
+
|
19
|
+
After:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
class User
|
23
|
+
include ComputedModel::Model
|
24
|
+
end
|
25
|
+
```
|
26
|
+
|
27
|
+
|
28
|
+
## Major breaking: Indirect dependencies are now rejected
|
29
|
+
|
30
|
+
computed_model 0.3 checks if the requested field is a direct dependency.
|
31
|
+
If not, it raises `ComputedModel::ForbiddenDependency`.
|
32
|
+
|
33
|
+
https://github.com/wantedly/computed_model/pull/23
|
34
|
+
|
35
|
+
### Case 1
|
36
|
+
|
37
|
+
This will mostly affect the following "indirect dependency" case:
|
38
|
+
|
39
|
+
Before:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
class User
|
43
|
+
dependency :bar
|
44
|
+
computed def foo
|
45
|
+
baz # Accepted in computed_model 0.2
|
46
|
+
# ...
|
47
|
+
end
|
48
|
+
|
49
|
+
dependency :baz
|
50
|
+
computed def bar
|
51
|
+
# ...
|
52
|
+
end
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
After:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
class User
|
60
|
+
dependency :bar, :baz # Specify dependencies correctly
|
61
|
+
computed def foo
|
62
|
+
baz
|
63
|
+
# ...
|
64
|
+
end
|
65
|
+
|
66
|
+
dependency :baz
|
67
|
+
computed def bar
|
68
|
+
# ...
|
69
|
+
end
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
### Case 2
|
74
|
+
|
75
|
+
Before:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
class User
|
79
|
+
dependency :bar
|
80
|
+
computed def foo
|
81
|
+
# ...
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
users = User.bulk_load_and_compute([:foo], ...)
|
86
|
+
users[0].bar # Accepted in computed_model 0.2
|
87
|
+
```
|
88
|
+
|
89
|
+
After:
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
class User
|
93
|
+
dependency :bar
|
94
|
+
computed def foo
|
95
|
+
# ...
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
users = User.bulk_load_and_compute([:foo, :bar], ...) # Specify dependencies correctly
|
100
|
+
users[0].bar
|
101
|
+
```
|
102
|
+
|
103
|
+
### Other cases
|
104
|
+
|
105
|
+
Previously, it sometimes happens to work depending on the order in which fields are loaded.
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
class User
|
109
|
+
# No dependency between foo and bar
|
110
|
+
|
111
|
+
dependency :raw_user
|
112
|
+
computed def foo
|
113
|
+
# ...
|
114
|
+
end
|
115
|
+
|
116
|
+
dependency :raw_user
|
117
|
+
computed def bar
|
118
|
+
foo
|
119
|
+
# ...
|
120
|
+
end
|
121
|
+
end
|
122
|
+
```
|
123
|
+
|
124
|
+
It was already fragile in computed_model 0.2.
|
125
|
+
However, in computed_model 0.3,
|
126
|
+
it always leads to `ComputedModel::ForbiddenDependency`.
|
127
|
+
|
128
|
+
|
129
|
+
## Major breaking: `subdeps` are now called `subfields`
|
130
|
+
|
131
|
+
https://github.com/wantedly/computed_model/pull/31
|
132
|
+
|
133
|
+
Before:
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
class User
|
137
|
+
delegate_dependency :name, to: :raw_user, include_subdeps: true
|
138
|
+
end
|
139
|
+
```
|
140
|
+
|
141
|
+
After:
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
class User
|
145
|
+
delegate_dependency :name, to: :raw_user, include_subfields: true
|
146
|
+
end
|
147
|
+
```
|
148
|
+
|
149
|
+
We also recommend renaming block parameters named `subdeps` as `subfields`,
|
150
|
+
although not strictly necessary.
|
151
|
+
|
152
|
+
|
153
|
+
## Minor breaking: `computed_model_error` has been removed
|
154
|
+
|
155
|
+
It was useful in computed_model 0.1 but no longer needed in computed_model 0.2.
|
156
|
+
|
157
|
+
https://github.com/wantedly/computed_model/pull/18
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
# No longer possible
|
161
|
+
self.computed_model_error = User::NotFound.new
|
162
|
+
```
|
163
|
+
|
164
|
+
## Minor breaking: Behavior of `dependency` not directly followed by `computed def` has been changed.
|
165
|
+
|
166
|
+
It doesn't effect you if all `dependency` declarations are followed by `computed def`.
|
167
|
+
|
168
|
+
```ruby
|
169
|
+
# Keeps working
|
170
|
+
dependency :foo
|
171
|
+
computed def bar
|
172
|
+
end
|
173
|
+
```
|
174
|
+
|
175
|
+
Otherwise `dependency` might be consumed by the next `define_loader` or `define_primary_loader`.
|
176
|
+
|
177
|
+
https://github.com/wantedly/computed_model/pull/20
|
178
|
+
|
179
|
+
Before:
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
dependency :foo # dependency of bar in computed_model 0.2
|
183
|
+
|
184
|
+
define_loader :quux, key: -> { id } do
|
185
|
+
# ...
|
186
|
+
end
|
187
|
+
|
188
|
+
computed def bar
|
189
|
+
# ...
|
190
|
+
end
|
191
|
+
```
|
192
|
+
|
193
|
+
After:
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
# This would be interpreted as a dependency of quux
|
197
|
+
# dependency :foo
|
198
|
+
|
199
|
+
define_loader :quux, key: -> { id } do
|
200
|
+
# ...
|
201
|
+
end
|
202
|
+
|
203
|
+
dependency :foo # Place it here
|
204
|
+
computed def bar
|
205
|
+
# ...
|
206
|
+
end
|
207
|
+
```
|
208
|
+
|
209
|
+
Additionally, `dependency` before `define_primary_loader` will be an error.
|
210
|
+
|
211
|
+
## Minor breaking: Cyclic dependency is an error even if it is unused
|
212
|
+
|
213
|
+
https://github.com/wantedly/computed_model/pull/24
|
214
|
+
|
215
|
+
Before:
|
216
|
+
|
217
|
+
```ruby
|
218
|
+
class User
|
219
|
+
|
220
|
+
# Cyclic dependency is allowed as long as it's unused
|
221
|
+
|
222
|
+
dependency :bar
|
223
|
+
computed def foo
|
224
|
+
end
|
225
|
+
|
226
|
+
dependency :foo
|
227
|
+
computed def bar
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
users = User.bulk_load_and_compute([], ...) # Neither :foo nor :bar is used
|
232
|
+
```
|
233
|
+
|
234
|
+
After:
|
235
|
+
|
236
|
+
```ruby
|
237
|
+
class User
|
238
|
+
# Remove cyclic dependency altogether
|
239
|
+
end
|
240
|
+
|
241
|
+
users = User.bulk_load_and_compute([], ...) # Neither :foo nor :bar is used
|
242
|
+
```
|
243
|
+
|
244
|
+
## Minor breaking: `nil`, `true`, `false` and `Proc` in subdeps are treated differently
|
245
|
+
|
246
|
+
They now have special meaning, so you should avoid using them as a normal subdependency.
|
247
|
+
|
248
|
+
https://github.com/wantedly/computed_model/pull/25
|
249
|
+
|
250
|
+
### `nil` and `false`
|
251
|
+
|
252
|
+
They are constantly false condition in conditional dependency. Unless otherwise enabled, the dependency won't be used.
|
253
|
+
|
254
|
+
Before:
|
255
|
+
|
256
|
+
```ruby
|
257
|
+
dependency foo: [nil, false] # foo will be used
|
258
|
+
computed def bar
|
259
|
+
end
|
260
|
+
```
|
261
|
+
|
262
|
+
After:
|
263
|
+
|
264
|
+
```ruby
|
265
|
+
# dependency foo: [nil, false] # foo won't be used
|
266
|
+
dependency foo: [:nil, :false] # Use other values instead
|
267
|
+
computed def bar
|
268
|
+
end
|
269
|
+
```
|
270
|
+
|
271
|
+
### `true`
|
272
|
+
|
273
|
+
They are constantly true condition in conditional dependency. It's filtered out before passed to a loader or a primary loader.
|
274
|
+
|
275
|
+
Before:
|
276
|
+
|
277
|
+
```ruby
|
278
|
+
dependency foo: [true] # true is given to "define_loader :foo do ... end"
|
279
|
+
computed def bar
|
280
|
+
end
|
281
|
+
```
|
282
|
+
|
283
|
+
After:
|
284
|
+
|
285
|
+
```ruby
|
286
|
+
# dependency foo: [true] # true is ignored
|
287
|
+
dependency foo: [:true] # Use other values instead
|
288
|
+
computed def bar
|
289
|
+
end
|
290
|
+
```
|
291
|
+
|
292
|
+
### Proc
|
293
|
+
|
294
|
+
Callable objects (objects implementing `#call`), including instances of `Proc`, is interpreted as a dynamic dependency.
|
295
|
+
|
296
|
+
Before:
|
297
|
+
|
298
|
+
```ruby
|
299
|
+
dependency foo: -> { raise "foo" } # Passed to foo as-is
|
300
|
+
computed def bar
|
301
|
+
end
|
302
|
+
```
|
303
|
+
|
304
|
+
After:
|
305
|
+
|
306
|
+
```ruby
|
307
|
+
# dependency foo: -> { raise "foo" } # Dynamic dependency. Called during dependency resolution.
|
308
|
+
dependency foo: { callback: -> { raise "foo" } } # Wrap it in something
|
309
|
+
computed def bar
|
310
|
+
end
|
311
|
+
```
|
312
|
+
|
313
|
+
|
314
|
+
## Behavioral change: The order in which fields are loaded is changed
|
315
|
+
|
316
|
+
https://github.com/wantedly/computed_model/pull/24
|
317
|
+
|
318
|
+
Independent fields may be loaded in an arbitrary order. But implementation-wise, this is to some degree predictable.
|
319
|
+
|
320
|
+
computed_model 0.3 uses different dependency resolution algorithm and may produce different orders.
|
321
|
+
As a result, if your model is accidentally order-dependent, it may break with computed_model 0.3.
|
322
|
+
|
323
|
+
|
324
|
+
## Behavioral change: `ComputedModel::Model` now uses `ActiveSupport::Concern`
|
325
|
+
|
326
|
+
It won't affect you if you simply did `include ComputedModel::Model` (previously `include ComputedModel`) and nothing more.
|
327
|
+
Be cautious if you have a more complex inheritance/inclusion graph than that.
|
328
|
+
|
329
|
+
|
330
|
+
## Recommendation: `#verify_dependencies`
|
331
|
+
|
332
|
+
`#verify_dependencies` allows you to check the graph in the initialization process, rather than just before loading records.
|
333
|
+
We recommend doing this at the end of the class definition.
|
334
|
+
|
335
|
+
```ruby
|
336
|
+
class User
|
337
|
+
define_loader ...
|
338
|
+
|
339
|
+
computed def ... end
|
340
|
+
|
341
|
+
verify_dependencies # HERE
|
342
|
+
end
|
343
|
+
```
|