smart_preloader 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5b818838fe062bbaf5c32f1ebdeb2144e345c778155e4913f7954402c729588f
4
+ data.tar.gz: a065007b057efb7dbe3f8ee862cf7c89b4defdf2d73e2b3a8ebbaf85a9234b69
5
+ SHA512:
6
+ metadata.gz: 73cceb0acb5496af9545ef59a409c78a7b62c5509b2c309334d46d8929162d2f2dc118af7fc7364925a63ecebc8a6876bb6bf9130353aaa611a11cc130ce30cf
7
+ data.tar.gz: a52c72d6c2773d5ed1ce93f80e57a3b31ff588d0449d98d4c903b957b2d7039df7aa956b51d5f5cdbc26b4cbc272dd5c888ec822b14521d230a8334c7872b833
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+ .idea
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in smart_preloader.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
data/Gemfile.lock ADDED
@@ -0,0 +1,34 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ smart_preloader (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ diff-lcs (1.4.4)
10
+ rake (13.0.6)
11
+ rspec (3.10.0)
12
+ rspec-core (~> 3.10.0)
13
+ rspec-expectations (~> 3.10.0)
14
+ rspec-mocks (~> 3.10.0)
15
+ rspec-core (3.10.1)
16
+ rspec-support (~> 3.10.0)
17
+ rspec-expectations (3.10.1)
18
+ diff-lcs (>= 1.2.0, < 2.0)
19
+ rspec-support (~> 3.10.0)
20
+ rspec-mocks (3.10.2)
21
+ diff-lcs (>= 1.2.0, < 2.0)
22
+ rspec-support (~> 3.10.0)
23
+ rspec-support (3.10.2)
24
+
25
+ PLATFORMS
26
+ x86_64-darwin-20
27
+
28
+ DEPENDENCIES
29
+ rake (~> 13.0)
30
+ rspec (~> 3.0)
31
+ smart_preloader!
32
+
33
+ BUNDLED WITH
34
+ 2.2.25
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Serg Tyatin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # SmartPreloader
2
+
3
+ Smart preloader allows to:
4
+ - Preload polymorphic associations
5
+ - Filter records for preloading
6
+ - Preload composite key associations
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem 'smart_preloader'
14
+ ```
15
+
16
+ And then execute:
17
+
18
+ $ bundle install
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install smart_preloader
23
+
24
+ ## Usage
25
+
26
+ ### Preload polymorphic associations
27
+ ```ruby
28
+ class Comment < ApplicationRecord
29
+ belongs_to :owner, polymorphic: true # User or Post
30
+ end
31
+ ```
32
+
33
+ To preload use the same syntax as for Rails eager load with one extra layer for polymorphic association
34
+ Use class constants to specify corresponding preloads
35
+ ```ruby
36
+ comments = Comment.all
37
+ ActiveRecord::SmartPreloader.(comments, owner: [
38
+ User => :account,
39
+ Post => :votes
40
+ ])
41
+ ```
42
+ Class name in preload tree considered as a filter for a records
43
+
44
+ ### Filter records for preloading
45
+ It allows filter records at ruby level for further preloading
46
+ ```ruby
47
+ comments = Comment.all
48
+ ActiveRecord::SmartPreloader.(comments, ->(comment) { comment.rated? } => :author)
49
+ ```
50
+ The code filters `rated?` comments and preloads `Comment#author` association
51
+
52
+ ### Preload composite key associations
53
+ Models could be referenced not by single id but by composite/multi key, like [:category_id, :author_id]
54
+
55
+ ```ruby
56
+ class AuthorRating < ApplicationRecord
57
+ belongs_to :author
58
+ belongs_to :category
59
+ end
60
+ class Post < ApplicationRecord
61
+ belongs_to :category
62
+ belongs_to :author
63
+ has_one :author_rating,
64
+ ->(post) { where(category_id: post.category_id) },
65
+ class_name: 'AuthorRating', foreign_key: :author_id, primary_key: :author_id
66
+ end
67
+ ```
68
+
69
+ To preload post's author rating in the category:
70
+ ```ruby
71
+ ActiveRecord::SmartPreloader.(posts, ActiveRecord::CompositeKey.new(:author_rating, [:author_id, :category_id]))
72
+ ```
73
+
74
+ and it could be put in tree as usual:
75
+ ```ruby
76
+ ActiveRecord::SmartPreloader.(comments, post: [
77
+ ActiveRecord::CompositeKey.new(:author_rating, [:author_id, :category_id]) => :voters
78
+ ])
79
+ ```
80
+
81
+
82
+ ## Contributing
83
+
84
+ Bug reports and pull requests are welcome on GitHub at https://github.com/2rba/smart_preloader.
85
+
86
+ ## License
87
+
88
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+ module ActiveRecord
3
+ class CompositeKey
4
+ attr_reader :association, :key
5
+
6
+ def initialize(association, key)
7
+ @association = association
8
+ @key = key
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ # Modified ActiveRecord::Associations::Preloader::Association
5
+ class CompositeKeyPreloader
6
+ # The CompositeKeyPreloader does the same as default rails preloader ( ActiveRecord::Associations::Preloader )
7
+ # The difference: CompositeKeyPreloader allows to reference another table by multiple columns
8
+ #
9
+ # @param [Array<ActiveModel>] records Collection of active record models
10
+ # @param [Symbol] association ActiveRecord model association name to preload
11
+ # @param [Array<Symbol>] composite_key Array of primary_keys, defines how association should be loaded.
12
+ # Default Rails implementation does not allow to specify composite key (multi-column key)
13
+ def self.call(records, association, composite_key)
14
+ records_for_preload = records.reject { |record| record.association(association).loaded? }
15
+ return if records_for_preload.blank?
16
+
17
+ assoc = records.first.association(association)
18
+ new(assoc.klass, records, assoc.reflection, composite_key).run
19
+ end
20
+
21
+ def initialize(klass, owners, reflection, composite_key)
22
+ @klass = klass
23
+ @owners = owners
24
+ @reflection = reflection
25
+ @preloaded_records = []
26
+ @composite_key = composite_key
27
+ end
28
+
29
+ def run
30
+ records = load_records
31
+
32
+ owners.each do |owner|
33
+ owner_key = @composite_key.map { |key_name| owner[key_name] }
34
+ associate_records_to_owner(owner, records[owner_key] || [])
35
+ end
36
+ end
37
+
38
+ protected
39
+
40
+ attr_reader :owners, :reflection, :klass
41
+
42
+ private
43
+
44
+ def associate_records_to_owner(owner, records)
45
+ association = owner.association(reflection.name)
46
+ association.loaded!
47
+ raise 'no tested yet' if reflection.collection?
48
+
49
+ association.target = records.first unless records.empty?
50
+ end
51
+
52
+ def owner_keys
53
+ @owner_keys ||= owners_by_key.keys
54
+ end
55
+
56
+ def owners_by_key
57
+ unless defined?(@owners_by_key)
58
+ @owners_by_key = owners.each_with_object({}) do |owner, h|
59
+ key = @composite_key.map { |key_name| owner[key_name] }
60
+ h[key] = owner if key
61
+ end
62
+ end
63
+ @owners_by_key
64
+ end
65
+
66
+ def load_records
67
+ return {} if owner_keys.empty?
68
+
69
+ # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
70
+ # Make several smaller queries if necessary or make one query if the adapter supports it
71
+ slices = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size)
72
+ @preloaded_records = slices.flat_map do |slice|
73
+ records_for(slice)
74
+ end
75
+ @preloaded_records.group_by do |record|
76
+ @composite_key.map { |key_name| record[key_name] }
77
+ end
78
+ end
79
+
80
+ def records_for(ids)
81
+ composed_id_rows = ids.map { |multiple_ids| "ROW(#{multiple_ids.map(&:to_i).join(',')})" }.join(', ')
82
+ klass.scope_for_association.where("(#{@composite_key.join(', ')}) IN (#{composed_id_rows})")
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ class SmartPreloader
5
+ VERSION = '0.1.0'
6
+
7
+ def self.call(records, association)
8
+ case association
9
+ when Hash
10
+ association.each do |key, value|
11
+ preloaded_records = call(records, key)
12
+ call(preloaded_records, value)
13
+ end
14
+ when Array
15
+ association.each do |key|
16
+ call(records, key)
17
+ end
18
+ when Symbol, String
19
+ ActiveRecord::Associations::Preloader.new.preload(records, association)
20
+ records.flat_map(&association.to_sym.to_proc).compact
21
+ when CompositeKey
22
+ ActiveRecord::CompositeKeyPreloader.(records, association.association, association.key)
23
+ records.flat_map(&association.association.to_sym.to_proc).compact
24
+ when Proc
25
+ records.select(&association)
26
+ else
27
+ if association.instance_of?(Class) && association.ancestors.include?(ApplicationRecord)
28
+ return records.grep(association)
29
+ end
30
+
31
+ raise ArgumentError, "#{association.inspect} was not recognized for preload"
32
+ end
33
+ end
34
+
35
+ def initialize(preloads)
36
+ @preloads = preloads
37
+ end
38
+
39
+ def call(records)
40
+ self.class.call(records, @preloads)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/active_record/smart_preloader"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "smart_preloader"
7
+ spec.version = ActiveRecord::SmartPreloader::VERSION
8
+ spec.authors = ["Serg Tyatin"]
9
+ spec.email = ["700@2rba.com"]
10
+
11
+ spec.summary = "Allows to preload associations in a smart way"
12
+ spec.description = "Allows to preload ActiveRecord associations in a smart way"
13
+ spec.homepage = "https://github.com/2rba/smart_preloader"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0")
16
+
17
+ # Specify which files should be added to the gem when it is released.
18
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
20
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
21
+ end
22
+ spec.require_paths = ["lib"]
23
+ end
metadata ADDED
@@ -0,0 +1,54 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: smart_preloader
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Serg Tyatin
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-08-31 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Allows to preload ActiveRecord associations in a smart way
14
+ email:
15
+ - 700@2rba.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".gitignore"
21
+ - ".rspec"
22
+ - Gemfile
23
+ - Gemfile.lock
24
+ - LICENSE.txt
25
+ - README.md
26
+ - Rakefile
27
+ - lib/active_record/composite_key.rb
28
+ - lib/active_record/composite_key_preloader.rb
29
+ - lib/active_record/smart_preloader.rb
30
+ - smart_preloader.gemspec
31
+ homepage: https://github.com/2rba/smart_preloader
32
+ licenses:
33
+ - MIT
34
+ metadata: {}
35
+ post_install_message:
36
+ rdoc_options: []
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 2.4.0
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ requirements: []
50
+ rubygems_version: 3.2.22
51
+ signing_key:
52
+ specification_version: 4
53
+ summary: Allows to preload associations in a smart way
54
+ test_files: []