ar_lazy_preload 0.2.4 → 0.3.1

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: 8e76eb3ed4d0d069552b4c805c82d0fb28829559fa63a70adeb81de0e8477409
4
- data.tar.gz: 6033b6faf439eeaccc02c525d00cb24bc80521c267ad4e4960b89cc9871506ca
3
+ metadata.gz: b8f6aa06d7f037bd8762f0000db03adde926f41ee536ec4faac8b1f3ca93ff1e
4
+ data.tar.gz: c6a8b59093090fc898692c102acda5ed949a34846605b65a6f27b89b940d2d46
5
5
  SHA512:
6
- metadata.gz: 1b9b9658859f6b8a138735bcba398d2324671951480e861129530389ceefd242efa2266d08f4dea8d2b02905ab3ec18097629bb6c5a294c427531a903dc26caf
7
- data.tar.gz: c7bb4abcddefa8ab551b1fcb5a9eaab508fb02af7e1285ed419ba20e97dfac3cb3186149293d71bb0436e973656c2d03a73ee842063b4aa4a7942ec52aa6031c
6
+ metadata.gz: 41abf78b2668b8f39c5713a72a6536a57f660bea16bdebdb4a2f1d069c86285d0455c2d193d46f2dbe12551e12d823c1187163223193826d3861154f67c03c1d
7
+ data.tar.gz: 7cd15b8bf27071baa258992e99a726e9b49f25ea5d8edb2802fa9d2e30ce652bb204acfb131b8e0adff33f178a6a1d322e4717e2f8339da4224765a5da92ca30
data/README.md CHANGED
@@ -1,64 +1,57 @@
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
- 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.
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`
10
9
 
11
- 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.
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>
12
15
 
13
- ## Installation
16
+ ## Why should I use it?
14
17
 
15
- 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.
16
19
 
17
- ```ruby
18
- gem "ar_lazy_preload"
19
- ```
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.
20
21
 
21
22
  ## Usage
22
23
 
23
- 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):
24
25
 
25
26
  ```ruby
26
- 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)
27
29
  ```
28
30
 
29
- and use it in the following way
31
+ However, when we try to load posts, there will be one more request for posts:
30
32
 
31
33
  ```ruby
32
- users.map(&:first_name)
34
+ users.map(&:posts) # => SELECT * FROM posts WHERE user_id in (...)
33
35
  ```
34
36
 
35
- there will be one query because we've never accessed posts:
36
-
37
- ```sql
38
- SELECT * FROM users LIMIT 10
39
- ```
37
+ ## Auto preloading
40
38
 
41
- However, when we try to load posts
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:
42
40
 
43
41
  ```ruby
44
- users.map(&:posts)
42
+ ArLazyPreload.config.auto_preload = true
45
43
  ```
46
44
 
47
- there will be one more request for posts:
48
-
49
- ```sql
50
- SELECT * FROM posts WHERE user_id in (...)
51
- ```
45
+ After that there is no need to call `#lazy_preload` on the association, everything would be loaded lazily.
52
46
 
53
- ## Auto preloading
47
+ ## Installation
54
48
 
55
- 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:
49
+ Add this line to your application's Gemfile, and you're all set:
56
50
 
57
51
  ```ruby
58
- ArLazyPreload.config.auto_preload = true
52
+ gem "ar_lazy_preload"
59
53
  ```
60
54
 
61
- After that there is no need to call `lazy_preload` on the association, everything would be loaded lazily.
62
-
63
55
  ## License
56
+
64
57
  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
@@ -6,16 +6,21 @@ module ArLazyPreload
6
6
  module AssociationRelation
7
7
  def initialize(*args)
8
8
  super(*args)
9
+ setup_preloading_context unless ArLazyPreload.config.auto_preload?
10
+ end
11
+
12
+ delegate :owner, :reflection, to: :proxy_association
13
+ delegate :lazy_preload_context, to: :owner
9
14
 
10
- context = owner.lazy_preload_context
11
- return if context.nil?
15
+ private
12
16
 
13
- association_tree_builder = AssociationTreeBuilder.new(context.association_tree)
17
+ def setup_preloading_context
18
+ return if lazy_preload_context.nil?
19
+
20
+ association_tree_builder = AssociationTreeBuilder.new(lazy_preload_context.association_tree)
14
21
  subtree = association_tree_builder.subtree_for(reflection.name)
15
22
 
16
23
  lazy_preload!(subtree)
17
24
  end
18
-
19
- delegate :owner, :reflection, to: :proxy_association
20
25
  end
21
26
  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
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
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
@@ -24,11 +24,13 @@ module ArLazyPreload
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
26
  def perform
27
- records_by_class = parent_context.records.group_by(&:class)
27
+ associated_records = parent_context.records.flat_map do |record|
28
+ next if record.nil?
28
29
 
29
- associated_records = records_by_class.map do |klass, klass_records|
30
- associated_records_for(klass, klass_records)
31
- end.flatten
30
+ record_association = record.association(association_name)
31
+ reflection = reflection_cache[record.class]
32
+ reflection.collection? ? record_association.target : record_association.reader
33
+ end
32
34
 
