ar_lazy_preload 0.2.6 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +39 -33
- data/Rakefile +10 -0
- data/lib/ar_lazy_preload/active_record/base.rb +7 -0
- data/lib/ar_lazy_preload/active_record/collection_association.rb +21 -0
- data/lib/ar_lazy_preload/active_record/merger.rb +1 -1
- data/lib/ar_lazy_preload/active_record/relation.rb +20 -1
- data/lib/ar_lazy_preload/associated_context_builder.rb +9 -5
- data/lib/ar_lazy_preload/context.rb +2 -2
- data/lib/ar_lazy_preload/contexts/auto_preload_context.rb +4 -0
- data/lib/ar_lazy_preload/contexts/base_context.rb +5 -0
- data/lib/ar_lazy_preload/railtie.rb +2 -0
- data/lib/ar_lazy_preload/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 302e5031cc0d1046c7ef7400dca6767fb13e4f8e68ba98a7cfa20b648f64d60a
|
4
|
+
data.tar.gz: 40cc089d2e4863636d21f13bc9273f0c1341c209275d9162e6ce3ecad05c7e24
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 00b77bb2a441ad78f74d96776d36073dbdcc29d811d98de5430ec750217c4f6d8bbf103465debd67c67fd3d86a296f8ab39d2d3e098f193fb348657d0d38c30f
|
7
|
+
data.tar.gz: 0e06520adf62f0bb9f0a1bb7b6d9d667b4478b206a4ffcb1e7c40efd2314d7d0205e72cc27bbb214a1a93e6a3d4a5cd5c5def11d458dfcb3054bd4240162c818
|
data/README.md
CHANGED
@@ -1,67 +1,73 @@
|
|
1
|
-
[![Cult Of Martians](http://cultofmartians.com/assets/badges/badge.svg)](https://cultofmartians.com/tasks/activerecord-lazy-preload.html)
|
2
|
-
[![Gem Version](https://badge.fury.io/rb/ar_lazy_preload.svg)](https://rubygems.org/gems/ar_lazy_preload)
|
3
|
-
[![Build Status](https://travis-ci.org/DmitryTsepelev/ar_lazy_preload.svg?branch=master)](https://travis-ci.org/DmitryTsepelev/ar_lazy_preload)
|
4
|
-
[![Maintainability](https://api.codeclimate.com/v1/badges/00d04595661820dfba80/maintainability)](https://codeclimate.com/github/DmitryTsepelev/ar_lazy_preload/maintainability)
|
5
|
-
[![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://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)
|
6
2
|
|
7
|
-
#
|
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.
|
8
4
|
|
9
|
-
|
10
|
-
|
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`)
|
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
|
+
- **Auto-preload support**. If you don't want to specify the association list–set `ArLazyPreload.config.auto_preload` to `true`
|
11
9
|
|
12
|
-
|
10
|
+
<p align="center">
|
11
|
+
<a href="https://evilmartians.com/?utm_source=ar_lazy_preload">
|
12
|
+
<img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54">
|
13
|
+
</a>
|
14
|
+
</p>
|
13
15
|
|
14
|
-
|
16
|
+
## Why should I use it?
|
15
17
|
|
16
|
-
|
17
|
-
|
18
|
-
Add this line to your application's Gemfile, and you're all set:
|
18
|
+
Lazy loading is super helpful when the list of associations to load is determined dynamically. For instance, in GraphQL this list comes from the API client, and you'll have to inspect the selection set to find out what associations are going to be used.
|
19
19
|
|
20
|
-
|
21
|
-
gem "ar_lazy_preload"
|
22
|
-
```
|
20
|
+
This gem uses a different approach: it won't load anything until the association is called for a first time. When it happens–it loads all the associated records for all records from the initial relation in a single query.
|
23
21
|
|
24
22
|
## Usage
|
25
23
|
|
26
|
-
|
24
|
+
Let's try `#lazy_preload` in action! The following code will perform a single SQL request (because we've never accessed posts):
|
27
25
|
|
28
26
|
```ruby
|
29
|
-
users = User.lazy_preload(:posts).limit(10)
|
27
|
+
users = User.lazy_preload(:posts).limit(10) # => SELECT * FROM users LIMIT 10
|
28
|
+
users.map(&:first_name)
|
30
29
|
```
|
31
30
|
|
32
|
-
|
31
|
+
However, when we try to load posts, there will be one more request for posts:
|
33
32
|
|
34
33
|
```ruby
|
35
|
-
users.map(&:
|
34
|
+
users.map(&:posts) # => SELECT * FROM posts WHERE user_id in (...)
|
36
35
|
```
|
37
36
|
|
38
|
-
|
37
|
+
## Auto preloading
|
39
38
|
|
40
|
-
|
41
|
-
|
39
|
+
If you want the gem to be even lazier–you can configure it to load all the associations lazily without specifying them explicitly. To do that you'll need to change the configuration in the following way:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
ArLazyPreload.config.auto_preload = true
|
42
43
|
```
|
43
44
|
|
44
|
-
|
45
|
+
After that there is no need to call `#lazy_preload` on the association, everything would be loaded lazily.
|
46
|
+
|
47
|
+
If you want to turn automatic preload off for a specific record, you can call `.skip_preload` before any associations method:
|
45
48
|
|
46
49
|
```ruby
|
47
|
-
users.
|
50
|
+
users.first.skip_preload.posts # => SELECT * FROM posts WHERE user_id = ?
|
48
51
|
```
|
49
52
|
|
50
|
-
|
53
|
+
### Relation auto preloading
|
54
|
+
|
55
|
+
Another alternative for auto preloading is using relation `#preload_associations_lazily` method
|
51
56
|
|
52
|
-
```
|
53
|
-
|
57
|
+
```ruby
|
58
|
+
posts = User.preload_associations_lazily.flat_map(&:posts)
|
59
|
+
# => SELECT * FROM users LIMIT 10
|
60
|
+
# => SELECT * FROM posts WHERE user_id in (...)
|
54
61
|
```
|
55
62
|
|
56
|
-
##
|
63
|
+
## Installation
|
57
64
|
|
58
|
-
|
65
|
+
Add this line to your application's Gemfile, and you're all set:
|
59
66
|
|
60
67
|
```ruby
|
61
|
-
|
68
|
+
gem "ar_lazy_preload"
|
62
69
|
```
|
63
70
|
|
64
|
-
After that there is no need to call `lazy_preload` on the association, everything would be loaded lazily.
|
65
|
-
|
66
71
|
## License
|
72
|
+
|
67
73
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
CHANGED
@@ -13,3 +13,13 @@ if !ENV["APPRAISAL_INITIALIZED"] && !ENV["TRAVIS"]
|
|
13
13
|
else
|
14
14
|
task default: [:rubocop, :spec]
|
15
15
|
end
|
16
|
+
|
17
|
+
task :bench do
|
18
|
+
cmd = %w[bundle exec ruby benchmark/main.rb]
|
19
|
+
exit system(*cmd)
|
20
|
+
end
|
21
|
+
|
22
|
+
task :memory do
|
23
|
+
cmd = %w[bundle exec ruby benchmark/memory.rb]
|
24
|
+
exit system(*cmd)
|
25
|
+
end
|
@@ -5,10 +5,17 @@ module ArLazyPreload
|
|
5
5
|
module Base
|
6
6
|
def self.included(base)
|
7
7
|
base.class.delegate :lazy_preload, to: :all
|
8
|
+
base.class.delegate :preload_associations_lazily, to: :all
|
8
9
|
end
|
9
10
|
|
10
11
|
attr_accessor :lazy_preload_context
|
11
12
|
|
12
13
|
delegate :try_preload_lazily, to: :lazy_preload_context, allow_nil: true
|
14
|
+
|
15
|
+
def skip_preload
|
16
|
+
lazy_preload_context&.records&.delete(self)
|
17
|
+
self.lazy_preload_context = nil
|
18
|
+
self
|
19
|
+
end
|
13
20
|
end
|
14
21
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ArLazyPreload
|
4
|
+
# ActiveRecord::CollectionAssociation patch with a hook for lazy preloading
|
5
|
+
module CollectionAssociation
|
6
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
7
|
+
def ids_reader
|
8
|
+
return super if owner.lazy_preload_context.blank?
|
9
|
+
|
10
|
+
primary_key = reflection.association_primary_key.to_sym
|
11
|
+
if loaded?
|
12
|
+
target.map(&primary_key)
|
13
|
+
elsif !target.empty?
|
14
|
+
load_target.map(&primary_key)
|
15
|
+
else
|
16
|
+
@association_ids ||= reader.map(&primary_key)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
20
|
+
end
|
21
|
+
end
|
@@ -5,6 +5,8 @@ require "ar_lazy_preload/context"
|
|
5
5
|
module ArLazyPreload
|
6
6
|
# ActiveRecord::Relation patch with lazy preloading support
|
7
7
|
module Relation
|
8
|
+
attr_writer :preloads_associations_lazily
|
9
|
+
|
8
10
|
# Enhanced #load method will check if association has not been loaded yet and add a context
|
9
11
|
# for lazy preloading to loaded each record
|
10
12
|
def load
|
@@ -13,12 +15,25 @@ module ArLazyPreload
|
|
13
15
|
if need_context
|
14
16
|
Context.register(
|
15
17
|
records: ar_lazy_preload_records,
|
16
|
-
association_tree: lazy_preload_values
|
18
|
+
association_tree: lazy_preload_values,
|
19
|
+
auto_preload: preloads_associations_lazily?
|
17
20
|
)
|
18
21
|
end
|
19
22
|
result
|
20
23
|
end
|
21
24
|
|
25
|
+
# Lazily autoloads all associations. For example:
|
26
|
+
#
|
27
|
+
# users = User.preload_associations_lazily
|
28
|
+
# users.each do |user|
|
29
|
+
# user.posts.flat_map {|post| post.comments.map(&:id)}
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# Same effect can be achieved by User.lazy_preload(posts: :comments)
|
33
|
+
def preload_associations_lazily
|
34
|
+
spawn.tap { |relation| relation.preloads_associations_lazily = true }
|
35
|
+
end
|
36
|
+
|
22
37
|
# Specify relationships to be loaded lazily when association is loaded for the first time. For
|
23
38
|
# example:
|
24
39
|
#
|
@@ -56,6 +71,10 @@ module ArLazyPreload
|
|
56
71
|
@records
|
57
72
|
end
|
58
73
|
|
74
|
+
def preloads_associations_lazily?
|
75
|
+
@preloads_associations_lazily ||= false
|
76
|
+
end
|
77
|
+
|
59
78
|
attr_writer :lazy_preload_values
|
60
79
|
end
|
61
80
|
end
|
@@ -23,23 +23,27 @@ module ArLazyPreload
|
|
23
23
|
|
24
24
|
# Takes all the associated records for the records, attached to the :parent_context and creates
|
25
25
|
# a preloading context for them
|
26
|
-
def perform
|
26
|
+
def perform # rubocop:disable Metrics/MethodLength
|
27
27
|
associated_records = parent_context.records.flat_map do |record|
|
28
28
|
next if record.nil?
|
29
29
|
|
30
|
-
record_association = record.
|
30
|
+
record_association = record.association(association_name)
|
31
31
|
reflection = reflection_cache[record.class]
|
32
|
-
reflection.collection? ? record_association.target : record_association
|
32
|
+
reflection.collection? ? record_association.target : record_association.reader
|
33
33
|
end
|
34
34
|
|
35
|
-
Context.register(
|
35
|
+
Context.register(
|
36
|
+
records: associated_records,
|
37
|
+
association_tree: child_association_tree,
|
38
|
+
auto_preload: parent_context.auto_preload?
|
39
|
+
)
|
36
40
|
end
|
37
41
|
|
38
42
|
private
|
39
43
|
|
40
44
|
def child_association_tree
|
41
45
|
# `association_tree` is unnecessary when auto preload is enabled
|
42
|
-
return nil if
|
46
|
+
return nil if parent_context.auto_preload?
|
43
47
|
|
44
48
|
AssociationTreeBuilder.new(parent_context.association_tree).subtree_for(association_name)
|
45
49
|
end
|
@@ -7,10 +7,10 @@ require "ar_lazy_preload/contexts/lazy_preload_context"
|
|
7
7
|
module ArLazyPreload
|
8
8
|
class Context
|
9
9
|
# Initiates lazy preload context for given records
|
10
|
-
def self.register(records:, association_tree:)
|
10
|
+
def self.register(records:, association_tree:, auto_preload: false)
|
11
11
|
return if records.empty?
|
12
12
|
|
13
|
-
if ArLazyPreload.config.auto_preload?
|
13
|
+
if ArLazyPreload.config.auto_preload? || auto_preload
|
14
14
|
Contexts::AutoPreloadContext.new(records: records)
|
15
15
|
elsif association_tree.any?
|
16
16
|
Contexts::LazyPreloadContext.new(
|
@@ -15,6 +15,7 @@ module ArLazyPreload
|
|
15
15
|
def initialize(records:)
|
16
16
|
@records = records.dup
|
17
17
|
@records.compact!
|
18
|
+
@records.uniq!
|
18
19
|
@records.each { |record| record.lazy_preload_context = self }
|
19
20
|
end
|
20
21
|
|
@@ -27,6 +28,10 @@ module ArLazyPreload
|
|
27
28
|
perform_preloading(association_name)
|
28
29
|
end
|
29
30
|
|
31
|
+
def auto_preload?
|
32
|
+
false
|
33
|
+
end
|
34
|
+
|
30
35
|
protected
|
31
36
|
|
32
37
|
def association_needs_preload?(_association_name)
|
@@ -3,6 +3,7 @@
|
|
3
3
|
require "ar_lazy_preload/active_record/base"
|
4
4
|
require "ar_lazy_preload/active_record/relation"
|
5
5
|
require "ar_lazy_preload/active_record/association"
|
6
|
+
require "ar_lazy_preload/active_record/collection_association"
|
6
7
|
require "ar_lazy_preload/active_record/merger"
|
7
8
|
require "ar_lazy_preload/active_record/association_relation"
|
8
9
|
require "ar_lazy_preload/active_record/collection_proxy"
|
@@ -22,6 +23,7 @@ module ArLazyPreload
|
|
22
23
|
ActiveRecord::Associations::Association
|
23
24
|
].each { |klass| klass.prepend(Association) }
|
24
25
|
|
26
|
+
ActiveRecord::Associations::CollectionAssociation.prepend(CollectionAssociation)
|
25
27
|
ActiveRecord::Associations::CollectionProxy.prepend(CollectionProxy)
|
26
28
|
end
|
27
29
|
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: 0.4.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: 2020-08-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -164,6 +164,7 @@ files:
|
|
164
164
|
- lib/ar_lazy_preload/active_record/association.rb
|
165
165
|
- lib/ar_lazy_preload/active_record/association_relation.rb
|
166
166
|
- lib/ar_lazy_preload/active_record/base.rb
|
167
|
+
- lib/ar_lazy_preload/active_record/collection_association.rb
|
167
168
|
- lib/ar_lazy_preload/active_record/collection_proxy.rb
|
168
169
|
- lib/ar_lazy_preload/active_record/merger.rb
|
169
170
|
- lib/ar_lazy_preload/active_record/relation.rb
|
@@ -195,8 +196,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
195
196
|
- !ruby/object:Gem::Version
|
196
197
|
version: '0'
|
197
198
|
requirements: []
|
198
|
-
|
199
|
-
rubygems_version: 2.7.6
|
199
|
+
rubygems_version: 3.0.3
|
200
200
|
signing_key:
|
201
201
|
specification_version: 4
|
202
202
|
summary: lazy_preload implementation for ActiveRecord models
|