n1_loader 1.1.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8a365bb6b58a962185cd24033f6dd6cb98510712973dacdea4977a3121b64adc
4
- data.tar.gz: d06031503475dc13f52acddb881931de5b7dd244acab6b46553d4a73c8efbd2d
3
+ metadata.gz: 8b9b61121f94d9dba30e969c1b9a3fa9c812292d618434a98262c90aef43e6cd
4
+ data.tar.gz: a1ecf5359a1a53200e354b30289566a437cf372c54ccf3f13c2e0adc2cb71640
5
5
  SHA512:
6
- metadata.gz: 9172c4cd233739a0ebc379b3988037349d028020d5eb066642532f2b3da2a75d3faa34edd872aa5b5925eed91509c2c3a202e7a9aa448ca5020435b4f5e9c5a5
7
- data.tar.gz: e74f92e69f47909b812adecb1aa7dc319c090a2ad865e89732a4ebdbbd378c02dbec28cc7ff5b0ff42262acf3a07e8deb8a4a949634d70c6d6c3986837a6ab5d
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
- - 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,7 @@
1
+ ## [1.2.0]
2
+
3
+ - Introduce arguments support.
4
+
1
5
  ## [1.1.0] - 2021-12-27
2
6
 
3
7
  - Introduce `fulfill` method to abstract the storage.
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,119 +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
- elements.each { |element| fulfill(element, [element]) }
51
- end
52
-
53
- # with custom loader
54
- n1_loader :something, MyLoader
55
- end
56
-
57
- # Custom loader that can be shared with many classes
58
- class MyLoader < N1Loader::Loader
59
- def perform(elements)
60
- elements.each { |element| fulfill(element, [element]) }
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]) }
61
52
  end
62
53
  end
63
54
 
64
55
  # For single object
65
- ex = Example.new
66
- ex.anything
56
+ user = User.new
57
+ user.orders_count
67
58
 
68
59
  # For multiple objects without N+1
69
- objects = [Example.new, Example.new]
70
- N1Loader::Preloader.new(objects).preload(:anything)
71
- objects.map(&:anything)
60
+ users = [User.new, User.new]
61
+ N1Loader::Preloader.new(users).preload(:orders_count)
62
+ users.map(&:orders_count)
72
63
  ```
73
64
 
74
65
  ### Lazy loading
75
66
 
76
67
  ```ruby
77
- class Example
68
+ class User
78
69
  include N1Loader::Loadable
79
-
80
- n1_loader :anything do |elements|
81
- elements.each { |element| fulfill(element, [element]) }
70
+
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]) }
82
76
  end
83
77
  end
84
78
 
85
- object = Example.new # => nothing was done for loading
86
- object.anything # => first time loading
79
+ user = User.new # => nothing was done for loading
80
+ user.orders_count # => first time loading
87
81
 
88
- objects = [Example.new, Example.new] # => nothing was done for loading
89
- N1Loader::Preloader.new([objects]).preload(:anything) # => we only initial loader but didn't perform it yet
90
- 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)
91
85
  ```
92
86
 
93
87
 
94
88
  ### Shareable loaders
95
89
 
96
90
  ```ruby
97
- class MyLoader < N1Loader::Loader
98
- def perform(elements)
99
- elements.each { |element| fulfill(element, [element]) }
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]) }
100
96
  end
101
97
  end
102
98
 
103
- class A
99
+ class User
104
100
  include N1Loader::Loadable
105
101
 
106
- n1_loader :anything, MyLoader
102
+ n1_loader :orders_count, OrdersCountLoader
107
103
  end
108
104
 
109
- class B
105
+ class Customer
110
106
  include N1Loader::Loadable
111
107
 
112
- n1_loader :something, MyLoader
108
+ n1_loader :orders_count, OrdersCountLoader
113
109
  end
114
110
 
115
- A.new.anything # => works
116
- B.new.something # => works
111
+ User.new.orders_count # => works
112
+ Customer.new.orders_count # => works
117
113
  ```
118
114
 
119
115
  ### Reloading
120
116
 
121
117
  ```ruby
122
- class Example
118
+ class User
123
119
  include N1Loader::Loadable
124
120
 
125
121
  # with inline loader
126
- n1_loader :anything do |elements|
127
- elements.each { |element| fulfill(element, [element]) }
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]) }
128
126
  end
129
127
  end
130
128
 
