n1_loader 0.1.1 → 1.2.0

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