n1_loader 0.1.2 → 1.3.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: be50b582e5f3bc33bb085b9ceefb620f3c1c49b28618e953103bf47ff343ffe6
4
- data.tar.gz: bc9008a74662754eb20a798dd1969a59551f0127b6484561420335276980acf5
3
+ metadata.gz: cc47894a27d425f70a38dc43b9b40053d823f4815fe8e135a7cb1cf5afbde0f3
4
+ data.tar.gz: 132d5851c19763db2f1910ebeeb6a368437aba6d3340c2c69ad2ffe348791011
5
5
  SHA512:
6
- metadata.gz: 0bef5f775c2ac8d31ad828d8b6ef55f7a9915eb19e8f0e2169a8196cf3ed71bd5153b4bbdb36e664b5994a9a0e47b196905772470accfafcd803438d83de43c7
7
- data.tar.gz: d609dab38a23c3d0e87da754345477e5c1eaa00dcab59435b6015dfe010a1fd49f833139aa0610bc318fa467396a654b8134b3a1d2399267e85b090c22b9f995
6
+ metadata.gz: 945258069b57bc7cd0fe26416411322886d734db0ed4eead1c88a3acced70bdb14ffc92eb9403e5d346c0853d64dc09e5afb8039a9026c9b4c09a1cc6f5fc9e8
7
+ data.tar.gz: 036e5a8a4b2d63bed2aee558164fdd042ac8c0c68b0ecdb1d5ec9f64231ca9f6841dc56cd6cdc63b87fdb9bf964e4f42089f89d9783cf2513cf81243a473150a
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,23 @@
1
- ## [Unreleased]
1
+ ## [1.3.0] - 2022-02-22
2
+
3
+ - add support of named arguments with `argument <name>`
4
+
5
+ BREAKING CHANGES:
6
+ - rename `n1_load` to `n1_optimized`
7
+ - rework `def self.arguments_key` to `cache_key`
8
+
9
+ ## [1.2.0] - 2022-01-14
10
+
11
+ - Introduce arguments support.
12
+
13
+ ## [1.1.0] - 2021-12-27
14
+
15
+ - Introduce `fulfill` method to abstract the storage.
16
+
17
+ ## [1.0.0] - 2021-12-26
18
+
19
+ - Various of great features.
2
20
 
3
21
  ## [0.1.0] - 2021-12-16
4
22
 
5
- - Initial release
23
+ - 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,59 +30,221 @@ 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_optimized :orders_count do |users|
49
+ orders_per_user = Order.where(user: users).group(:user_id).count
50
+
51
+ users.each { |user| fulfill(user, orders_per_user[user.id]) }
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
70
+
71
+ # with inline loader
72
+ n1_optimized :orders_count do |users|
73
+ orders_per_user = Order.where(user: users).group(:user_id).count
47
74
 