131
- object = Example.new
132
- object.anything # => loader is executed first time and value was cached
133
- object.anything(reload: true) # => loader is executed again and a new value was cached
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
134
132
 
135
- objects = [Example.new, Example.new]
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
136
 
137
- N1Loader::Preloader.new(objects).preload(:anything) # => loader was initialized but not yet executed
138
- objects.map(&:anything) # => loader was executed first time without N+1 issue and values were cached
139
-
140
- N1Loader::Preloader.new(objects).preload(:anything) # => loader was initialized again but not yet executed
141
- 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
142
139
  ```
143
140
 
144
141
  ### Isolated loaders
145
142
 
146
143
  ```ruby
147
- class MyLoader < N1Loader::Loader
144
+ class IsolatedLoader < N1Loader::Loader
148
145
  def perform(elements)
149
146
  elements.each { |element| fulfill(element, [element]) }
150
147
  end
151
148
  end
152
149
 
153
150
  objects = [1, 2, 3, 4]
154
- loader = MyLoader.new(objects)
151
+ loader = IsolatedLoader.new(objects)
155
152
  objects.each do |object|
156
153
  loader.for(object) # => it has no N+1 and it doesn't require to be injected in the class
157
154
  end
@@ -160,46 +157,92 @@ end
160
157
  ### Optimized single case
161
158
 
162
159
  ```ruby
163
- class Example
160
+ class User
164
161
  include N1Loader::Loadable
165
162
 
166
- n1_loader :something do # no arguments passed to the block, so we can override both perform and single.
167
- def perform(elements)
168
- elements.each { |element| fulfill(element, [element]) }
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]) }
169
168
  end
170
169
 
171
170
  # Optimized for single object loading
172
- def single(element)
173
- # Just return a value you want to have for this element
174
- [element]
171
+ def single(user)
172
+ user.orders.count
175
173
  end
176
174
  end
177
175
  end
178
176
 
179
- object = Example.new
180
- object.something # single will be used here
177
+ user = User.new
178
+ user.orders_count # single will be used here
181
179
 
182
- objects = [Example.new, Example.new]
183
- N1Loader::Preloader.new(objects).preload(:something)
184
- objects.map(&:something) # perform will be used once without N+1
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
185
183
  ```
186
184
 
187
- ## Integrations
185
+ ### Arguments
188
186
 
189
- ### [ActiveRecord][5]
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
206
+ ```
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
+ ```
190
232
 
191
- **Supported versions**: 5, 6.
192
233
 
193
- _Note_: Please open an issue if you interested in support of version 7 or other.
234
+ ## Integrations
235
+
236
+ ### [ActiveRecord][5]
194
237
 
195
238
  ```ruby
196
239
  class User < ActiveRecord::Base
197
240
  include N1Loader::Loadable
198
241
 
199
242
  n1_loader :orders_count do |users|
200
- hash = Order.where(user: users).group(:user_id).count
201
-
202
- users.each { |user| fulfill(user, hash[user.id]) }
243
+ orders_per_user = Order.where(user: users).group(:user_id).count
244
+
245
+ users.each { |user| fulfill(user, orders_per_user[user.id]) }
203
246
  end
204
247
  end
205
248
 
@@ -225,9 +268,9 @@ class User < ActiveRecord::Base
225
268
  include N1Loader::Loadable
226
269
 
227
270
  n1_loader :orders_count do |users|
228
- hash = Order.where(user: users).group(:user_id).count
271
+ orders_per_user = Order.where(user: users).group(:user_id).count
229
272
 
230
- users.each { |user| fulfill(user, hash[user.id]) }
273
+ users.each { |user| fulfill(user, orders_per_user[user.id]) }
231
274
  end
232
275
  end
233
276
 
@@ -244,12 +287,6 @@ ArLazyPreload.config.auto_preload = true
244
287
  User.all.map(:orders_count)
245
288
  ```
246
289
 
247
- ## Development
248
-
249
- 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.
250
-
251
- 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).
252
-
253
290
  ## Contributing
254
291
 
255
292
  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,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,10 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Load core library
3
4
  require_relative "../n1_loader"
4
- require_relative "active_record/loader"
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
@@ -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
- 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,8 +7,10 @@ module N1Loader
7
7
  # include N1Loader::Loadable
8
8
  #
9
9
  # # with inline loader
