n1_loader 1.0.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e86d093c173eed577feb029f483c8b283f516bba1611391f9036e5675dec91d5
4
- data.tar.gz: 626e0305af7e75fd6a5dbb23d6970a8972ca292a9449f2f32d61cf7849c57ec9
3
+ metadata.gz: 2d0f96166ae91c856012ea26c933cc56c7d33f68065b753a18adc3c6b9da7e87
4
+ data.tar.gz: adccc88c382d9283c2af9b41059298fa15563e47615678ef20629a0183a51253
5
5
  SHA512:
6
- metadata.gz: 3b450b20c25c935d3644003bf320e50644376cb6fc77cbda414522947a50857c3b28c5607bbcfd484dd9626a64523726cb4c6dd98b402a9ed3b73f1d6a40785a
7
- data.tar.gz: 9d07c4a64e89f2c2e9aa230d86c780388aaf720002db5fac37fe9b98ffd6297b914ab49ec54306c728c3457ea7a323a52d66c3c9e78c40ccbaed0a511be1f012
6
+ metadata.gz: ad3b1273627cf7abb5922cc7139c34e5e9543dc91d296d7e44deea690367d32082dd1ddc028ba40a4ed7fa402a65eb8f5aed36b6d8df0436937e59aaa6cef65a
7
+ data.tar.gz: fde03e325f2b780a070786f1648a98d278933c84b3266cedf4db50cd268b029839ff06290416846a9ab2a85a25a0795c30684845e4da15909417389dbd1a7a82
data/.circleci/config.yml CHANGED
@@ -11,8 +11,8 @@ executors:
11
11
  docker:
12
12
  - image: circleci/ruby:<< parameters.tag >>
13
13
  environment:
14
- - BUNDLE_JOBS: 4
15
- - BUNDLE_RETRY: 3
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/rspec.xml \
53
+ --out test_results/core.xml \
52
54
  --format progress \
53
- $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
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,22 +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
- "gemfiles/ar_5_latest.gemfile",
90
- "gemfiles/ar_6_latest.gemfile"
107
+ activerecord-gemfile: [
108
+ "ar_5_latest",
109
+ "ar_6_latest"
110
+ ]
111
+ ar_lazy_preload-gemfile: [
112
+ "ar_lazy_preload_0.6.1",
113
+ "ar_lazy_preload_master"
91
114
  ]
92
115
  exclude:
93
- - ruby-version: "3.0"
94
- gemfile: "gemfiles/ar_5_latest.gemfile"
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"
95
129
  - ruby-version: "latest"
96
- gemfile: "gemfiles/ar_5_latest.gemfile"
130
+ activerecord-gemfile: "ar_5_latest"
131
+ ar_lazy_preload-gemfile: "ar_lazy_preload_master"
97
132
 
98
- name: << matrix.gemfile >>-build-ruby-<< matrix.ruby-version >>
133
+ name: ruby-<< matrix.ruby-version >>-<< matrix.activerecord-gemfile >>-<< matrix.ar_lazy_preload-gemfile >>
99
134
  - rubocop:
100
135
  requires:
101
136
  - checkout
data/CHANGELOG.md CHANGED
@@ -1,3 +1,27 @@
1
+ ## [1.4.0] - 2022-02-22
2
+
3
+ - add support of optional arguments
4
+
5
+ BREAKING CHANGES:
6
+ - rework arguments to use single definition through `argument <name>` only
7
+ - use keyword arguments
8
+
9
+ ## [1.3.0] - 2022-02-22
10
+
11
+ - add support of named arguments with `argument <name>`
12
+
13
+ BREAKING CHANGES:
14
+ - rename `n1_load` to `n1_optimized`
15
+ - rework `def self.arguments_key` to `cache_key`
16
+
17
+ ## [1.2.0] - 2022-01-14
18
+
19
+ - Introduce arguments support.
20
+
21
+ ## [1.1.0] - 2021-12-27
22
+
23
+ - Introduce `fulfill` method to abstract the storage.
24
+
1
25
  ## [1.0.0] - 2021-12-26
2
26
 
