n1_loader 1.0.0 → 1.4.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 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: []