n1_loader 1.5.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eef4b4cb71bedafe0820631695d26d1782f11d940899c8cdd2f745179197e489
4
- data.tar.gz: 909035eceab471c23057fc075a719f22ab8d2138c114c92500153cc19178f8c0
3
+ metadata.gz: 3d914dd09225515a579945ab616916bf1ad446101c5051c4cf1b72c46d38c107
4
+ data.tar.gz: a304c32e35294c5646fafe5c65a5de6c637e80e080d80ac16f957d77a3ebe4c6
5
5
  SHA512:
6
- metadata.gz: 5a47920e7df24aefbd413b8c2f2fdd6fd4e5918481109b8b79af01070d9c0f262784df0410ad6e758753405acaa7ffcc6a9dd2fc22eeab51cd5377885473b842
7
- data.tar.gz: 8139a30fc4b15740c403aeee3791e56a4268d1890d0cb4aedd4d9d29e9b54c26a1e98541b2865932a005abf7d854b7461c0de4c3322beb7e08124f97fb75a11c
6
+ metadata.gz: 738d8ca021bca48a41486689c74506af32dc2bca8a542398272edaa72aa57dd49247249d04afe8d9b834cc28cd9b725f0a9b1d8d7f5dd80176cfd3c9ca711f08
7
+ data.tar.gz: c73ddc81593e0cbd0450457143726b9d57fbce0a27c09ede70ea3f269816b7ae7374e5dc1c06dda4e8b0ee8c21a0c96248ed4afb9e3d7f26ec5b5ac46e038d16
@@ -0,0 +1,23 @@
1
+ name: Rubocop
2
+
3
+ on:
4
+ - pull_request
5
+
6
+ jobs:
7
+ tests:
8
+ runs-on: ubuntu-latest
9
+
10
+ steps:
11
+ - uses: actions/checkout@v3
12
+
13
+ - name: Set up Ruby
14
+ uses: ruby/setup-ruby@v1
15
+ with:
16
+ ruby-version: 2.7
17
+ bundler-cache: true
18
+
19
+ - name: Install dependencies
20
+ run: bundle install
21
+
22
+ - name: Run Rubocop
23
+ run: bundle exec rubocop
@@ -0,0 +1,62 @@
1
+ name: RSpec tests
2
+
3
+ on:
4
+ - pull_request
5
+
6
+ jobs:
7
+ tests:
8
+ runs-on: ubuntu-latest
9
+
10
+ strategy:
11
+ matrix:
12
+ ruby-version:
13
+ - '2.7'
14
+ - '3.0'
15
+ - 'head'
16
+ activerecord-gemfile:
17
+ - 'ar_5_latest'
18
+ - 'ar_6_latest'
19
+ - 'ar_7_latest'
20
+ ar_lazy_preload-gemfile:
21
+ - 'ar_lazy_preload_0.6.1'
22
+ - 'ar_lazy_preload_master'
23
+ exclude:
24
+ - ruby-version: 'head'
25
+ activerecord-gemfile: 'ar_5_latest'
26
+
27
+ - ruby-version: 'head'
28
+ activerecord-gemfile: 'ar_5_latest'
29
+
30
+ - ruby-version: '3.0'
31
+ activerecord-gemfile: 'ar_5_latest'
32
+
33
+ - ruby-version: '3.0'
34
+ activerecord-gemfile: 'ar_5_latest'
35
+
36
+ - activerecord-gemfile: 'ar_7_latest'
37
+ ar_lazy_preload-gemfile: 'ar_lazy_preload_0.6.1'
38
+
39
+ env:
40
+ ACTIVERECORD_GEMFILE: ${{ matrix.activerecord-gemfile }}
41
+ AR_LAZY_PRELOAD_GEMFILE: ${{ matrix.ar_lazy_preload-gemfile }}
42
+
43
+ steps:
44
+ - uses: actions/checkout@v3
45
+
46
+ - name: Set up Ruby ${{ matrix.ruby-version }}
47
+ uses: ruby/setup-ruby@v1
48
+ with:
49
+ ruby-version: ${{ matrix.ruby-version }}
50
+ bundler-cache: true
51
+
52
+ - name: Install dependencies
53
+ run: bundle install
54
+
55
+ - name: Run Core tests
56
+ run: bundle exec rspec spec/n1_loader_spec.rb
57
+
58
+ - name: Run ActiveRecord tests
59
+ run: bundle exec rspec spec/n1_loader_spec.rb spec/activerecord_spec.rb
60
+
61
+ - name: Run ArLazyPreload tests
62
+ run: bundle exec rspec spec/n1_loader_spec.rb spec/activerecord_spec.rb spec/ar_lazy_preload_spec.rb
data/.rubocop.yml CHANGED
@@ -2,6 +2,7 @@ AllCops:
2
2
  TargetRubyVersion: 2.5
