n1_loader 0.1.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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