3
27
  - Various of great features.
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
@@ -14,6 +14,7 @@ It has many benefits:
14
14
  - it supports [shareable loaders](#shareable-loaders) between multiple classes
15
15
  - it supports [reloading](#reloading)
16
16
  - it supports optimized [single object loading](#optimized-single-case)
17
+ - it supports [arguments](#arguments)
17
18
  - it has an integration with [ActiveRecord][5] which makes it brilliant ([example](#activerecord))
18
19
  - it has an integration with [ArLazyPreload][6] which makes it excellent ([example](#arlazypreload))
19
20
 
@@ -39,125 +40,115 @@ gem 'n1_loader', require: 'n1_loader/ar_lazy_preload'
39
40
 
40
41
  ## Usage
41
42
 
42
- **Supported Ruby version:** 2.5, 2.6, 2.7, 3.0, and latest.
43
-
44
43
  ```ruby
45
- class Example
44
+ class User
46
45
  include N1Loader::Loadable
47
46
 
48
47
  # with inline loader
49
- n1_loader :anything do |elements|
50
- # Has to return a hash that has keys as element from elements
51
- elements.group_by(&:itself)
52
- end
53
-
54
- # with custom loader
55
- n1_loader :something, MyLoader
56
- end
57
-
58
- # Custom loader that can be shared with many classes
59
- class MyLoader < N1Loader::Loader
60
- # Has to return a hash that has keys as element from elements
61
- def perform(elements)
62
- elements.group_by(&:itself)
48
+ n1_optimized :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]) }
63
52
  end
64
53
  end
65
54
 
66
55
  # For single object
67
- ex = Example.new
68
- ex.anything
56
+ user = User.new
57
+ user.orders_count
69
58
 
70
59
  # For multiple objects without N+1
71
- objects = [Example.new, Example.new]
72
- N1Loader::Preloader.new(objects).preload(:anything)
73
- objects.map(&:anything)
60
+ users = [User.new, User.new]
61
+ N1Loader::Preloader.new(users).preload(:orders_count)
62
+ users.map(&:orders_count)
74
63
  ```
75
64
 
76
65
  ### Lazy loading
77
66
 
78
67
  ```ruby
79
- class Example
68
+ class User
80
69
  include N1Loader::Loadable
81
-
82
- n1_loader :anything do |elements|
83
- # Has to return a hash that has keys as element from elements
84
- elements.group_by(&:itself)
70
+
71
+ # with inline loader
72
+ n1_optimized :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]) }
85
76
  end
86
77
  end
87
78
 
88
- object = Example.new # => nothing was done for loading
89
- object.anything # => first time loading
79
+ user = User.new # => nothing was done for loading
80
+ user.orders_count # => first time loading
90
81
 
91
- objects = [Example.new, Example.new] # => nothing was done for loading
92
- N1Loader::Preloader.new([objects]).preload(:anything) # => we only initial loader but didn't perform it yet
93
- objects.map(&:anything) # => loading happen for the first time (without N+1)
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)
94
85
  ```
95
86
 
96
87
 
97
88
  ### Shareable loaders
98
89
 
99
90
  ```ruby
100
- class MyLoader < N1Loader::Loader
101
- def perform(elements)
102
- # Has to return a hash that has keys as element from elements
103
- elements.group_by(&:itself)
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]) }
104
96
  end
105
97
  end
106
98
 
107
- class A
99
+ class User
108
100
  include N1Loader::Loadable
109
-
110
- n1_loader :anything, MyLoader
101
+
102
+ n1_optimized :orders_count, OrdersCountLoader
111
103
  end
112
104
 
113
- class B
105
+ class Customer
114
106
  include N1Loader::Loadable
115
-
116
- n1_loader :something, MyLoader
107
+
108
+ n1_optimized :orders_count, OrdersCountLoader
117
109
  end
118
110
 
119
- A.new.anything # => works
120
- B.new.something # => works
111
+ User.new.orders_count # => works
112
+ Customer.new.orders_count # => works
121
113
  ```
122
114
 
123
115
  ### Reloading
124
116
 
125
117
  ```ruby
126
- class Example
118
+ class User
127
119
  include N1Loader::Loadable
128
120
 
129
121
  # with inline loader
130
- n1_loader :anything do |elements|
131
- # Has to return a hash that has keys as element from elements
132
- elements.group_by(&:itself)
122
+ n1_optimized :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]) }
133
126
  end
134
127
  end
135
128
 
136
- object = Example.new
137
- object.anything # => loader is executed first time and value was cached
138
- object.anything(reload: true) # => loader is executed again and a new value was cached
139
-
140
- objects = [Example.new, Example.new]
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
141
132
 
142
- N1Loader::Preloader.new(objects).preload(:anything) # => loader was initialized but not yet executed
143
- objects.map(&:anything) # => loader was executed first time without N+1 issue and values were cached
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
144
136
 
145
- N1Loader::Preloader.new(objects).preload(:anything) # => loader was initialized again but not yet executed
146
- objects.map(&:anything) # => new loader was executed first time without N+1 issue and new values were cached
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
147
139
  ```
148
140
 
149
141
  ### Isolated loaders
150
142
 
151
143
  ```ruby
152
- class MyLoader < N1Loader::Loader
144
+ class IsolatedLoader < N1Loader::Loader
153
145
  def perform(elements)
154
- # Has to return a hash that has keys as element from elements
155
- elements.group_by(&:itself)
146
+ elements.each { |element| fulfill(element, [element]) }
156
147
  end
157
148
  end
158
149
 
159
150
  objects = [1, 2, 3, 4]
160
- loader = MyLoader.new(objects)
151
+ loader = IsolatedLoader.new(objects)
161
152
  objects.each do |object|
162
153
  loader.for(object) # => it has no N+1 and it doesn't require to be injected in the class
163
154
  end
@@ -166,48 +157,98 @@ end
166
157
  ### Optimized single case
167
158
 
168
159
  ```ruby
169
- class Example
160
+ class User
170
161
  include N1Loader::Loadable
171
-
172
- n1_loader :something do # no arguments passed to the block, so we can override both perform and single.
173
- def perform(elements)
174
- # Has to return a hash that has keys as element from elements
175
- elements.group_by(&:itself)
162
+
163
+ n1_optimized :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]) }
176
168
  end
177
169
 
178
170
  # Optimized for single object loading
179
- def single(element)
180
- # Just return a value you want to have for this element
181
- element
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_optimized :orders_count do
192
+ argument :type
193
+
194
+ def perform(users)
195
+ orders_per_user = Order.where(type: type, user: users).group(:user_id).count
196
+
197
+ users.each { |user| fulfill(user, orders_per_user[user.id]) }
182
198
  end
183
199
  end
184
200
  end
185
201
 
186
- object = Example.new
187
- object.something # single will be used here
202
+ user = User.new
203
+ user.orders_count(type: :gifts) # The loader will be performed first time for this argument
204
+ user.orders_count(type: :sales) # The loader will be performed first time for this argument
205
+ user.orders_count(type: :gifts) # The cached value will be used
188
206
 
189
- objects = [Example.new, Example.new]
190
- N1Loader::Preloader.new(objects).preload(:something)
191
- objects.map(&:something) # perform will be used once without N+1
207
+ users = [User.new, User.new]
208
+ N1Loader::Preloader.new(users).preload(:orders_count)
209
+ users.map { |user| user.orders_count(type: :gifts) } # No N+1 here
192
210
  ```
193
211
 
212
+ _Note_: By default, we use `arguments.map(&:object_id)` to identify arguments but in some cases,
213
+ you may want to override it, for example:
214
+
215
+ ```ruby
216
+ class User
217
+ include N1Loader::Loadable
218
+
219
+ n1_optimized :orders_count do
220
+ argument :sale
221
+
222
+ cache_key { sale.id }
223
+
224
+ def perform(users)
225
+ orders_per_user = Order.where(sale: sale, user: users).group(:user_id).count
226
+
227
+ users.each { |user| fulfill(user, orders_per_user[user.id]) }
228
+ end
229
+ end
230
+ end
231
+
232
+ user = User.new
233
+ user.orders_count(sale: Sale.first) # perform will be executed and value will be cached
234
+ user.orders_count(sale: Sale.first) # the cached value will be returned
235
+ ```
236
+
237
+
194
238
  ## Integrations
195
239
 
196
240
  ### [ActiveRecord][5]
197
241
 
198
- **Supported versions**: 5, 6.
199
-
200
- _Note_: Please open an issue if you interested in support of version 7 or other.
242
+ _Note_: Rails 7 support is coming soon! Stay tuned!
201
243
 
202
244
  ```ruby
203
245
  class User < ActiveRecord::Base
204
246
  include N1Loader::Loadable
205
-
206
- n1_loader :orders_count do |users|
207
- hash = Order.where(user: users).group(:user_id).count
208
-
209
- # hash has to have keys as initial elements
210
- hash.transform_keys! { |key| users.find { |user| user.id == key } }
247
+
248
+ n1_optimized :orders_count do |users|
249
+ orders_per_user = Order.where(user: users).group(:user_id).count
250
+
251
+ users.each { |user| fulfill(user, orders_per_user[user.id]) }
211
252
  end
212
253
  end
213
254
 
@@ -231,12 +272,11 @@ users.map(&:orders_count)
231
272
  ```ruby
232
273
  class User < ActiveRecord::Base
233
274
  include N1Loader::Loadable
234
-
235
- n1_loader :orders_count do |users|
236
- hash = Order.where(user: users).group(:user_id).count
237
-
238
- # hash has to have keys as initial elements
239
- hash.transform_keys! { |key| users.find { |user| user.id == key } }
275
+
276
+ n1_optimized :orders_count do |users|
277
+ orders_per_user = Order.where(user: users).group(:user_id).count
278
+
279
+ users.each { |user| fulfill(user, orders_per_user[user.id]) }
240
280
  end
241
281
  end
242
282
 
@@ -253,12 +293,6 @@ ArLazyPreload.config.auto_preload = true
253
293
  User.all.map(:orders_count)
254
294
  ```
255
295
 
256
- ## Development
257
-
258
- 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.
259
-
260
- 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).
261
-
262
296
  ## Contributing
263
297
 
264
298
  Bug reports and pull requests are welcome on GitHub at https://github.com/djezzzl/n1_loader.
@@ -1,7 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- source "https://rubygems.org"
4
-
5
3
  gem "activerecord", "~> 5"
6
-
7
- gemspec path: "../"
@@ -1,7 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- source "https://rubygems.org"
4
-
5
3
  gem "activerecord", "~> 6"
6
-
7
- gemspec path: "../"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ gem "ar_lazy_preload", "= 0.6.1"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ gem "ar_lazy_preload", git: "https://github.com/DmitryTsepelev/ar_lazy_preload", branch: "master"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ N1Loader::Loader.define_method :preloaded_records do
4
+ @preloaded_records ||= loaded.values
5
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ N1Loader::LoaderCollection.define_method :preloaded_records do
4
+ raise N1Loader::ActiveRecord::InvalidPreloading, "Cannot preload loader with arguments" if loader_class.arguments
5
+
6
+ with.preloaded_records
7
+ end
@@ -1,10 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Load core library
3
4
  require_relative "../n1_loader"
4
5
 
6
+ # Load integration dependency
5
7
  require "active_record"
6
8
 
9
+ module N1Loader
10
+ module ActiveRecord
11
+ class InvalidPreloading < N1Loader::Error; end
12
+ end
13
+ end
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
+
8
20
  case ActiveRecord::VERSION::MAJOR
9
21
  when 6
10
22
  require_relative "active_record/associations_preloader_v6"
@@ -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: parent_context.records.flat_map(&association_name),
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
  )
@@ -19,12 +19,15 @@ module N1Loader
19
19
  end
20
20
 
21
21
  def perform_preloading(association_name)
22
- N1Loader::Preloader.new(records).preload(association_name)
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
- AssociatedContextBuilder.prepare(
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
@@ -4,18 +4,18 @@ module N1Loader
4
4
  module ArLazyPreload
5
5
  module Loadable
6
6
  module ClassMethods # :nodoc:
7
- def n1_load(name, loader = nil, &block)
7
+ def n1_optimized(name, loader = nil, &block)
8
8
  name, loader_name, loader_variable_name = super
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
- instance_variable_set(loader_variable_name, self.class.send(loader_name).new([self]))
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 |elements|
11
- # elements.each_with_object({}) do |element, hash|
12
- # hash[element] = element.calculate_something
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.each_with_object({}) do |element, hash|
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, loader)
34
- send("#{name}_loader=", 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,15 +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, Metrics/AbcSize
48
+ def n1_optimized(name, loader = nil, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
51
49
  loader ||= Class.new(N1Loader::Loader) do
52
- if block && block.arity == 1
50
+ if block.arity == 1
53
51
  define_method(:perform, &block)
54
52
  else
55
53
  class_eval(&block)
56
54
  end
57
55
  end
58
-
59
56
  loader_name = "#{name}_loader"
60
57
  loader_variable_name = "@#{loader_name}"
61
58
 
@@ -64,21 +61,22 @@ module N1Loader
64
61
  end
65
62
 
66
63
  define_method("#{loader_name}_reload") do
67
- instance_variable_set(loader_variable_name, self.class.send(loader_name).new([self]))
64
+ instance_variable_set(loader_variable_name,
65
+ N1Loader::LoaderCollection.new(self.class.send(loader_name), [self]))
68
66
  end
69
67
 
70
- define_method("#{loader_name}=") do |loader_instance|
71
- instance_variable_set(loader_variable_name, loader_instance)
68
+ define_method("#{loader_name}=") do |loader_collection_instance|
69
+ instance_variable_set(loader_variable_name, loader_collection_instance)
72
70
  end
73
71
 
74
72
  define_method(loader_name) do
75
73
  instance_variable_get(loader_variable_name) || send("#{loader_name}_reload")
76
74
  end
77
75
 
78
- define_method(name) do |reload: false|
76
+ define_method(name) do |reload: false, **args|
79
77
  send("#{loader_name}_reload") if reload
80
78
 
81
- send(loader_name).for(self)
79
+ send(loader_name).with(**args).for(self)
82
80
  end
83
81
 
84
82
  [name, loader_name, loader_variable_name]
@@ -6,34 +6,112 @@ module N1Loader
6
6
  # Subclasses must define +perform+ method that accepts single argument
7
7
  # and returns hash where key is the element and value is what we want to load.
8
8
  class Loader
9
- def initialize(elements)
9
+ class << self
10
+ attr_reader :arguments
11
+
12
+ # Defines an argument that can be accessed within the loader.
13
+ #
14
+ # First defined argument will have the value of first passed argument,
15
+ # meaning the order is important.
16
+ #
17
+ # @param name [Symbol]
18
+ # @param opts [Hash]
19
+ # @option opts [Boolean] optional false by default
20
+ def argument(name, **opts)
21
+ @arguments ||= []
22
+
23
+ define_method(name) { args[name] }
24
+
25
+ @arguments << opts.merge(name: name)
26
+ end
27
+
28
+ # Defines a custom cache key that is calculated for passed arguments.
29
+ def cache_key(&block)
30
+ define_method(:cache_key) do
31
+ check_arguments!
32
+ instance_exec(&block)
33
+ end
34
+ end
35
+ end
36
+
37
+ def initialize(elements, **args)
10
38
  @elements = elements
39
+ @args = args
11
40
  end
12
41
 
13
- def perform(_elements)
14
- raise NotImplemented, "Subclasses have to implement the method"
42
+ def for(element)
43
+ if loaded.empty? && elements.any?
44
+ raise NotFilled, "Nothing was preloaded, perhaps you forgot to use fulfill method"
45
+ end
46
+ raise NotLoaded, "The data was not preloaded for the given element" unless loaded.key?(element)
47
+
48
+ loaded[element]
15
49
  end
16
50
 
17
- def loaded
18
- @loaded ||= if elements.size == 1 && respond_to?(:single)
19
- { elements.first => single(elements.first) }
20
- else
21
- perform(elements)
22
- end
51
+ def cache_key
52
+ check_arguments!
53
+ args.values.map(&:object_id)
54
+ end
55
+
56
+ private
57
+
58
+ attr_reader :elements, :args
59
+
60
+ def check_missing_arguments! # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
61
+ return unless (arguments = self.class.arguments)
62
+
63
+ min = arguments.count { |argument| !argument[:optional] }
64
+ max = arguments.count
65
+
66
+ return if args.size >= min && args.size <= max
67
+
68
+ str =
69
+ if min == max
70
+ max.to_s
71
+ else
72
+ "#{min}..#{max}"
73
+ end
74
+
75
+ raise MissingArgument, "Loader requires #{str} arguments but #{args.size} were given"
23
76
  end
24
77
 
25
- def preloaded_records
26
- @preloaded_records ||= loaded.values
78
+ def check_arguments!
79
+ check_missing_arguments!
80
+ check_invalid_arguments!
27
81
  end
28
82
 
29
- def for(element)
30
- raise NotLoaded, "The data was not preloaded for the given element" unless elements.include?(element)
83
+ def check_invalid_arguments!
84
+ return unless (arguments = self.class.arguments)
85
+
86
+ args.each_key do |arg|
87
+ next if arguments.find { |argument| argument[:name] == arg }
31
88
 
32
- loaded.compare_by_identity[element]
89
+ raise InvalidArgument, "Loader doesn't define #{arg} argument"
90
+ end
33
91
  end
34
92
 
35
- private
93
+ def perform(_elements)
94
+ raise NotImplemented, "Subclasses have to implement the method"
95
+ end
36
96
 
37
- attr_reader :elements
97
+ def fulfill(element, value)
98
+ @loaded[element] = value
99
+ end
100
+
101
+ def loaded
102
+ return @loaded if @loaded
103
+
104
+ check_arguments!
105
+
106
+ @loaded = {}.compare_by_identity
107
+
108
+ if elements.size == 1 && respond_to?(:single)
109
+ fulfill(elements.first, single(elements.first))
110
+ elsif elements.any?
111
+ perform(elements)
112
+ end
113
+
114
+ @loaded
115
+ end
38
116
  end
39
117
  end
@@ -0,0 +1,25 @@
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
+ loader = loader_class.new(elements, **args)
15
+
16
+ loaders[loader.cache_key] ||= loader
17
+ end
18
+
19
+ private
20
+
21
+ def loaders
22
+ @loaders ||= {}
23
+ end
24
+ end
25
+ 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
- loader = loader_class.new(grouped_elements)
23
- grouped_elements.each { |grouped_element| grouped_element.n1_loader_set(key, loader) }
24
- loader
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module N1Loader
4
- VERSION = "1.0.0"
4
+ VERSION = "1.4.0"
5
5
  end
data/lib/n1_loader.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require_relative "n1_loader/version"
4
4
 
5
5
  require_relative "n1_loader/core/loader"
6
+ require_relative "n1_loader/core/loader_collection"
6
7
  require_relative "n1_loader/core/loadable"
7
8
  require_relative "n1_loader/core/preloader"
8
9
 
@@ -10,4 +11,7 @@ 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
15
+ class MissingArgument < Error; end
16
+ class InvalidArgument < Error; end
13
17
  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 = "N+1 loader to solve the problem for good."
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"
@@ -23,7 +23,7 @@ Gem::Specification.new do |spec|
23
23
  spec.require_paths = ["lib"]
24
24
 
25
25
  spec.add_development_dependency "activerecord", ">= 5"
26
- spec.add_development_dependency "ar_lazy_preload", "~> 0.7"
26
+ spec.add_development_dependency "ar_lazy_preload", ">= 0.6"
27
27
  spec.add_development_dependency "db-query-matchers", "~> 0.10"
28
28
  spec.add_development_dependency "rails", ">= 5"
29
29
  spec.add_development_dependency "rspec", "~> 3.0"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: n1_loader
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.4.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: 2021-12-26 00:00:00.000000000 Z
11
+ date: 2022-02-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -28,16 +28,16 @@ dependencies:
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.7'
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.7'
40
+ version: '0.6'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: db-query-matchers
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -139,20 +139,28 @@ 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_5_latest.gemfile
145
- - gemfiles/ar_6_latest.gemfile
146
148
  - lib/n1_loader.rb
147
149
  - lib/n1_loader/active_record.rb
148
150
  - lib/n1_loader/active_record/associations_preloader_v5.rb
149
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
150
154
  - lib/n1_loader/ar_lazy_preload.rb
151
155
  - lib/n1_loader/ar_lazy_preload/associated_context_builder.rb
152
156
  - lib/n1_loader/ar_lazy_preload/context_adapter.rb
153
157
  - lib/n1_loader/ar_lazy_preload/loadable.rb
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
154
161
  - lib/n1_loader/core/loadable.rb
155
162
  - lib/n1_loader/core/loader.rb
163
+ - lib/n1_loader/core/loader_collection.rb
156
164
  - lib/n1_loader/core/preloader.rb
157
165
  - lib/n1_loader/version.rb
158
166
  - n1_loader.gemspec
@@ -181,5 +189,5 @@ requirements: []
181
189
  rubygems_version: 3.2.22
182
190
  signing_key:
183
191
  specification_version: 4
184
- summary: N+1 loader to solve the problem for good.
192
+ summary: Loader to solve N+1 issue for good.
185
193
  test_files: []