n1_loader 0.1.1 → 1.2.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/.circleci/config.yml +56 -14
- data/CHANGELOG.md +12 -2
- data/Gemfile +9 -0
- data/README.md +193 -35
- data/activerecord-gemfiles/ar_5_latest.gemfile +3 -0
- data/{gemfiles → activerecord-gemfiles}/ar_6_latest.gemfile +0 -4
- data/ar_lazy_preload-gemfiles/ar_lazy_preload_0.6.1.gemfile +3 -0
- data/ar_lazy_preload-gemfiles/ar_lazy_preload_master.gemfile +3 -0
- data/lib/n1_loader/active_record/associations_preloader_v5.rb +39 -0
- data/lib/n1_loader/active_record/{associations_preloader.rb → associations_preloader_v6.rb} +0 -0
- data/lib/n1_loader/active_record/loader.rb +5 -0
- data/lib/n1_loader/active_record/loader_collection.rb +9 -0
- data/lib/n1_loader/active_record.rb +20 -1
- data/lib/n1_loader/ar_lazy_preload/associated_context_builder.rb +8 -1
- data/lib/n1_loader/ar_lazy_preload/context_adapter.rb +9 -6
- data/lib/n1_loader/ar_lazy_preload/loadable.rb +2 -2
- data/lib/n1_loader/ar_lazy_preload/loader_collection_patch.rb +18 -0
- data/lib/n1_loader/ar_lazy_preload/loader_patch.rb +20 -0
- data/lib/n1_loader/ar_lazy_preload/preloader_patch.rb +23 -0
- data/lib/n1_loader/ar_lazy_preload.rb +9 -0
- data/lib/n1_loader/{loadable.rb → core/loadable.rb} +24 -17
- data/lib/n1_loader/core/loader.rb +53 -0
- data/lib/n1_loader/core/loader_collection.rb +23 -0
- data/lib/n1_loader/{preloader.rb → core/preloader.rb} +3 -3
- data/lib/n1_loader/version.rb +1 -1
- data/lib/n1_loader.rb +5 -3
- data/n1_loader.gemspec +4 -4
- metadata +30 -20
- data/lib/n1_loader/loader.rb +0 -35
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8b9b61121f94d9dba30e969c1b9a3fa9c812292d618434a98262c90aef43e6cd
|
4
|
+
data.tar.gz: a1ecf5359a1a53200e354b30289566a437cf372c54ccf3f13c2e0adc2cb71640
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5c545cc2033dc8e75edddd5c948eed78fc1d965464522147841d30e3c6a02aebb0b6758d3f801b020c25d62618f7ae4bbb7ff11441748f798546351b1bf34d85
|
7
|
+
data.tar.gz: a60fa813399debf1ae9c8f839272a683c2a7f733ea392b4b1bd4169af55369a1d3be2fd8648a548e50093005e3d2df962437adf08f1f9fdf1338fa4ab6217df7
|
data/.circleci/config.yml
CHANGED
@@ -11,8 +11,8 @@ executors:
|
|
11
11
|
docker:
|
12
12
|
- image: circleci/ruby:<< parameters.tag >>
|
13
13
|
environment:
|
14
|
-
|
15
|
-
|
14
|
+
BUNDLE_JOBS: 4
|
15
|
+
BUNDLE_RETRY: 3
|
16
16
|
working_directory: ~/n1_loader
|
17
17
|
|
18
18
|
jobs:
|
@@ -29,28 +29,49 @@ jobs:
|
|
29
29
|
parameters:
|
30
30
|
ruby-version:
|
31
31
|
type: string
|
32
|
-
gemfile:
|
32
|
+
activerecord-gemfile:
|
33
33
|
type: string
|
34
|
+
ar_lazy_preload-gemfile:
|
35
|
+
type: string
|
36
|
+
environment:
|
37
|
+
ACTIVERECORD_GEMFILE: << parameters.activerecord-gemfile >>
|
38
|
+
AR_LAZY_PRELOAD_GEMFILE: << parameters.ar_lazy_preload-gemfile >>
|
34
39
|
executor:
|
35
40
|
name: ruby
|
36
41
|
tag: << parameters.ruby-version >>
|
37
42
|
steps:
|
38
43
|
- attach_workspace:
|
39
44
|
at: ~/n1_loader
|
40
|
-
- run:
|
41
|
-
name: Use << parameters.gemfile >> as the Gemfile
|
42
|
-
command: bundle config --global gemfile << parameters.gemfile >>
|
43
45
|
- run:
|
44
46
|
name: Install the gems specified by the Gemfile
|
45
47
|
command: bundle install
|
46
48
|
- run:
|
47
|
-
name: Run RSpec
|
49
|
+
name: Run Core RSpec
|
48
50
|
command: |
|
49
51
|
bundle exec rspec --profile 10 \
|
50
52
|
--format RspecJunitFormatter \
|
51
|
-
--out test_results/
|
53
|
+
--out test_results/core.xml \
|
52
54
|
--format progress \
|
53
|
-
|
55
|
+
spec/n1_loader_spec.rb
|
56
|
+
- run:
|
57
|
+
name: Run ActiveRecord integration RSpec
|
58
|
+
command: |
|
59
|
+
bundle exec rspec --profile 10 \
|
60
|
+
--format RspecJunitFormatter \
|
61
|
+
--out test_results/activerecord-integration.xml \
|
62
|
+
--format progress \
|
63
|
+
spec/n1_loader_spec.rb \
|
64
|
+
spec/activerecord_spec.rb
|
65
|
+
- run:
|
66
|
+
name: Run ActiveRecord integration RSpec
|
67
|
+
command: |
|
68
|
+
bundle exec rspec --profile 10 \
|
69
|
+
--format RspecJunitFormatter \
|
70
|
+
--out test_results/ar-lazy-preload-integration.xml \
|
71
|
+
--format progress \
|
72
|
+
spec/n1_loader_spec.rb \
|
73
|
+
spec/activerecord_spec.rb \
|
74
|
+
spec/ar_lazy_preload_spec.rb
|
54
75
|
- store_test_results:
|
55
76
|
path: test_results
|
56
77
|
|
@@ -80,15 +101,36 @@ workflows:
|
|
80
101
|
parameters:
|
81
102
|
ruby-version: [
|
82
103
|
"2.5",
|
83
|
-
"2.6",
|
84
104
|
"2.7",
|
85
|
-
"3.0",
|
86
105
|
"latest"
|
87
106
|
]
|
88
|
-
gemfile: [
|
89
|
-
"
|
107
|
+
activerecord-gemfile: [
|
108
|
+
"ar_5_latest",
|
109
|
+
"ar_6_latest"
|
90
110
|
]
|
91
|
-
|
111
|
+
ar_lazy_preload-gemfile: [
|
112
|
+
"ar_lazy_preload_0.6.1",
|
113
|
+
"ar_lazy_preload_master"
|
114
|
+
]
|
115
|
+
exclude:
|
116
|
+
# Ruby 2.5 and AR Lazy Preload 1+
|
117
|
+
- ruby-version: "2.5"
|
118
|
+
activerecord-gemfile: "ar_5_latest"
|
119
|
+
ar_lazy_preload-gemfile: "ar_lazy_preload_master"
|
120
|
+
|
121
|
+
- ruby-version: "2.5"
|
122
|
+
activerecord-gemfile: "ar_6_latest"
|
123
|
+
ar_lazy_preload-gemfile: "ar_lazy_preload_master"
|
124
|
+
|
125
|
+
# AR 5 and ruby 3+
|
126
|
+
- ruby-version: "latest"
|
127
|
+
activerecord-gemfile: "ar_5_latest"
|
128
|
+
ar_lazy_preload-gemfile: "ar_lazy_preload_0.6.1"
|
129
|
+
- ruby-version: "latest"
|
130
|
+
activerecord-gemfile: "ar_5_latest"
|
131
|
+
ar_lazy_preload-gemfile: "ar_lazy_preload_master"
|
132
|
+
|
133
|
+
name: ruby-<< matrix.ruby-version >>-<< matrix.activerecord-gemfile >>-<< matrix.ar_lazy_preload-gemfile >>
|
92
134
|
- rubocop:
|
93
135
|
requires:
|
94
136
|
- checkout
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,15 @@
|
|
1
|
-
## [
|
1
|
+
## [1.2.0]
|
2
|
+
|
3
|
+
- Introduce arguments support.
|
4
|
+
|
5
|
+
## [1.1.0] - 2021-12-27
|
6
|
+
|
7
|
+
- Introduce `fulfill` method to abstract the storage.
|
8
|
+
|
9
|
+
## [1.0.0] - 2021-12-26
|
10
|
+
|
11
|
+
- Various of great features.
|
2
12
|
|
3
13
|
## [0.1.0] - 2021-12-16
|
4
14
|
|
5
|
-
- Initial release
|
15
|
+
- Initial release.
|
data/Gemfile
CHANGED
@@ -4,3 +4,12 @@ source "https://rubygems.org"
|
|
4
4
|
|
5
5
|
# Specify your gem's dependencies in n1_loader.gemspec
|
6
6
|
gemspec
|
7
|
+
|
8
|
+
# Hack to make Github work with Circle CI job names with slashes
|
9
|
+
gemfiles = []
|
10
|
+
gemfiles << "activerecord-gemfiles/#{ENV["ACTIVERECORD_GEMFILE"]}.gemfile" if ENV["ACTIVERECORD_GEMFILE"]
|
11
|
+
gemfiles << "ar_lazy_preload-gemfiles/#{ENV["AR_LAZY_PRELOAD_GEMFILE"]}.gemfile" if ENV["AR_LAZY_PRELOAD_GEMFILE"]
|
12
|
+
|
13
|
+
gemfiles.each do |path|
|
14
|
+
eval(File.read(path)) # rubocop:disable Security/Eval
|
15
|
+
end
|
data/README.md
CHANGED
@@ -9,11 +9,16 @@ We have a solution for you!
|
|
9
9
|
[N1Loader][8] is designed to solve the issue for good!
|
10
10
|
|
11
11
|
It has many benefits:
|
12
|
-
- it
|
13
|
-
- it
|
12
|
+
- it can be [isolated](#isolated-loaders)
|
13
|
+
- it loads data [lazily](#lazy-loading)
|
14
|
+
- it supports [shareable loaders](#shareable-loaders) between multiple classes
|
15
|
+
- it supports [reloading](#reloading)
|
16
|
+
- it supports optimized [single object loading](#optimized-single-case)
|
17
|
+
- it supports [arguments](#arguments)
|
14
18
|
- it has an integration with [ActiveRecord][5] which makes it brilliant ([example](#activerecord))
|
15
19
|
- it has an integration with [ArLazyPreload][6] which makes it excellent ([example](#arlazypreload))
|
16
20
|
|
21
|
+
... and even more features to come! Stay tuned!
|
17
22
|
|
18
23
|
## Installation
|
19
24
|
|
@@ -25,48 +30,209 @@ gem 'n1_loader'
|
|
25
30
|
|
26
31
|
You can add integration with [ActiveRecord][5] by:
|
27
32
|
```ruby
|
28
|
-
require 'n1_loader/active_record'
|
33
|
+
gem 'n1_loader', require: 'n1_loader/active_record'
|
29
34
|
```
|
30
35
|
|
31
36
|
You can add the integration with [ActiveRecord][5] and [ArLazyPreload][6] by:
|
32
37
|
```ruby
|
33
|
-
require 'n1_loader/ar_lazy_preload'
|
38
|
+
gem 'n1_loader', require: 'n1_loader/ar_lazy_preload'
|
34
39
|
```
|
35
40
|
|
36
41
|
## Usage
|
37
42
|
|
38
43
|
```ruby
|
39
|
-
class
|
44
|
+
class User
|
40
45
|
include N1Loader::Loadable
|
41
46
|
|
42
47
|
# with inline loader
|
43
|
-
n1_loader :
|
44
|
-
|
45
|
-
|
48
|
+
n1_loader :orders_count do |users|
|
49
|
+
orders_per_user = Order.where(user: users).group(:user_id).count
|
50
|
+
|
51
|
+
users.each { |user| fulfill(user, orders_per_user[user.id]) }
|
46
52
|
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# For single object
|
56
|
+
user = User.new
|
57
|
+
user.orders_count
|
58
|
+
|
59
|
+
# For multiple objects without N+1
|
60
|
+
users = [User.new, User.new]
|
61
|
+
N1Loader::Preloader.new(users).preload(:orders_count)
|
62
|
+
users.map(&:orders_count)
|
63
|
+
```
|
64
|
+
|
65
|
+
### Lazy loading
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
class User
|
69
|
+
include N1Loader::Loadable
|
47
70
|
|
48
|
-
# with
|
49
|
-
n1_loader :
|
71
|
+
# with inline loader
|
72
|
+
n1_loader :orders_count do |users|
|
73
|
+
orders_per_user = Order.where(user: users).group(:user_id).count
|
74
|
+
|
75
|
+
users.each { |user| fulfill(user, orders_per_user[user.id]) }
|
76
|
+
end
|
50
77
|
end
|
51
78
|
|
52
|
-
|
53
|
-
|
54
|
-
|
79
|
+
user = User.new # => nothing was done for loading
|
80
|
+
user.orders_count # => first time loading
|
81
|
+
|
82
|
+
users = [User.new, User.new] # => nothing was done for loading
|
83
|
+
N1Loader::Preloader.new([users]).preload(:orders_count) # => we only initialized loader but didn't perform it yet
|
84
|
+
users.map(&:orders_count) # => loading has happen for the first time (without N+1)
|
85
|
+
```
|
86
|
+
|
87
|
+
|
88
|
+
### Shareable loaders
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
class OrdersCountLoader < N1Loader::Loader
|
92
|
+
def perform(users)
|
93
|
+
orders_per_user = Order.where(user: users).group(:user_id).count
|
94
|
+
|
95
|
+
users.each { |user| fulfill(user, orders_per_user[user.id]) }
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
class User
|
100
|
+
include N1Loader::Loadable
|
101
|
+
|
102
|
+
n1_loader :orders_count, OrdersCountLoader
|
103
|
+
end
|
104
|
+
|
105
|
+
class Customer
|
106
|
+
include N1Loader::Loadable
|
107
|
+
|
108
|
+
n1_loader :orders_count, OrdersCountLoader
|
109
|
+
end
|
110
|
+
|
111
|
+
User.new.orders_count # => works
|
112
|
+
Customer.new.orders_count # => works
|
113
|
+
```
|
114
|
+
|
115
|
+
### Reloading
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
class User
|
119
|
+
include N1Loader::Loadable
|
120
|
+
|
121
|
+
# with inline loader
|
122
|
+
n1_loader :orders_count do |users|
|
123
|
+
orders_per_user = Order.where(user: users).group(:user_id).count
|
124
|
+
|
125
|
+
users.each { |user| fulfill(user, orders_per_user[user.id]) }
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
user = User.new
|
130
|
+
user.orders_count # => loader is executed first time and value was cached
|
131
|
+
user.orders_count(reload: true) # => loader is executed again and a new value was cached
|
132
|
+
|
133
|
+
users = [User.new, User.new]
|
134
|
+
N1Loader::Preloader.new(users).preload(:orders_count) # => loader was initialized but not yet executed
|
135
|
+
users.map(&:orders_count) # => loader was executed first time without N+1 issue and values were cached
|
136
|
+
|
137
|
+
N1Loader::Preloader.new(users).preload(:orders_count) # => loader was initialized again but not yet executed
|
138
|
+
users.map(&:orders_count) # => new loader was executed first time without N+1 issue and new values were cached
|
139
|
+
```
|
140
|
+
|
141
|
+
### Isolated loaders
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
class IsolatedLoader < N1Loader::Loader
|
55
145
|
def perform(elements)
|
56
|
-
elements.
|
146
|
+
elements.each { |element| fulfill(element, [element]) }
|
57
147
|
end
|
58
148
|
end
|
59
149
|
|
60
|
-
|
61
|
-
|
62
|
-
|
150
|
+
objects = [1, 2, 3, 4]
|
151
|
+
loader = IsolatedLoader.new(objects)
|
152
|
+
objects.each do |object|
|
153
|
+
loader.for(object) # => it has no N+1 and it doesn't require to be injected in the class
|
154
|
+
end
|
155
|
+
```
|
63
156
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
157
|
+
### Optimized single case
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
class User
|
161
|
+
include N1Loader::Loadable
|
162
|
+
|
163
|
+
n1_loader :orders_count do # no arguments passed to the block, so we can override both perform and single.
|
164
|
+
def perform(users)
|
165
|
+
orders_per_user = Order.where(user: users).group(:user_id).count
|
166
|
+
|
167
|
+
users.each { |user| fulfill(user, orders_per_user[user.id]) }
|
168
|
+
end
|
169
|
+
|
170
|
+
# Optimized for single object loading
|
171
|
+
def single(user)
|
172
|
+
user.orders.count
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
user = User.new
|
178
|
+
user.orders_count # single will be used here
|
179
|
+
|
180
|
+
users = [User.new, User.new]
|
181
|
+
N1Loader::Preloader.new(users).preload(:orders_count)
|
182
|
+
users.map(&:orders_count) # perform will be used once without N+1
|
183
|
+
```
|
184
|
+
|
185
|
+
### Arguments
|
186
|
+
|
187
|
+
```ruby
|
188
|
+
class User
|
189
|
+
include N1Loader::Loadable
|
190
|
+
|
191
|
+
n1_loader :orders_count do |users, type|
|
192
|
+
orders_per_user = Order.where(type: type, user: users).group(:user_id).count
|
193
|
+
|
194
|
+
users.each { |user| fulfill(user, orders_per_user[user.id]) }
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
user = User.new
|
199
|
+
user.orders_count(:gifts) # The loader will be performed first time for this argument
|
200
|
+
user.orders_count(:sales) # The loader will be performed first time for this argument
|
201
|
+
user.orders_count(:gifts) # The cached value will be used
|
202
|
+
|
203
|
+
users = [User.new, User.new]
|
204
|
+
N1Loader::Preloader.new(users).preload(:orders_count)
|
205
|
+
users.map { |user| user.orders_count(:gifts) } # No N+1 here
|
68
206
|
```
|
69
207
|
|
208
|
+
_Note_: By default, we use `arguments.map(&:object_id)` to identify arguments but in some cases,
|
209
|
+
you may want to override it, for example:
|
210
|
+
|
211
|
+
```ruby
|
212
|
+
class User
|
213
|
+
include N1Loader::Loadable
|
214
|
+
|
215
|
+
n1_loader :orders_count do
|
216
|
+
def perform(users, sale)
|
217
|
+
orders_per_user = Order.where(sale: sale, user: users).group(:user_id).count
|
218
|
+
|
219
|
+
users.each { |user| fulfill(user, orders_per_user[user.id]) }
|
220
|
+
end
|
221
|
+
|
222
|
+
def self.arguments_key(sale)
|
223
|
+
sale.id
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
user = User.new
|
229
|
+
user.orders_count(Sale.first) # perform will be executed and value will be cached
|
230
|
+
user.orders_count(Sale.first) # the cached value will be returned
|
231
|
+
```
|
232
|
+
|
233
|
+
|
234
|
+
## Integrations
|
235
|
+
|
70
236
|
### [ActiveRecord][5]
|
71
237
|
|
72
238
|
```ruby
|
@@ -74,10 +240,9 @@ class User < ActiveRecord::Base
|
|
74
240
|
include N1Loader::Loadable
|
75
241
|
|
76
242
|
n1_loader :orders_count do |users|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
hash.transform_keys! { |key| users.find { |user| user.id == key } }
|
243
|
+
orders_per_user = Order.where(user: users).group(:user_id).count
|
244
|
+
|
245
|
+
users.each { |user| fulfill(user, orders_per_user[user.id]) }
|
81
246
|
end
|
82
247
|
end
|
83
248
|
|
@@ -103,10 +268,9 @@ class User < ActiveRecord::Base
|
|
103
268
|
include N1Loader::Loadable
|
104
269
|
|
105
270
|
n1_loader :orders_count do |users|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
hash.transform_keys! { |key| users.find { |user| user.id == key } }
|
271
|
+
orders_per_user = Order.where(user: users).group(:user_id).count
|
272
|
+
|
273
|
+
users.each { |user| fulfill(user, orders_per_user[user.id]) }
|
110
274
|
end
|
111
275
|
end
|
112
276
|
|
@@ -123,12 +287,6 @@ ArLazyPreload.config.auto_preload = true
|
|
123
287
|
User.all.map(:orders_count)
|
124
288
|
```
|
125
289
|
|
126
|
-
## Development
|
127
|
-
|
128
|
-
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.
|
129
|
-
|
130
|
-
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 the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
131
|
-
|
132
290
|
## Contributing
|
133
291
|
|
134
292
|
Bug reports and pull requests are welcome on GitHub at https://github.com/djezzzl/n1_loader.
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module N1Loader
|
4
|
+
module ActiveRecord
|
5
|
+
module Associations
|
6
|
+
module Preloader # :nodoc:
|
7
|
+
N1LoaderReflection = Struct.new(:key, :loader) do
|
8
|
+
def options
|
9
|
+
{}
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def preloaders_for_one(association, records, scope)
|
14
|
+
grouped_records(association, records).flat_map do |reflection, klasses|
|
15
|
+
next N1Loader::Preloader.new(records).preload(reflection.key) if reflection.is_a?(N1LoaderReflection)
|
16
|
+
|
17
|
+
klasses.map do |rhs_klass, rs|
|
18
|
+
loader = preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope)
|
19
|
+
loader.run self
|
20
|
+
loader
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def grouped_records(association, records)
|
26
|
+
n1_load_records, records = records.partition do |record|
|
27
|
+
record.class.respond_to?(:n1_loader_defined?) && record.class.n1_loader_defined?(association)
|
28
|
+
end
|
29
|
+
|
30
|
+
hash = n1_load_records.group_by do |record|
|
31
|
+
N1LoaderReflection.new(association, record.class.n1_loader(association))
|
32
|
+
end
|
33
|
+
|
34
|
+
hash.merge(super)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
File without changes
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
N1Loader::LoaderCollection.define_method :preloaded_records do
|
4
|
+
unless loader_class.instance_method(:perform).arity == 1
|
5
|
+
raise N1Loader::ActiveRecord::InvalidPreloading, "Cannot preload loader with arguments"
|
6
|
+
end
|
7
|
+
|
8
|
+
with.preloaded_records
|
9
|
+
end
|
@@ -1,9 +1,28 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# Load core library
|
4
|
+
require_relative "../n1_loader"
|
5
|
+
|
6
|
+
# Load integration dependency
|
3
7
|
require "active_record"
|
4
8
|
|
5
|
-
|
9
|
+
module N1Loader
|
10
|
+
module ActiveRecord
|
11
|
+
class InvalidPreloading < N1Loader::Error; end
|
12
|
+
end
|
13
|
+
end
|
6
14
|
|
15
|
+
# Library integration
|
7
16
|
ActiveSupport.on_load(:active_record) do
|
17
|
+
require_relative "active_record/loader"
|
18
|
+
require_relative "active_record/loader_collection"
|
19
|
+
|
20
|
+
case ActiveRecord::VERSION::MAJOR
|
21
|
+
when 6
|
22
|
+
require_relative "active_record/associations_preloader_v6"
|
23
|
+
else
|
24
|
+
require_relative "active_record/associations_preloader_v5"
|
25
|
+
end
|
26
|
+
|
8
27
|
ActiveRecord::Associations::Preloader.prepend(N1Loader::ActiveRecord::Associations::Preloader)
|
9
28
|
end
|
@@ -4,9 +4,16 @@ module N1Loader
|
|
4
4
|
module ArLazyPreload
|
5
5
|
# Context builder for N1Loader
|
6
6
|
class AssociatedContextBuilder < ::ArLazyPreload::AssociatedContextBuilder
|
7
|
+
attr_reader :records
|
8
|
+
|
9
|
+
def initialize(parent_context:, association_name:, records:)
|
10
|
+
super(parent_context: parent_context, association_name: association_name)
|
11
|
+
@records = records
|
12
|
+
end
|
13
|
+
|
7
14
|
def perform
|
8
15
|
::ArLazyPreload::Context.register(
|
9
|
-
records:
|
16
|
+
records: records.flatten(1).select { |record| record.respond_to?(:lazy_preload_context=) },
|
10
17
|
association_tree: child_association_tree,
|
11
18
|
auto_preload: parent_context.auto_preload?
|
12
19
|
)
|
@@ -6,7 +6,7 @@ module N1Loader
|
|
6
6
|
class ContextAdapter
|
7
7
|
attr_reader :context
|
8
8
|
|
9
|
-
|
9
|
+
delegate_missing_to :context
|
10
10
|
|
11
11
|
def initialize(context)
|
12
12
|
@context = context
|
@@ -19,12 +19,15 @@ module N1Loader
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def perform_preloading(association_name)
|
22
|
-
|
22
|
+
context_setup = lambda { |records|
|
23
|
+
AssociatedContextBuilder.prepare(
|
24
|
+
parent_context: self,
|
25
|
+
association_name: association_name,
|
26
|
+
records: records
|
27
|
+
)
|
28
|
+
}
|
23
29
|
|
24
|
-
|
25
|
-
parent_context: self,
|
26
|
-
association_name: association_name
|
27
|
-
)
|
30
|
+
N1Loader::Preloader.new(records, context_setup).preload(association_name)
|
28
31
|
end
|
29
32
|
end
|
30
33
|
end
|
@@ -9,13 +9,13 @@ module N1Loader
|
|
9
9
|
|
10
10
|
define_method(loader_name) do
|
11
11
|
loader = instance_variable_get(loader_variable_name)
|
12
|
-
|
13
12
|
return loader if loader
|
13
|
+
|
14
14
|
if respond_to?(:lazy_preload_context) && ContextAdapter.new(lazy_preload_context).try_preload_lazily(name)
|
15
15
|
return instance_variable_get(loader_variable_name)
|
16
16
|
end
|
17
17
|
|
18
|
-
|
18
|
+
send("#{loader_name}_reload")
|
19
19
|
end
|
20
20
|
end
|
21
21
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module N1Loader
|
4
|
+
module ArLazyPreload
|
5
|
+
# A patch to {N1Loader::LoaderCollection} to setup lazy context lazily.
|
6
|
+
module LoaderCollectionPatch
|
7
|
+
attr_accessor :context_setup
|
8
|
+
|
9
|
+
def with(*args)
|
10
|
+
result = super
|
11
|
+
|
12
|
+
result.context_setup = context_setup if context_setup && result.context_setup.nil?
|
13
|
+
|
14
|
+
result
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module N1Loader
|
4
|
+
module ArLazyPreload
|
5
|
+
# A patch to {N1Loader::Loader} to setup lazy context lazily.
|
6
|
+
module LoaderPatch
|
7
|
+
attr_accessor :context_setup
|
8
|
+
|
9
|
+
def loaded
|
10
|
+
return @loaded if @loaded
|
11
|
+
|
12
|
+
super
|
13
|
+
|
14
|
+
context_setup&.call(preloaded_records)
|
15
|
+
|
16
|
+
@loaded
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module N1Loader
|
4
|
+
module ArLazyPreload
|
5
|
+
# A patch to {N1Loader::Preloader} setup lazy context lazily.
|
6
|
+
module PreloaderPatch
|
7
|
+
def initialize(elements, context_setup = nil)
|
8
|
+
super(elements)
|
9
|
+
@context_setup = context_setup
|
10
|
+
end
|
11
|
+
|
12
|
+
def preload(*keys)
|
13
|
+
super.each do |loader_collection|
|
14
|
+
loader_collection.context_setup = context_setup
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :context_setup
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -1,12 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# Load core library
|
3
4
|
require_relative "active_record"
|
4
5
|
|
6
|
+
# Load integration dependency
|
5
7
|
require "rails"
|
6
8
|
require "ar_lazy_preload"
|
7
9
|
|
10
|
+
# Library integration
|
8
11
|
require_relative "ar_lazy_preload/loadable"
|
9
12
|
require_relative "ar_lazy_preload/context_adapter"
|
10
13
|
require_relative "ar_lazy_preload/associated_context_builder"
|
14
|
+
require_relative "ar_lazy_preload/loader_collection_patch"
|
15
|
+
require_relative "ar_lazy_preload/preloader_patch"
|
16
|
+
require_relative "ar_lazy_preload/loader_patch"
|
11
17
|
|
12
18
|
N1Loader::Loadable::ClassMethods.prepend(N1Loader::ArLazyPreload::Loadable::ClassMethods)
|
19
|
+
N1Loader::Preloader.prepend(N1Loader::ArLazyPreload::PreloaderPatch)
|
20
|
+
N1Loader::Loader.prepend(N1Loader::ArLazyPreload::LoaderPatch)
|
21
|
+
N1Loader::LoaderCollection.prepend(N1Loader::ArLazyPreload::LoaderCollectionPatch)
|
@@ -7,9 +7,9 @@ module N1Loader
|
|
7
7
|
# include N1Loader::Loadable
|
8
8
|
#
|
9
9
|
# # with inline loader
|
10
|
-
# n1_loader :something do
|
11
|
-
# elements
|
12
|
-
#
|
10
|
+
# n1_loader :something do
|
11
|
+
# def perform(elements)
|
12
|
+
# elements.each { |element| fulfill(element,, element.calculate_something) }
|
13
13
|
# end
|
14
14
|
# end
|
15
15
|
#
|
@@ -20,9 +20,7 @@ module N1Loader
|
|
20
20
|
# # custom loader
|
21
21
|
# class MyLoader < N1Loader::Loader
|
22
22
|
# def perform(elements)
|
23
|
-
# elements.
|
24
|
-
# hash[element] = element.calculate_something
|
25
|
-
# end
|
23
|
+
# elements.each { |element| fulfill(element,, element.calculate_something) }
|
26
24
|
# end
|
27
25
|
# end
|
28
26
|
module Loadable
|
@@ -30,8 +28,8 @@ module N1Loader
|
|
30
28
|
send("#{name}_loader")
|
31
29
|
end
|
32
30
|
|
33
|
-
def n1_loader_set(name,
|
34
|
-
send("#{name}_loader=",
|
31
|
+
def n1_loader_set(name, loader_collection)
|
32
|
+
send("#{name}_loader=", loader_collection)
|
35
33
|
end
|
36
34
|
|
37
35
|
def self.included(base)
|
@@ -47,11 +45,14 @@ module N1Loader
|
|
47
45
|
respond_to?("#{name}_loader")
|
48
46
|
end
|
49
47
|
|
50
|
-
def n1_load(name, loader = nil, &block) # rubocop:disable Metrics/MethodLength
|
48
|
+
def n1_load(name, loader = nil, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
51
49
|
loader ||= Class.new(N1Loader::Loader) do
|
52
|
-
|
50
|
+
if block&.arity&.positive?
|
51
|
+
define_method(:perform, &block)
|
52
|
+
else
|
53
|
+
class_eval(&block)
|
54
|
+
end
|
53
55
|
end
|
54
|
-
|
55
56
|
loader_name = "#{name}_loader"
|
56
57
|
loader_variable_name = "@#{loader_name}"
|
57
58
|
|
@@ -59,17 +60,23 @@ module N1Loader
|
|
59
60
|
loader
|
60
61
|
end
|
61
62
|
|
62
|
-
define_method("#{loader_name}
|
63
|
-
instance_variable_set(loader_variable_name,
|
63
|
+
define_method("#{loader_name}_reload") do
|
64
|
+
instance_variable_set(loader_variable_name,
|
65
|
+
N1Loader::LoaderCollection.new(self.class.send(loader_name), [self]))
|
66
|
+
end
|
67
|
+
|
68
|
+
define_method("#{loader_name}=") do |loader_collection_instance|
|
69
|
+
instance_variable_set(loader_variable_name, loader_collection_instance)
|
64
70
|
end
|
65
71
|
|
66
72
|
define_method(loader_name) do
|
67
|
-
instance_variable_get(loader_variable_name) ||
|
68
|
-
instance_variable_set(loader_variable_name, self.class.send(loader_name).new([self]))
|
73
|
+
instance_variable_get(loader_variable_name) || send("#{loader_name}_reload")
|
69
74
|
end
|
70
75
|
|
71
|
-
define_method(name) do
|
72
|
-
send(loader_name)
|
76
|
+
define_method(name) do |*args, reload: false|
|
77
|
+
send("#{loader_name}_reload") if reload
|
78
|
+
|
79
|
+
send(loader_name).with(*args).for(self)
|
73
80
|
end
|
74
81
|
|
75
82
|
[name, loader_name, loader_variable_name]
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module N1Loader
|
4
|
+
# Loader that performs the loading.
|
5
|
+
#
|
6
|
+
# Subclasses must define +perform+ method that accepts single argument
|
7
|
+
# and returns hash where key is the element and value is what we want to load.
|
8
|
+
class Loader
|
9
|
+
def self.arguments_key(*args)
|
10
|
+
args.map(&:object_id)
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(elements, *args)
|
14
|
+
@elements = elements
|
15
|
+
@args = args
|
16
|
+
end
|
17
|
+
|
18
|
+
def for(element)
|
19
|
+
if loaded.empty? && elements.any?
|
20
|
+
raise NotFilled, "Nothing was preloaded, perhaps you forgot to use fulfill method"
|
21
|
+
end
|
22
|
+
raise NotLoaded, "The data was not preloaded for the given element" unless loaded.key?(element)
|
23
|
+
|
24
|
+
loaded[element]
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :elements, :args
|
30
|
+
|
31
|
+
def perform(_elements)
|
32
|
+
raise NotImplemented, "Subclasses have to implement the method"
|
33
|
+
end
|
34
|
+
|
35
|
+
def fulfill(element, value)
|
36
|
+
@loaded[element] = value
|
37
|
+
end
|
38
|
+
|
39
|
+
def loaded
|
40
|
+
return @loaded if @loaded
|
41
|
+
|
42
|
+
@loaded = {}.compare_by_identity
|
43
|
+
|
44
|
+
if elements.size == 1 && respond_to?(:single)
|
45
|
+
fulfill(elements.first, single(elements.first, *args))
|
46
|
+
elsif elements.any?
|
47
|
+
perform(elements, *args)
|
48
|
+
end
|
49
|
+
|
50
|
+
@loaded
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module N1Loader
|
4
|
+
# The class is used for storing collections of loaders for elements per set of arguments.
|
5
|
+
class LoaderCollection
|
6
|
+
attr_reader :loader_class, :elements
|
7
|
+
|
8
|
+
def initialize(loader_class, elements)
|
9
|
+
@loader_class = loader_class
|
10
|
+
@elements = elements
|
11
|
+
end
|
12
|
+
|
13
|
+
def with(*args)
|
14
|
+
loaders[loader_class.arguments_key(*args)] ||= loader_class.new(elements, *args)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def loaders
|
20
|
+
@loaders ||= {}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -19,9 +19,9 @@ module N1Loader
|
|
19
19
|
elements
|
20
20
|
.group_by { |element| element.class.n1_loader(key) }
|
21
21
|
.map do |loader_class, grouped_elements|
|
22
|
-
|
23
|
-
grouped_elements.each { |grouped_element| grouped_element.n1_loader_set(key,
|
24
|
-
|
22
|
+
loader_collection = N1Loader::LoaderCollection.new(loader_class, grouped_elements)
|
23
|
+
grouped_elements.each { |grouped_element| grouped_element.n1_loader_set(key, loader_collection) }
|
24
|
+
loader_collection
|
25
25
|
end
|
26
26
|
end
|
27
27
|
end
|
data/lib/n1_loader/version.rb
CHANGED
data/lib/n1_loader.rb
CHANGED
@@ -2,12 +2,14 @@
|
|
2
2
|
|
3
3
|
require_relative "n1_loader/version"
|
4
4
|
|
5
|
-
require_relative "n1_loader/loader"
|
6
|
-
require_relative "n1_loader/
|
7
|
-
require_relative "n1_loader/
|
5
|
+
require_relative "n1_loader/core/loader"
|
6
|
+
require_relative "n1_loader/core/loader_collection"
|
7
|
+
require_relative "n1_loader/core/loadable"
|
8
|
+
require_relative "n1_loader/core/preloader"
|
8
9
|
|
9
10
|
module N1Loader # :nodoc:
|
10
11
|
class Error < StandardError; end
|
11
12
|
class NotImplemented < Error; end
|
12
13
|
class NotLoaded < Error; end
|
14
|
+
class NotFilled < Error; end
|
13
15
|
end
|
data/n1_loader.gemspec
CHANGED
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.authors = ["Evgeniy Demin"]
|
9
9
|
spec.email = ["lawliet.djez@gmail.com"]
|
10
10
|
|
11
|
-
spec.summary = "
|
11
|
+
spec.summary = "Loader to solve N+1 issue for good."
|
12
12
|
spec.homepage = "https://github.com/djezzzl/n1_loader"
|
13
13
|
spec.license = "MIT"
|
14
14
|
spec.required_ruby_version = ">= 2.5.0"
|
@@ -22,10 +22,10 @@ Gem::Specification.new do |spec|
|
|
22
22
|
end
|
23
23
|
spec.require_paths = ["lib"]
|
24
24
|
|
25
|
-
spec.add_development_dependency "activerecord", "
|
26
|
-
spec.add_development_dependency "ar_lazy_preload", "
|
25
|
+
spec.add_development_dependency "activerecord", ">= 5"
|
26
|
+
spec.add_development_dependency "ar_lazy_preload", ">= 0.6"
|
27
27
|
spec.add_development_dependency "db-query-matchers", "~> 0.10"
|
28
|
-
spec.add_development_dependency "rails", "
|
28
|
+
spec.add_development_dependency "rails", ">= 5"
|
29
29
|
spec.add_development_dependency "rspec", "~> 3.0"
|
30
30
|
spec.add_development_dependency "rspec_junit_formatter", "~> 0.4"
|
31
31
|
spec.add_development_dependency "rubocop", "~> 1.7"
|
metadata
CHANGED
@@ -1,43 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: n1_loader
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Evgeniy Demin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-01-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '5'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '5'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: ar_lazy_preload
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '0.
|
33
|
+
version: '0.6'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '0.
|
40
|
+
version: '0.6'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: db-query-matchers
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -56,16 +56,16 @@ dependencies:
|
|
56
56
|
name: rails
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- - "
|
59
|
+
- - ">="
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '
|
61
|
+
version: '5'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- - "
|
66
|
+
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: '
|
68
|
+
version: '5'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: rspec
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -139,19 +139,29 @@ files:
|
|
139
139
|
- LICENSE.txt
|
140
140
|
- README.md
|
141
141
|
- Rakefile
|
142
|
+
- activerecord-gemfiles/ar_5_latest.gemfile
|
143
|
+
- activerecord-gemfiles/ar_6_latest.gemfile
|
144
|
+
- ar_lazy_preload-gemfiles/ar_lazy_preload_0.6.1.gemfile
|
145
|
+
- ar_lazy_preload-gemfiles/ar_lazy_preload_master.gemfile
|
142
146
|
- bin/console
|
143
147
|
- bin/setup
|
144
|
-
- gemfiles/ar_6_latest.gemfile
|
145
148
|
- lib/n1_loader.rb
|
146
149
|
- lib/n1_loader/active_record.rb
|
147
|
-
- lib/n1_loader/active_record/
|
150
|
+
- lib/n1_loader/active_record/associations_preloader_v5.rb
|
151
|
+
- lib/n1_loader/active_record/associations_preloader_v6.rb
|
152
|
+
- lib/n1_loader/active_record/loader.rb
|
153
|
+
- lib/n1_loader/active_record/loader_collection.rb
|
148
154
|
- lib/n1_loader/ar_lazy_preload.rb
|
149
155
|
- lib/n1_loader/ar_lazy_preload/associated_context_builder.rb
|
150
156
|
- lib/n1_loader/ar_lazy_preload/context_adapter.rb
|
151
157
|
- lib/n1_loader/ar_lazy_preload/loadable.rb
|
152
|
-
- lib/n1_loader/
|
153
|
-
- lib/n1_loader/
|
154
|
-
- lib/n1_loader/
|
158
|
+
- lib/n1_loader/ar_lazy_preload/loader_collection_patch.rb
|
159
|
+
- lib/n1_loader/ar_lazy_preload/loader_patch.rb
|
160
|
+
- lib/n1_loader/ar_lazy_preload/preloader_patch.rb
|
161
|
+
- lib/n1_loader/core/loadable.rb
|
162
|
+
- lib/n1_loader/core/loader.rb
|
163
|
+
- lib/n1_loader/core/loader_collection.rb
|
164
|
+
- lib/n1_loader/core/preloader.rb
|
155
165
|
- lib/n1_loader/version.rb
|
156
166
|
- n1_loader.gemspec
|
157
167
|
homepage: https://github.com/djezzzl/n1_loader
|
@@ -179,5 +189,5 @@ requirements: []
|
|
179
189
|
rubygems_version: 3.2.22
|
180
190
|
signing_key:
|
181
191
|
specification_version: 4
|
182
|
-
summary:
|
192
|
+
summary: Loader to solve N+1 issue for good.
|
183
193
|
test_files: []
|
data/lib/n1_loader/loader.rb
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module N1Loader
|
4
|
-
# Loader that performs the loading.
|
5
|
-
#
|
6
|
-
# Subclasses must define +perform+ method that accepts single argument
|
7
|
-
# and returns hash where key is the element and value is what we want to load.
|
8
|
-
class Loader
|
9
|
-
def initialize(elements)
|
10
|
-
@elements = elements
|
11
|
-
end
|
12
|
-
|
13
|
-
def perform(_elements)
|
14
|
-
raise NotImplemented, "Subclasses have to implement the method"
|
15
|
-
end
|
16
|
-
|
17
|
-
def loaded
|
18
|
-
@loaded ||= perform(elements)
|
19
|
-
end
|
20
|
-
|
21
|
-
def preloaded_records
|
22
|
-
@preloaded_records ||= loaded.values
|
23
|
-
end
|
24
|
-
|
25
|
-
def for(element)
|
26
|
-
raise NotLoaded, "The data was not preloaded for the given element" unless elements.include?(element)
|
27
|
-
|
28
|
-
loaded.compare_by_identity[element]
|
29
|
-
end
|
30
|
-
|
31
|
-
private
|
32
|
-
|
33
|
-
attr_reader :elements
|
34
|
-
end
|
35
|
-
end
|