n1_loader 1.1.0 → 1.2.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: 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: []