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 +4 -4
- data/README.md +3 -3
- data/lib/ar_lazy_preload/active_record/association_relation.rb +3 -3
- data/lib/ar_lazy_preload/active_record/relation.rb +27 -0
- data/lib/ar_lazy_preload/context.rb +1 -0
- data/lib/ar_lazy_preload/contexts/base_context.rb +33 -7
- data/lib/ar_lazy_preload/contexts/temporary_preload_config.rb +23 -0
- data/lib/ar_lazy_preload/preloaded_records_converter.rb +21 -0
- data/lib/ar_lazy_preload/preloader.rb +24 -0
- data/lib/ar_lazy_preload/railtie.rb +2 -0
- data/lib/ar_lazy_preload/version.rb +1 -1
- metadata +23 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 853f4f5f10ba5f9aa5e6530eb767f9d4063d62745b2b332776c7ebb44c753700
|
4
|
+
data.tar.gz: ff2f6a626e8e944734dcab46eef9677bda77582027f1e687b27b616a5cdac0cb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f873a03ba3abc319042f601e512b48b64d638f3b6358e722768e92745992c654f4aef229e792690c0c250563c9f28ae2d69fd77931c9af27db9a09f719f6341f
|
7
|
+
data.tar.gz: 521c1bf8d19bc8bc2c5e71c8fd2918ac400102368016a9d6448b19426bcefc569b51fda0e13b205d86a872553f08a619a436fb3c7821bb4b866056c771065121
|
data/README.md
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
-
# ArLazyPreload [](https://cultofmartians.com/tasks/activerecord-lazy-preload.html) [](https://rubygems.org/gems/ar_lazy_preload) [](https://cultofmartians.com/tasks/activerecord-lazy-preload.html) [](https://rubygems.org/gems/ar_lazy_preload) [](https://github.com/DmitryTsepelev/ar_lazy_preload/actions/workflows/rspec.yml) [](https://codeclimate.com/github/DmitryTsepelev/ar_lazy_preload/maintainability) [](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 [
|
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: :
|
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(
|
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
|
@@ -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
|
-
|
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
|
69
|
-
|
79
|
+
def preloadable_record?(association_name, record)
|
80
|
+
preloadable_reflections_cache.dig(record.class, association_name)
|
70
81
|
end
|
71
82
|
|
72
|
-
def
|
73
|
-
@
|
74
|
-
|
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
|
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.
|
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:
|
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: '
|
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: '
|
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.
|
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.
|
230
|
+
rubygems_version: 3.1.4
|
214
231
|
signing_key:
|
215
232
|
specification_version: 4
|
216
233
|
summary: lazy_preload implementation for ActiveRecord models
|