n1_loader 1.5.1 → 1.6.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: 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.