3
3
  Exclude:
4
4
  - examples/**/*
5
+ - vendor/bundle/**/*
5
6
 
6
7
  Style/StringLiterals:
7
8
  Enabled: true
@@ -16,4 +17,4 @@ Layout/LineLength:
16
17
 
17
18
  Metrics/BlockLength:
18
19
  Exclude:
19
- - spec/**/*
20
+ - spec/**/*
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## [1.6.0] - 2022/10/24
2
+
3
+ - Add support of ArLazyPreload context for isolated loaders.
4
+
1
5
  ## [1.5.1] - 2022/09/20
2
6
 
3
7
  - Fix support of falsey value of arguments. Thanks [Aitor Lopez Beltran](https://github.com/aitorlb) for the [contribution](https://github.com/djezzzl/n1_loader/pull/23)!
data/README.md CHANGED
@@ -149,6 +149,17 @@ p User.all.includes(:payments_total).map { |user| user.payments_total(from: from
149
149
  - Has [integration](examples/active_record_integration.rb) with [ActiveRecord][5] which makes it brilliant
150
150
  - Has [integration](examples/ar_lazy_integration.rb) with [ArLazyPreload][6] which makes it excellent
151
151
 
152
+ ### Feature killer for [ArLazyPreload][6] integration with isolated loaders
153
+
154
+ In [version 1.6.0](CHANGELOG.md#160---20221019) isolated loaders were integrated with [ArLazyPreload][6] context.
155
+ This means, it isn't required to inject `N1Loader` into your [ActiveRecord][5] models to avoid N+1 issues out of the box.
156
+ It is especially great as many engineers are trying to avoid extra coupling between their models/services when it's possible.
157
+ And this feature was designed exactly for this without losing an out of a box solution for N+1.
158
+
159
+ Without further ado, please have a look at the [example](examples/ar_lazy_integration_with_isolated_loader.rb).
160
+
161
+ _Spoiler:_ as soon as you have your loader defined, it will be as simple as `Loader.for(element)` to get your data efficiently and without N+1.
162
+
152
163
  ## Funding
153
164
 
154
165
  ### Open Collective Backers
@@ -29,9 +29,8 @@ fill_database
29
29
  # Has N+1
30
30
  p User.all.map { |user| user.payments.sum(&:amount) }
31
31
 
32
- # Has no N+1 but we load too many data that we don't need
32
+ # Has no N+1 and loads only required data
33
33
  p User.preload_associations_lazily.map(&:payments_total)
34
-
35
- # Has no N+1 and calculation is the most efficient
34
+ # or
36
35
  ArLazyPreload.config.auto_preload = true
37
- User.all.map(&:payments_total)
36
+ User.all.map(&:payments_total)
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "n1_loader/ar_lazy_preload"
4
+
5
+ require_relative 'context/setup_ar_lazy'
6
+ require_relative 'context/setup_database'
7
+
8
+ class Loader < N1Loader::Loader
9
+ def perform(users)
10
+ total_per_user = Payment.group(:user_id).where(user: users).sum(:amount).tap { |h| h.default = 0 }
11
+
12
+ users.each do |user|
13
+ total = total_per_user[user.id]
14
+ fulfill(user, total)
15
+ end
16
+ end
17
+ end
18
+
19
+ class User < ActiveRecord::Base
20
+ has_many :payments
21
+ end
22
+
23
+ class Payment < ActiveRecord::Base
24
+ belongs_to :user
25
+
26
+ validates :amount, presence: true
27
+ end
28
+
29
+ fill_database
30
+
31
+ # Has N+1 and loads redundant data
32
+ p User.all.map { |user| user.payments.sum(&:amount) }
33
+
34
+ # Has no N+1 and loads only required data
35
+ p User.preload_associations_lazily.all.map { |user| Loader.for(user) }
36
+
37
+ # or
38
+ ArLazyPreload.config.auto_preload = true
39
+ p User.all.map { |user| Loader.for(user) }
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Returns cached N1Loader::LoaderCollection from context for a loader.
4
+ # In case there is none yet, saves passed block to a cache.
5
+ ArLazyPreload::Contexts::BaseContext.define_method :fetch_n1_loader_collection do |loader, &block|
6
+ (@n1_loader_collections ||= {})[loader] ||= block.call
7
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module N1Loader
4
4
  module ArLazyPreload
5
- # Context adapter for N1Loader
5
+ # Context adapter for injected N1Loader loaders.
6
6
  class ContextAdapter
7
7
  attr_reader :context
8
8
 
@@ -12,12 +12,15 @@ module N1Loader
12
12
  @context = context
13
13
  end
14
14
 
15
+ # Assign initialized preloader to +association_name+ in case it wasn't yet preloaded within the given context.
15
16
  def try_preload_lazily(association_name)
16
17
  return unless context&.send(:association_needs_preload?, association_name)
17
18
 
18
19
  perform_preloading(association_name)
19
20
  end
20
21
 
22
+ # Initialize preloader for +association_name+ with context builder callback.
23
+ # The callback will be executed when on records load.
21
24
  def perform_preloading(association_name)
22
25
  context_setup = lambda { |records|
23
26
  AssociatedContextBuilder.prepare(
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Raised when a single object without ArLazyPreload context was passed to an isolated loader.
4
+ N1Loader::Loader::MissingArLazyPreloadContext = Class.new(StandardError)
5
+
6
+ # Defines a singleton method method that allows isolated loaders
7
+ # to use ArLazyPreload context without passing sibling records.
8
+ N1Loader::Loader.define_singleton_method(:for) do |element, **args|
9
+ # It is required to have an ArLazyPreload context defined
10
+ if !element.respond_to?(:lazy_preload_context) || element.lazy_preload_context.nil?
11
+ raise N1Loader::Loader::MissingArLazyPreloadContext
12
+ end
13
+
14
+ # Fetch or initialize loader from ArLazyPreload context
15
+ loader_collection = element.lazy_preload_context.fetch_n1_loader_collection(self) do
16
+ context_setup = lambda { |records|
17
+ N1Loader::ArLazyPreload::AssociatedContextBuilder.prepare(
18
+ parent_context: element.lazy_preload_context,
19
+ association_name: "cached_n1_loader_collection_#{self}".downcase.to_sym,
20
+ records: records
21
+ )
22
+ }
23
+
24
+ N1Loader::LoaderCollection.new(self, element.lazy_preload_context.records).tap do |collection|
25
+ collection.context_setup = context_setup
26
+ end
27
+ end
28
+
29
+ # Fetch value from loader
30
+ loader_collection.with(**args).for(element)
31
+ end
@@ -14,6 +14,8 @@ require_relative "ar_lazy_preload/associated_context_builder"
14
14
  require_relative "ar_lazy_preload/loader_collection_patch"
15
15
  require_relative "ar_lazy_preload/preloader_patch"
16
16
  require_relative "ar_lazy_preload/loader_patch"
17
+ require_relative "ar_lazy_preload/loader"
18
+ require_relative "ar_lazy_preload/context"
17
19
 
18
20
  N1Loader::Loadable::ClassMethods.prepend(N1Loader::ArLazyPreload::Loadable::ClassMethods)
19
21
  N1Loader::Preloader.prepend(N1Loader::ArLazyPreload::PreloaderPatch)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module N1Loader
4
- VERSION = "1.5.1"
4
+ VERSION = "1.6.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: n1_loader
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.1
4
+ version: 1.6.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: 2022-09-20 00:00:00.000000000 Z
11
+ date: 2022-10-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -144,6 +144,8 @@ extensions: []
144
144
  extra_rdoc_files: []
145
145
  files:
146
146
  - ".circleci/config.yml"
147
+ - ".github/workflows/rubocop.yml"
148
+ - ".github/workflows/tests.yml"
147
149
  - ".gitignore"
148
150
  - ".rspec"
149
151
  - ".rubocop.yml"
@@ -162,6 +164,7 @@ files:
162
164
  - bin/setup
163
165
  - examples/active_record_integration.rb
164
166
  - examples/ar_lazy_integration.rb
167
+ - examples/ar_lazy_integration_with_isolated_loader.rb
165
168
  - examples/arguments_support.rb
166
169
  - examples/context/service.rb
167
170
  - examples/context/setup_ar_lazy.rb
@@ -184,8 +187,10 @@ files:
184
187
  - lib/n1_loader/active_record/loader_collection.rb
185
188
  - lib/n1_loader/ar_lazy_preload.rb
186
189
  - lib/n1_loader/ar_lazy_preload/associated_context_builder.rb
190
+ - lib/n1_loader/ar_lazy_preload/context.rb
187
191
  - lib/n1_loader/ar_lazy_preload/context_adapter.rb
188
192
  - lib/n1_loader/ar_lazy_preload/loadable.rb
193
+ - lib/n1_loader/ar_lazy_preload/loader.rb
189
194
  - lib/n1_loader/ar_lazy_preload/loader_collection_patch.rb
190
195
  - lib/n1_loader/ar_lazy_preload/loader_patch.rb
191
196
  - lib/n1_loader/ar_lazy_preload/preloader_patch.rb
@@ -217,7 +222,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
217
222
  - !ruby/object:Gem::Version
218
223
  version: '0'
219
224
  requirements: []
220
- rubygems_version: 3.2.22
225
+ rubygems_version: 3.2.33
221
226
  signing_key:
222
227
  specification_version: 4
223
228
  summary: Loader to solve N+1 issue for good.