smart_preloader 0.1.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 +7 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +34 -0
- data/LICENSE.txt +21 -0
- data/README.md +88 -0
- data/Rakefile +8 -0
- data/lib/active_record/composite_key.rb +11 -0
- data/lib/active_record/composite_key_preloader.rb +85 -0
- data/lib/active_record/smart_preloader.rb +43 -0
- data/smart_preloader.gemspec +23 -0
- metadata +54 -0
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
data/.rspec
ADDED
data/Gemfile
ADDED
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,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: []
|