ar_lazy_preload 0.6.0 → 1.0.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: 3d54ffe605f60682678626dc9fb88be7490cccc9d9d8e9d012e2118c97806531
4
- data.tar.gz: d88d6afa1cd40385645771b6b8117df04e57af4753ab062c92a0dbe231836dc9
3
+ metadata.gz: 853f4f5f10ba5f9aa5e6530eb767f9d4063d62745b2b332776c7ebb44c753700
4
+ data.tar.gz: ff2f6a626e8e944734dcab46eef9677bda77582027f1e687b27b616a5cdac0cb
5
5
  SHA512:
6
- metadata.gz: b0865957982e3b35ab825b0a800c391a27989026865ca8efcd7c4b2f664e4054b2dcd92926ee3ed75ca81f2e4bc2d38fb61aa75f635f72256554fffd944eef93
7
- data.tar.gz: b06ed1f0e44254b1d2e4660f0aa63e257c1dba54383e14d480c7852d92f3912a356e67f236f3e1107621db1a3b80a8abaf8ebbf31413f50f870ceb8c586aadfe
6
+ metadata.gz: f873a03ba3abc319042f601e512b48b64d638f3b6358e722768e92745992c654f4aef229e792690c0c250563c9f28ae2d69fd77931c9af27db9a09f719f6341f
7
+ data.tar.gz: 521c1bf8d19bc8bc2c5e71c8fd2918ac400102368016a9d6448b19426bcefc569b51fda0e13b205d86a872553f08a619a436fb3c7821bb4b866056c771065121
data/README.md CHANGED
@@ -1,9 +1,9 @@
1
- # ArLazyPreload [![Cult Of Martians](http://cultofmartians.com/assets/badges/badge.svg)](https://cultofmartians.com/tasks/activerecord-lazy-preload.html) [![Gem Version](https://badge.fury.io/rb/ar_lazy_preload.svg)](https://rubygems.org/gems/ar_lazy_preload) [![Build Status](https://travis-ci.org/DmitryTsepelev/ar_lazy_preload.svg?branch=master)](https://travis-ci.org/DmitryTsepelev/ar_lazy_preload) [![Maintainability](https://api.codeclimate.com/v1/badges/00d04595661820dfba80/maintainability)](https://codeclimate.com/github/DmitryTsepelev/ar_lazy_preload/maintainability) [![Coverage Status](https://coveralls.io/repos/github/DmitryTsepelev/ar_lazy_preload/badge.svg?branch=master)](https://coveralls.io/github/DmitryTsepelev/ar_lazy_preload?branch=master)
1
+ # ArLazyPreload [![Cult Of Martians](http://cultofmartians.com/assets/badges/badge.svg)](https://cultofmartians.com/tasks/activerecord-lazy-preload.html) [![Gem Version](https://badge.fury.io/rb/ar_lazy_preload.svg)](https://rubygems.org/gems/ar_lazy_preload) [![Build Status](https://github.com/DmitryTsepelev/ar_lazy_preload/actions/workflows/rspec.yml/badge.svg)](https://github.com/DmitryTsepelev/ar_lazy_preload/actions/workflows/rspec.yml) [![Maintainability](https://api.codeclimate.com/v1/badges/00d04595661820dfba80/maintainability)](https://codeclimate.com/github/DmitryTsepelev/ar_lazy_preload/maintainability) [![Coverage Status](https://coveralls.io/repos/github/DmitryTsepelev/ar_lazy_preload/badge.svg?branch=master)](https://coveralls.io/github/DmitryTsepelev/ar_lazy_preload?branch=master)
2
2
 
3
3
  **ArLazyPreload** is a gem that brings association lazy load functionality to your Rails applications. There is a number of built-in methods to solve [N+1 problem](https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations), but sometimes a list of associations to preload is not obvious–this is when you can get most of this gem.
4
4
 
5
5
  - **Simple**. The only thing you need to change is to use `#lazy_preload` instead of `#includes`, `#eager_load` or `#preload`