33
35
  Context.register(records: associated_records, association_tree: child_association_tree)
34
36
  end
@@ -36,13 +38,16 @@ module ArLazyPreload
36
38
  private
37
39
 
38
40
  def child_association_tree
41
+ # `association_tree` is unnecessary when auto preload is enabled
42
+ return nil if ArLazyPreload.config.auto_preload?
43
+
39
44
  AssociationTreeBuilder.new(parent_context.association_tree).subtree_for(association_name)
40
45
  end
41
46
 
42
- def associated_records_for(klass, records)
43
- record_associations = records.map { |record| record.send(association_name) }
44
- reflection = klass.reflect_on_association(association_name)
45
- reflection.collection? ? record_associations.map(&:target).flatten : record_associations
47
+ def reflection_cache
48
+ @reflection_cache ||= Hash.new do |hash, klass|
49
+ hash[klass] = klass.reflect_on_association(association_name)
50
+ end
46
51
  end
47
52
  end
48
53
  end
@@ -8,9 +8,6 @@ module ArLazyPreload
8
8
  attr_reader :association_tree
9
9
 
10
10
  def initialize(association_tree)
11
- # Since `association_tree` can be an array or a single hash
12
- # Converting it to an array is easier for processing
13
- # like jquery
14
11
  @association_tree =
15
12
  case association_tree
16
13
  when Array
@@ -18,8 +15,7 @@ module ArLazyPreload
18
15
  when Hash
19
16
  [association_tree]
20
17
  else
21
- raise NotImplementedError,
22
- "unexpected association_tree with class #{association_tree.class}"
18
+ raise ArgumentError, "unexpected association_tree with class #{association_tree.class}"
23
19
  end.select { |node| node.is_a?(Hash) }
24
20
  end
25
21
 
@@ -31,7 +27,7 @@ module ArLazyPreload
31
27
 
32
28
  def subtree_cache
33
29
  @subtree_cache ||= Hash.new do |hash, association|
34
- hash[association] = association_tree.map { |node| node[association] }.flatten
30
+ hash[association] = association_tree.flat_map { |node| node[association] }
35
31
  end
36
32
  end
37
33
  end
@@ -1,57 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "ar_lazy_preload/associated_context_builder"
3
+ require "ar_lazy_preload/contexts/base_context"
4
+ require "ar_lazy_preload/contexts/auto_preload_context"
5
+ require "ar_lazy_preload/contexts/lazy_preload_context"
4
6
 
5
7
  module ArLazyPreload
6
- # This class is responsible for holding a connection between a list of ActiveRecord::Base objects
7
- # which have been loaded by the same instance of ActiveRecord::Relation. It also contains a tree
8
- # of associations, which were requested to be loaded lazily.
9
- # Calling #preload_association method will cause loading of ALL associated objects for EACH
10
- # ecord when requested association is found in the association tree.
11
8
  class Context
12
9
  # Initiates lazy preload context for given records
13
10
  def self.register(records:, association_tree:)
14
- return if records.empty? || association_tree.empty? && !ArLazyPreload.config.auto_preload?
15
-
16
- ArLazyPreload::Context.new(records: records, association_tree: association_tree)
17
- end
18
-
19
- attr_reader :records, :association_tree
20
-
21
- # :records - array of ActiveRecord instances
22
- # :association_tree - list of symbols or hashes representing a tree of preloadable associations
23
- def initialize(records:, association_tree:)
24
- @records = records.compact
25
- @association_tree = association_tree
26
-
27
- @records.each { |record| record.lazy_preload_context = self }
28
- end
29
-
30
- # This method checks if the association is present in the association_tree and preloads for all
31
- # objects in the context it if needed.
32
- def try_preload_lazily(association_name)
33
- return unless association_needs_preload?(association_name)
34
-
35
- preloader.preload(records, association_name)
36
- AssociatedContextBuilder.prepare(parent_context: self, association_name: association_name)
37
- end
38
-
39
- private
40
-
41
- def association_needs_preload?(association_name, node_tree = association_tree)
42
- return true if ArLazyPreload.config.auto_preload?
43
-
44
- node_tree.any? do |node|
45
- if node.is_a?(Symbol)
46
- node == association_name
47
- elsif node.is_a?(Hash)
48
- node.key?(association_name)
49
- end
11
+ return if records.empty?
12
+
13
+ if ArLazyPreload.config.auto_preload?
14
+ Contexts::AutoPreloadContext.new(records: records)
15
+ elsif association_tree.any?
16
+ Contexts::LazyPreloadContext.new(
17
+ records: records,
18
+ association_tree: association_tree
19
+ )
50
20
  end
51
21
  end
52
-
53
- def preloader
54
- @preloader ||= ActiveRecord::Associations::Preloader.new
55
- end
56
22
  end
