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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a265f11a7c21888ebe4ac60ccce4ab42bf56ff8527217fa8946013df0c2b0324
4
- data.tar.gz: ce5ed84890259cff5d994a6618fc8017af7e46dc6acb974ef44ec5cc183e494f
3
+ metadata.gz: 302e5031cc0d1046c7ef7400dca6767fb13e4f8e68ba98a7cfa20b648f64d60a
4
+ data.tar.gz: 40cc089d2e4863636d21f13bc9273f0c1341c209275d9162e6ce3ecad05c7e24
5
5
  SHA512:
6
- metadata.gz: 0e7220676078a6a8fb0ed8b93e7eeb81260d20ce692177466dfa97e6ee8915fd56bd524fbac81b039f48ef6ba43023d9838235d6540f0518de73053395ca7ae1
7
- data.tar.gz: 35280832594c531085e7d51047282852615029d4b019532cf00e098c069f85f98a4bfa9cc85f54866baa039c9124346420aa989919161a465d53dc2a873db473
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
- # ArLazyPreload
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
- <a href="https://evilmartians.com/?utm_source=ar_lazy_preload">
10
- <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
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
- Lazy loading associations for the ActiveRecord models. `#includes`, `#eager_load` and `#preload` are built-in methods to avoid N+1 problem, but sometimes when DB request is made we don't know what associations we are going to need later (for instance when your API allows client to define a list of loaded associations dynamically). The only possible solution for such cases is to load _all_ the associations we might need, but it can be a huge overhead.
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
- This gem allows to set up _lazy_ preloading for associations - it won't load anything until association is called for a first time, but when it happens - it loads all the associated records for all records from the initial relation in a single query.
16
+ ## Why should I use it?
15
17
 
16
- ## Installation
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
- ```ruby
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
- For example, if we define the following relation
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
- and use it in the following way
31
+ However, when we try to load posts, there will be one more request for posts:
33
32
 
34
33
  ```ruby
35
- users.map(&:first_name)
34
+ users.map(&:posts) # => SELECT * FROM posts WHERE user_id in (...)
36
35
  ```
37
36
 
38
- there will be one query because we've never accessed posts:
37
+ ## Auto preloading
39
38
 
40
- ```sql
41
- SELECT * FROM users LIMIT 10
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
- However, when we try to load posts
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.map(&:posts)
50
+ users.first.skip_preload.posts # => SELECT * FROM posts WHERE user_id = ?
48
51
  ```
49
52
 
50
- there will be one more request for posts:
53
+ ### Relation auto preloading
54
+
55
+ Another alternative for auto preloading is using relation `#preload_associations_lazily` method
51
56
 
52
- ```sql
53
- SELECT * FROM posts WHERE user_id in (...)
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
- ## Auto preloading
63
+ ## Installation
57
64
 
58
- If you want the gem to be even more lazy - you can configure it to load all the associations lazily without specifying them explicitly. In order to do that you'll need to change the configuration in the following way:
65
+ Add this line to your application's Gemfile, and you're all set:
59
66
 
60
67
  ```ruby
61
- ArLazyPreload.config.auto_preload = true
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
@@ -8,7 +8,7 @@ module ArLazyPreload
8
8
  def merge
9
9
  result = super
10
10
 
11
- if other.lazy_preload_values
11
+ if other.lazy_preload_values.any?
12
12
  if other.klass == relation.klass
13
13
  merge_lazy_preloads
14
14
  else
@@ -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.public_send(association_name)
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(records: associated_records, association_tree: child_association_tree)
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 ArLazyPreload.config.auto_preload?
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(
@@ -4,6 +4,10 @@ module ArLazyPreload
4
4
  module Contexts
5
5
  # This class is responsible for automatic association preloading
6
6
  class AutoPreloadContext < BaseContext
7
+ def auto_preload?
8
+ true
9
+ end
10
+
7
11
  protected
8
12
 
9
13
  def association_needs_preload?(_association_name)
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ArLazyPreload
4
- VERSION = "0.2.6"
4
+ VERSION = "0.4.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.2.6
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: 2018-12-04 00:00:00.000000000 Z
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
- rubyforge_project:
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