6
- - **Fast**. Take a look at [benchmarks](https://travis-ci.org/DmitryTsepelev/ar_lazy_preload) (`TASK=bench` and `TASK=memory`)
6
+ - **Fast**. Take a look at [performance benchmark](https://github.com/DmitryTsepelev/ar_lazy_preload/actions/workflows/bench.yml) and [memory benchmark](https://github.com/DmitryTsepelev/ar_lazy_preload/actions/workflows/memory.yml)
7
7
  - **Perfect fit for GraphQL**. Define a list of associations to load at the top-level resolver and let the gem do its job
8
8
  - **Auto-preload support**. If you don't want to specify the association list–set `ArLazyPreload.config.auto_preload` to `true`
9
9
 
@@ -62,7 +62,7 @@ posts = User.preload_associations_lazily.flat_map(&:posts)
62
62
 
63
63
  ## Gotchas
64
64
 
65
- 1. Lazy preloading [does not work](https://github.com/DmitryTsepelev/ar_lazy_preload/pull/40/files) when `.includes` is called earlier:
65
+ 1. Lazy preloading [does not work](https://github.com/DmitryTsepelev/ar_lazy_preload/pull/40/files) for ActiveRecord < 6 when `.includes` is called earlier:
66
66
 
67
67
  ```ruby
68
68
  Post.includes(:user).preload_associations_lazily.each do |p|
@@ -9,8 +9,8 @@ module ArLazyPreload
9
9
  setup_preloading_context unless ArLazyPreload.config.auto_preload?
10
10
  end
11
11
 
12
- delegate :owner, :reflection, to: :proxy_association
13
- delegate :lazy_preload_context, to: :owner
12
+ delegate :owner, :reflection, to: :proxy_association, prefix: true
13
+ delegate :lazy_preload_context, to: :proxy_association_owner
14
14
 
15
15
  private
16
16
 
@@ -19,7 +19,7 @@ module ArLazyPreload
19
19
  return if lazy_preload_context.association_tree.nil?
20
20
 
21
21
  association_tree_builder = AssociationTreeBuilder.new(lazy_preload_context.association_tree)
22
- subtree = association_tree_builder.subtree_for(reflection.name)
22
+ subtree = association_tree_builder.subtree_for(proxy_association_reflection.name)
23
23
 
24
24
  lazy_preload!(subtree)
25
25
  end
@@ -1,12 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "ar_lazy_preload/context"
4
+ require "ar_lazy_preload/contexts/temporary_preload_config"
5
+ require "ar_lazy_preload/preloaded_records_converter"
4
6
 
5
7
  module ArLazyPreload
6
8
  # ActiveRecord::Relation patch with lazy preloading support
7
9
  module Relation
8
10
  attr_writer :preloads_associations_lazily
9
11
 
12
+ def preload_associations(records)
13
+ preload = preload_values
14
+ preload += includes_values unless eager_loading?
15
+
16
+ preload.each do |associations|
17
+ ArLazyPreload::Preloader.new(records, associations).preload.each do |preloader_association|
18
+ handle_preloaded_records(preloader_association.preloaded_records)
19
+ end
20
+ end
21
+ end
22
+
10
23
  # Enhanced #load method will check if association has not been loaded yet and add a context
11
24
  # for lazy preloading to loaded each record
12
25
  def load
@@ -75,6 +88,20 @@ module ArLazyPreload
75
88
  @preloads_associations_lazily ||= false
76
89
  end
77
90
 
91
+ def handle_preloaded_records(preloaded_records)
92
+ return unless Contexts::TemporaryPreloadConfig.enabled? || preloads_associations_lazily?
93
+
94
+ records_array = PreloadedRecordsConverter.call(preloaded_records)
95
+
96
+ return unless records_array&.any?
97
+
98
+ Context.register(
99
+ records: records_array,
100
+ association_tree: lazy_preload_values,
101
+ auto_preload: true
102
+ )
103
+ end
104
+
78
105
  attr_writer :lazy_preload_values
79
106
  end
80
107
  end
@@ -3,6 +3,7 @@
3
3
  require "ar_lazy_preload/contexts/base_context"
4
4
  require "ar_lazy_preload/contexts/auto_preload_context"
5
5
  require "ar_lazy_preload/contexts/lazy_preload_context"
6
+ require "ar_lazy_preload/contexts/temporary_preload_config"
6
7
 
7
8
  module ArLazyPreload
8
9
  class Context
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "set"
4
4
  require "ar_lazy_preload/associated_context_builder"
5
+ require "ar_lazy_preload/preloader"
5
6
 
6
7
  module ArLazyPreload
7
8
  module Contexts
@@ -45,10 +46,10 @@ module ArLazyPreload
45
46
 
46
47
  def perform_preloading(association_name)
47
48
  filtered_records = records.select do |record|
48
- reflection_names_cache[record.class].include?(association_name)
49
+ preloadable_record?(association_name, record)
49
50
  end
50
- preloader.preload(filtered_records, association_name)
51
51
 
52
+ preload_records(association_name, filtered_records)
52
53
  loaded_association_names.add(association_name)
53
54
 
54
55
  AssociatedContextBuilder.prepare(
@@ -57,6 +58,16 @@ module ArLazyPreload
57
58
  )
58
59
  end
59
60
 
61
+ # Method preloads associations for the specific sets of the records
62
+ # and provides automatically provides context for the records
63
+ # loaded using `includes` inside Relation#preload_associations with the
64
+ # help of the TemporaryPreloadConfig
65
+ def preload_records(association_name, records)
66
+ TemporaryPreloadConfig.within_context do
67
+ ArLazyPreload::Preloader.new(records, [association_name]).preload
68
+ end
69
+ end
70
+
60
71
  def association_loaded?(association_name)
61
72
  loaded_association_names.include?(association_name)
62
73
  end
@@ -65,15 +76,30 @@ module ArLazyPreload
65
76
  @loaded_association_names ||= Set.new
66
77
  end
67
78
 
68
- def preloader
69
- @preloader ||= ActiveRecord::Associations::Preloader.new
79
+ def preloadable_record?(association_name, record)
80
+ preloadable_reflections_cache.dig(record.class, association_name)
70
81
  end
71
82
 
72
- def reflection_names_cache
73
- @reflection_names_cache ||= Hash.new do |hash, klass|
74
- hash[klass] = klass.reflect_on_all_associations.map(&:name)
83
+ def preloadable_reflections_cache
84
+ @preloadable_reflections_cache ||= Hash.new do |hash, klass|
85
+ associations = klass.reflect_on_all_associations
86
+
87
+ hash[klass] = associations.each_with_object({}) do |reflection, cache|
88
+ cache[reflection.name] = preloadable_reflection?(klass, reflection)
89
+ end
75
90
  end
76
91
  end
92
+
93
+ def preloadable_reflection?(klass, reflection)
94
+ scope = reflection.scope
95
+ preloadable_scope = scope&.arity&.zero?
96
+ through_reflection =
97
+ reflection.options[:through] && klass.reflect_on_association(reflection.options[:through])
98
+ preloadable_through_reflection =
99
+ through_reflection && preloadable_reflection?(klass, through_reflection)
100
+
101
+ (!scope || preloadable_scope) && (!through_reflection || preloadable_through_reflection)
102
+ end
77
103
  end
78
104
  end
79
105
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ArLazyPreload
4
+ module Contexts
5
+ # Preload config that used to enable preloading only for specfic part of the application
6
+ class TemporaryPreloadConfig
7
+ THREAD_KEY = "temporary_preload_context_enabled"
8
+
9
+ class << self
10
+ def enabled?
11
+ Thread.current[THREAD_KEY] == true
12
+ end
13
+
14
+ def within_context
15
+ Thread.current[THREAD_KEY] = true
16
+ yield
17
+ ensure
18
+ Thread.current[THREAD_KEY] = nil
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ArLazyPreload
4
+ class PreloadedRecordsConverter
5
+ # For different versions of rails we have different records class
6
+ # for ~> 6.1.0 it returns plain array
7
+ # for ~> 6.0.0 it returns ActiveRecord::Relation
8
+ def self.call(preloaded_records)
9
+ case preloaded_records
10
+ when Array
11
+ preloaded_records
12
+ when ::ActiveRecord::Relation
13
+ raise(ArgumentError, "The relation is not preloaded") unless preloaded_records.loaded?
14
+
15
+ preloaded_records.to_a
16
+ else
17
+ raise(ArgumentError, "Unsupported class for preloaded records")
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ArLazyPreload
4
+ class Preloader
5
+ def initialize(records, associations)
6
+ @records = records
7
+ @associations = associations
8
+ end
9
+
10
+ class << self
11
+ def patch_for_rails_7!
12
+ define_method(:preload) do
13
+ ActiveRecord::Associations::Preloader.new(
14
+ records: @records, associations: @associations
15
+ ).call
16
+ end
17
+ end
18
+ end
19
+
20
+ def preload
21
+ ActiveRecord::Associations::Preloader.new.preload(@records, @associations)
22
+ end
23
+ end
24
+ end
@@ -25,6 +25,8 @@ module ArLazyPreload
25
25
 
26
26
  ActiveRecord::Associations::CollectionAssociation.prepend(CollectionAssociation)
27
27
  ActiveRecord::Associations::CollectionProxy.prepend(CollectionProxy)
28
+
29
+ ArLazyPreload::Preloader.patch_for_rails_7! if ActiveRecord::VERSION::MAJOR >= 7
28
30
  end
29
31
  end
30
32
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ArLazyPreload
4
- VERSION = "0.6.0"
4
+ VERSION = "1.0.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ar_lazy_preload
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - DmitryTsepelev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-11-18 00:00:00.000000000 Z
11
+ date: 2021-12-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '4.2'
19
+ version: '5.2'
20
20
  type: :runtime
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: '4.2'
26
+ version: '5.2'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rspec
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -164,6 +164,20 @@ dependencies:
164
164
  - - ">="
165
165
  - !ruby/object:Gem::Version
166
166
  version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: pry
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
167
181
  description: lazy_preload implementation for ActiveRecord models
168
182
  email:
169
183
  - dmitry.a.tsepelev@gmail.com
@@ -189,6 +203,9 @@ files:
189
203
  - lib/ar_lazy_preload/contexts/auto_preload_context.rb
190
204
  - lib/ar_lazy_preload/contexts/base_context.rb
191
205
  - lib/ar_lazy_preload/contexts/lazy_preload_context.rb
206
+ - lib/ar_lazy_preload/contexts/temporary_preload_config.rb
207
+ - lib/ar_lazy_preload/preloaded_records_converter.rb
208
+ - lib/ar_lazy_preload/preloader.rb
192
209
  - lib/ar_lazy_preload/railtie.rb
193
210
  - lib/ar_lazy_preload/version.rb
194
211
  homepage: https://github.com/DmitryTsepelev/ar_lazy_preload
@@ -203,14 +220,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
203
220
  requirements:
204
221
  - - ">="
205
222
  - !ruby/object:Gem::Version
206
- version: '2.3'
223
+ version: '2.6'
207
224
  required_rubygems_version: !ruby/object:Gem::Requirement
208
225
  requirements:
209
226
  - - ">="
210
227
  - !ruby/object:Gem::Version
211
228
  version: '0'
212
229
  requirements: []
213
- rubygems_version: 3.0.3
230
+ rubygems_version: 3.1.4
214
231
  signing_key:
215
232
  specification_version: 4
216
233
  summary: lazy_preload implementation for ActiveRecord models