57
23
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ArLazyPreload
4
+ module Contexts
5
+ # This class is responsible for automatic association preloading
6
+ class AutoPreloadContext < BaseContext
7
+ protected
8
+
9
+ def association_needs_preload?(_association_name)
10
+ true
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+ require "ar_lazy_preload/associated_context_builder"
5
+
6
+ module ArLazyPreload
7
+ module Contexts
8
+ # This is a base context class, which is responsible for holding a connection between a list of
9
+ # ActiveRecord::Base objects which have been loaded by the same instance of
10
+ # ActiveRecord::Relation.
11
+ class BaseContext
12
+ attr_reader :records
13
+
14
+ # :records - array of ActiveRecord instances
15
+ def initialize(records:)
16
+ @records = records.dup
17
+ @records.compact!
18
+ @records.uniq!
19
+ @records.each { |record| record.lazy_preload_context = self }
20
+ end
21
+
22
+ # This method checks if the association should be loaded and preloads it for all
23
+ # objects in the context it if needed.
24
+ def try_preload_lazily(association_name)
25
+ return if association_loaded?(association_name) ||
26
+ !association_needs_preload?(association_name)
27
+
28
+ perform_preloading(association_name)
29
+ end
30
+
31
+ protected
32
+
33
+ def association_needs_preload?(_association_name)
34
+ raise NotImplementedError
35
+ end
36
+
37
+ private
38
+
39
+ def perform_preloading(association_name)
40
+ preloader.preload(records, association_name)
41
+ loaded_association_names.add(association_name)
42
+
43
+ AssociatedContextBuilder.prepare(
44
+ parent_context: self,
45
+ association_name: association_name
46
+ )
47
+ end
48
+
49
+ def association_loaded?(association_name)
50
+ loaded_association_names.include?(association_name)
51
+ end
52
+
53
+ def loaded_association_names
54
+ @loaded_association_names ||= Set.new
55
+ end
56
+
57
+ def preloader
58
+ @preloader ||= ActiveRecord::Associations::Preloader.new
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ArLazyPreload
4
+ module Contexts
5
+ # This class is responsible for lazy preloading. It contains a tree of associations, which were
6
+ # requested to be loaded lazily.
7
+ class LazyPreloadContext < BaseContext
8
+ attr_reader :association_tree
9
+
10
+ # :records - array of ActiveRecord instances
11
+ # :association_tree - list of symbols or hashes representing a tree of preloadable
12
+ # associations
13
+ def initialize(records:, association_tree:)
14
+ @association_tree = association_tree
15
+
16
+ super(records: records)
17
+ end
18
+
19
+ protected
20
+
21
+ def association_needs_preload?(association_name)
22
+ association_tree.any? do |node|
23
+ case node
24
+ when Symbol
25
+ node == association_name
26
+ when Hash
27
+ node.key?(association_name)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -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.4"
4
+ VERSION = "0.3.1"
5
5
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ar_lazy_preload
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - DmitryTsepelev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-11-21 00:00:00.000000000 Z
11
+ date: 2020-07-10 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '4.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '4.2'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: rspec
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -122,6 +136,20 @@ dependencies:
122
136
  - - ">="
123
137
  - !ruby/object:Gem::Version
124
138
  version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: memory_profiler
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
125
153
  description: lazy_preload implementation for ActiveRecord models
126
154
  email:
127
155
  - dmitry.a.tsepelev@gmail.com
@@ -136,6 +164,7 @@ files:
136
164
  - lib/ar_lazy_preload/active_record/association.rb
137
165
  - lib/ar_lazy_preload/active_record/association_relation.rb
138
166
  - lib/ar_lazy_preload/active_record/base.rb
167
+ - lib/ar_lazy_preload/active_record/collection_association.rb
139
168
  - lib/ar_lazy_preload/active_record/collection_proxy.rb
140
169
  - lib/ar_lazy_preload/active_record/merger.rb
141
170
  - lib/ar_lazy_preload/active_record/relation.rb
@@ -143,6 +172,9 @@ files:
143
172
  - lib/ar_lazy_preload/association_tree_builder.rb
144
173
  - lib/ar_lazy_preload/configuration.rb
145
174
  - lib/ar_lazy_preload/context.rb
175
+ - lib/ar_lazy_preload/contexts/auto_preload_context.rb
176
+ - lib/ar_lazy_preload/contexts/base_context.rb
177
+ - lib/ar_lazy_preload/contexts/lazy_preload_context.rb
146
178
  - lib/ar_lazy_preload/railtie.rb
147
179
  - lib/ar_lazy_preload/version.rb
148
180
  homepage: https://github.com/DmitryTsepelev/ar_lazy_preload
@@ -164,8 +196,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
164
196
  - !ruby/object:Gem::Version
165
197
  version: '0'
166
198
  requirements: []
167
- rubyforge_project:
168
- rubygems_version: 2.7.6
199
+ rubygems_version: 3.0.3
169
200
  signing_key:
170
201
  specification_version: 4
171
202
  summary: lazy_preload implementation for ActiveRecord models