10
- # n1_loader :something do |elements|
11
- # elements.each { |element| fulfill(element,, element.calculate_something) }
10
+ # n1_loader :something do
11
+ # def perform(elements)
12
+ # elements.each { |element| fulfill(element,, element.calculate_something) }
13
+ # end
12
14
  # end
13
15
  #
14
16
  # # with custom loader
@@ -26,8 +28,8 @@ module N1Loader
26
28
  send("#{name}_loader")
27
29
  end
28
30
 
29
- def n1_loader_set(name, loader)
30
- send("#{name}_loader=", loader)
31
+ def n1_loader_set(name, loader_collection)
32
+ send("#{name}_loader=", loader_collection)
31
33
  end
32
34
 
33
35
  def self.included(base)
@@ -45,13 +47,12 @@ module N1Loader
45
47
 
46
48
  def n1_load(name, loader = nil, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
47
49
  loader ||= Class.new(N1Loader::Loader) do
48
- if block && block.arity == 1
50
+ if block&.arity&.positive?
49
51
  define_method(:perform, &block)
50
52
  else
51
53
  class_eval(&block)
52
54
  end
53
55
  end
54
-
55
56
  loader_name = "#{name}_loader"
56
57
  loader_variable_name = "@#{loader_name}"
57
58
 
@@ -60,21 +61,22 @@ module N1Loader
60
61
  end
61
62
 
62
63
  define_method("#{loader_name}_reload") do
63
- 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]))
64
66
  end
65
67
 
66
- define_method("#{loader_name}=") do |loader_instance|
67
- 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)
68
70
  end
69
71
 
70
72
  define_method(loader_name) do
71
73
  instance_variable_get(loader_variable_name) || send("#{loader_name}_reload")
72
74
  end
73
75
 
74
- define_method(name) do |reload: false|
76
+ define_method(name) do |*args, reload: false|
75
77
  send("#{loader_name}_reload") if reload
76
78
 
77
- send(loader_name).for(self)
79
+ send(loader_name).with(*args).for(self)
78
80
  end
79
81
 
80
82
  [name, loader_name, loader_variable_name]
@@ -6,8 +6,13 @@ 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
+ def self.arguments_key(*args)
10
+ args.map(&:object_id)
11
+ end
12
+
13
+ def initialize(elements, *args)
10
14
  @elements = elements
15
+ @args = args
11
16
  end
12
17
 
13
18
  def for(element)
@@ -21,7 +26,7 @@ module N1Loader
21
26
 
22
27
  private
23
28
 
24
- attr_reader :elements
29
+ attr_reader :elements, :args
25
30
 
26
31
  def perform(_elements)
27
32
  raise NotImplemented, "Subclasses have to implement the method"
@@ -37,9 +42,9 @@ module N1Loader
37
42
  @loaded = {}.compare_by_identity
38
43
 
39
44
  if elements.size == 1 && respond_to?(:single)
40
- fulfill(elements.first, single(elements.first))
45
+ fulfill(elements.first, single(elements.first, *args))
41
46
  elsif elements.any?
42
- perform(elements)
47
+ perform(elements, *args)
43
48
  end
44
49
 
45
50
  @loaded
@@ -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
- 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.1.0"
4
+ VERSION = "1.2.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
 
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.1.0
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: 2021-12-27 00:00:00.000000000 Z
11
+ date: 2022-01-14 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,21 +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
150
152
  - lib/n1_loader/active_record/loader.rb
153
+ - lib/n1_loader/active_record/loader_collection.rb
151
154
  - lib/n1_loader/ar_lazy_preload.rb
152
155
  - lib/n1_loader/ar_lazy_preload/associated_context_builder.rb
153
156
  - lib/n1_loader/ar_lazy_preload/context_adapter.rb
154
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
155
161
  - lib/n1_loader/core/loadable.rb
156
162
  - lib/n1_loader/core/loader.rb
163
+ - lib/n1_loader/core/loader_collection.rb
157
164
  - lib/n1_loader/core/preloader.rb
158
165
  - lib/n1_loader/version.rb
159
166
  - n1_loader.gemspec
@@ -182,5 +189,5 @@ requirements: []
182
189
  rubygems_version: 3.2.22
183
190
  signing_key:
184
191
  specification_version: 4
185
- summary: N+1 loader to solve the problem for good.
192
+ summary: Loader to solve N+1 issue for good.
186
193
  test_files: []