48
- # with custom loader
49
- n1_loader :something, MyLoader
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_optimized :orders_count, OrdersCountLoader
103
+ end
104
+
105
+ class Customer
106
+ include N1Loader::Loadable
107
+
108
+ n1_optimized :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_optimized :orders_count do |users|
123
+ orders_per_user = Order.where(user: users).group(:user_id).count
124
+
125
+ users.each { |user| fulfill(user, orders_per_user[user.id]) }
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_optimized :orders_count do # no arguments passed to the block, so we can override both perform and single.
164
+ def perform(users)
165
+ orders_per_user = Order.where(user: users).group(:user_id).count
166
+
167
+ users.each { |user| fulfill(user, orders_per_user[user.id]) }
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
68
183
  ```
69
184
 
185
+ ### Arguments
186
+
187
+ ```ruby
188
+ class User
189
+ include N1Loader::Loadable
190
+
191
+ n1_optimized :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_optimized :orders_count do
216
+ argument :sale
217
+
218
+ cache_key { sale.id }
219
+
220
+ def perform(users, sale)
221
+ orders_per_user = Order.where(sale: sale, user: users).group(:user_id).count
222
+
223
+ users.each { |user| fulfill(user, orders_per_user[user.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
 
238
+ _Note_: Rails 7 support is coming soon! Stay tuned!
239
+
72
240
  ```ruby
73
241
  class User < ActiveRecord::Base
74
242
  include N1Loader::Loadable
75
-
76
- 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
+
244
+ n1_optimized :orders_count do |users|
245
+ orders_per_user = Order.where(user: users).group(:user_id).count
246
+
247
+ users.each { |user| fulfill(user, orders_per_user[user.id]) }
81
248
  end
82
249
  end
83
250
 
@@ -101,12 +268,11 @@ users.map(&:orders_count)
101
268
  ```ruby
102
269
  class User < ActiveRecord::Base
103
270
  include N1Loader::Loadable
104
-
105
- 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
+
272
+ n1_optimized :orders_count do |users|
273
+ orders_per_user = Order.where(user: users).group(:user_id).count
274
+
275
+ users.each { |user| fulfill(user, orders_per_user[user.id]) }
110
276
  end
111
277
  end
112
278
 
@@ -123,12 +289,6 @@ ArLazyPreload.config.auto_preload = true
123
289
  User.all.map(:orders_count)
124
290
  ```
125
291
 
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
292
  ## Contributing
133
293
 
134
294
  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,11 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Load core library
3
4
  require_relative "../n1_loader"
4
5
 
6
+ # Load integration dependency
5
7
  require "active_record"
6
8
 
7
- require_relative "active_record/associations_preloader"
9
+ module N1Loader
10
+ module ActiveRecord
11
+ class InvalidPreloading < N1Loader::Error; end
12
+ end
13
+ end
8
14
 
15
+ # Library integration
9
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
+
10
27
  ActiveRecord::Associations::Preloader.prepend(N1Loader::ActiveRecord::Associations::Preloader)
11
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
@@ -4,18 +4,18 @@ module N1Loader
4
4
  module ArLazyPreload
5
5
  module Loadable
6
6
  module ClassMethods # :nodoc:
7
- def n1_load(name, loader = nil, &block)
7
+ def n1_optimized(name, loader = nil, &block)
8
8
  name, loader_name, loader_variable_name = super
9
9
 
10
10
  define_method(loader_name) do
11
11
  loader = instance_variable_get(loader_variable_name)
12
-
13
12
  return loader if loader
13
+
14
14
  if respond_to?(:lazy_preload_context) && ContextAdapter.new(lazy_preload_context).try_preload_lazily(name)
15
15
  return instance_variable_get(loader_variable_name)
16
16
  end
17
17
 
18
- instance_variable_set(loader_variable_name, self.class.send(loader_name).new([self]))
18
+ send("#{loader_name}_reload")
19
19
  end
20
20
  end
21
21
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module N1Loader
4
+ module ArLazyPreload
5
+ # A patch to {N1Loader::LoaderCollection} to setup lazy context lazily.
6
+ module LoaderCollectionPatch
7
+ attr_accessor :context_setup
8
+
9
+ def with(*args)
10
+ result = super
11
+
12
+ result.context_setup = context_setup if context_setup && result.context_setup.nil?
13
+
14
+ result
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module N1Loader
4
+ module ArLazyPreload
5
+ # A patch to {N1Loader::Loader} to setup lazy context lazily.
6
+ module LoaderPatch
7
+ attr_accessor :context_setup
8
+
9
+ def loaded
10
+ return @loaded if @loaded
11
+
12
+ super
13
+
14
+ context_setup&.call(preloaded_records)
15
+
16
+ @loaded
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module N1Loader
4
+ module ArLazyPreload
5
+ # A patch to {N1Loader::Preloader} setup lazy context lazily.
6
+ module PreloaderPatch
7
+ def initialize(elements, context_setup = nil)
8
+ super(elements)
9
+ @context_setup = context_setup
10
+ end
11
+
12
+ def preload(*keys)
13
+ super.each do |loader_collection|
14
+ loader_collection.context_setup = context_setup
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :context_setup
21
+ end
22
+ end
23
+ end
@@ -1,12 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Load core library
3
4
  require_relative "active_record"
4
5
 
6
+ # Load integration dependency
5
7
  require "rails"
6
8
  require "ar_lazy_preload"
7
9
 
10
+ # Library integration
8
11
  require_relative "ar_lazy_preload/loadable"
9
12
  require_relative "ar_lazy_preload/context_adapter"
10
13
  require_relative "ar_lazy_preload/associated_context_builder"
14
+ require_relative "ar_lazy_preload/loader_collection_patch"
15
+ require_relative "ar_lazy_preload/preloader_patch"
16
+ require_relative "ar_lazy_preload/loader_patch"
11
17
 
12
18
  N1Loader::Loadable::ClassMethods.prepend(N1Loader::ArLazyPreload::Loadable::ClassMethods)
19
+ N1Loader::Preloader.prepend(N1Loader::ArLazyPreload::PreloaderPatch)
20
+ N1Loader::Loader.prepend(N1Loader::ArLazyPreload::LoaderPatch)
21
+ N1Loader::LoaderCollection.prepend(N1Loader::ArLazyPreload::LoaderCollectionPatch)
@@ -7,9 +7,9 @@ module N1Loader
7
7
  # include N1Loader::Loadable
8
8
  #
9
9
  # # with inline loader
10
- # n1_loader :something do |elements|
11
- # elements.each_with_object({}) do |element, hash|
12
- # hash[element] = element.calculate_something
10
+ # n1_loader :something do
11
+ # def perform(elements)
12
+ # elements.each { |element| fulfill(element,, element.calculate_something) }
13
13
  # end
14
14
  # end
15
15
  #
@@ -20,9 +20,7 @@ module N1Loader
20
20
  # # custom loader
21
21
  # class MyLoader < N1Loader::Loader
22
22
  # def perform(elements)
23
- # elements.each_with_object({}) do |element, hash|
24
- # hash[element] = element.calculate_something
25
- # end
23
+ # elements.each { |element| fulfill(element,, element.calculate_something) }
26
24
  # end
27
25
  # end
28
26
  module Loadable
@@ -30,8 +28,8 @@ module N1Loader
30
28
  send("#{name}_loader")
31
29
  end
32
30
 
33
- def n1_loader_set(name, loader)
34
- send("#{name}_loader=", loader)
31
+ def n1_loader_set(name, loader_collection)
32
+ send("#{name}_loader=", loader_collection)
35
33
  end
36
34
 
37
35
  def self.included(base)
@@ -47,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_optimized(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]
@@ -6,30 +6,80 @@ module N1Loader
6
6
  # Subclasses must define +perform+ method that accepts single argument
7
7
  # and returns hash where key is the element and value is what we want to load.
8
8
  class Loader
9
- def initialize(elements)
9
+ class << self
10
+ attr_reader :arguments
11
+
12
+ # Defines an argument that can be accessed within the loader.
13
+ #
14
+ # First defined argument will have the value of first passed argument,
15
+ # meaning the order is important.
16
+ def argument(name)
17
+ @arguments ||= []
18
+ index = @arguments.size
19
+ define_method(name) { args[index] }
20
+ @arguments << name
21
+ end
22
+
23
+ # Defines a custom cache key that is calculated for passed arguments.
24
+ def cache_key(&block)
25
+ define_method(:cache_key) do
26
+ check_arguments!
27
+ instance_exec(&block)
28
+ end
29
+ end
30
+ end
31
+
32
+ def initialize(elements, *args)
10
33
  @elements = elements
34
+ @args = args
11
35
  end
12
36
 
13
- def perform(_elements)
14
- raise NotImplemented, "Subclasses have to implement the method"
37
+ def for(element)
38
+ if loaded.empty? && elements.any?
39
+ raise NotFilled, "Nothing was preloaded, perhaps you forgot to use fulfill method"
40
+ end
41
+ raise NotLoaded, "The data was not preloaded for the given element" unless loaded.key?(element)
42
+
43
+ loaded[element]
15
44
  end
16
45
 
17
- def loaded
18
- @loaded ||= perform(elements)
46
+ def cache_key
47
+ args.map(&:object_id)
19
48
  end
20
49
 
21
- def preloaded_records
22
- @preloaded_records ||= loaded.values
50
+ private
51
+
52
+ attr_reader :elements, :args
53
+
54
+ def check_arguments!
55
+ return unless (required = self.class.arguments)
56
+ return if required.size == args.size
57
+
58
+ raise MissingArgument, "Loader defined #{required.size} arguments but #{args.size} were given"
23
59
  end
24
60
 
25
- def for(element)
26
- raise NotLoaded, "The data was not preloaded for the given element" unless elements.include?(element)
61
+ def perform(_elements)
62
+ raise NotImplemented, "Subclasses have to implement the method"
63
+ end
27
64
 
28
- loaded.compare_by_identity[element]
65
+ def fulfill(element, value)
66
+ @loaded[element] = value
29
67
  end
30
68
 
31
- private
69
+ def loaded # rubocop:disable Metrics/AbcSize
70
+ return @loaded if @loaded
71
+
72
+ check_arguments!
73
+
74
+ @loaded = {}.compare_by_identity
32
75
 
33
- attr_reader :elements
76
+ if elements.size == 1 && respond_to?(:single)
77
+ fulfill(elements.first, single(elements.first, *args))
78
+ elsif elements.any?
79
+ perform(elements, *args)
80
+ end
81
+
82
+ @loaded
83
+ end
34
84
  end
35
85
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module N1Loader
4
+ # The class is used for storing collections of loaders for elements per set of arguments.
5
+ class LoaderCollection
6
+ attr_reader :loader_class, :elements
7
+
8
+ def initialize(loader_class, elements)
9
+ @loader_class = loader_class
10
+ @elements = elements
11
+ end
12
+
13
+ def with(*args)
14
+ loader = loader_class.new(elements, *args)
15
+
16
+ loaders[loader.cache_key] ||= loader
17
+ end
18
+
19
+ private
20
+
21
+ def loaders
22
+ @loaders ||= {}
23
+ end
24
+ end
25
+ end
@@ -19,9 +19,9 @@ module N1Loader
19
19
  elements
20
20
  .group_by { |element| element.class.n1_loader(key) }
21
21
  .map do |loader_class, grouped_elements|
22
- loader = loader_class.new(grouped_elements)
23
- grouped_elements.each { |grouped_element| grouped_element.n1_loader_set(key, loader) }
24
- loader
22
+ loader_collection = N1Loader::LoaderCollection.new(loader_class, grouped_elements)
23
+ grouped_elements.each { |grouped_element| grouped_element.n1_loader_set(key, loader_collection) }
24
+ loader_collection
25
25
  end
26
26
  end
27
27
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module N1Loader
4
- VERSION = "0.1.2"
4
+ VERSION = "1.3.0"
5
5
  end
data/lib/n1_loader.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require_relative "n1_loader/version"
4
4
 
5
5
  require_relative "n1_loader/core/loader"
6
+ require_relative "n1_loader/core/loader_collection"
6
7
  require_relative "n1_loader/core/loadable"
7
8
  require_relative "n1_loader/core/preloader"
8
9
 
@@ -10,4 +11,6 @@ module N1Loader # :nodoc:
10
11
  class Error < StandardError; end
11
12
  class NotImplemented < Error; end
12
13
  class NotLoaded < Error; end
14
+ class NotFilled < Error; end
15
+ class MissingArgument < Error; end
13
16
  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.2
4
+ version: 1.3.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-02-22 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,18 +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_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
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
152
161
  - lib/n1_loader/core/loadable.rb
153
162
  - lib/n1_loader/core/loader.rb
163
+ - lib/n1_loader/core/loader_collection.rb
154
164
  - lib/n1_loader/core/preloader.rb
155
165
  - lib/n1_loader/version.rb
156
166
  - n1_loader.gemspec
@@ -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: []