deep_preloader 1.0.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/.circleci/config.yml +103 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.travis.yml +9 -0
- data/Appraisals +7 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +4 -0
- data/Rakefile +7 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/deep_preloader.gemspec +34 -0
- data/gemfiles/activerecord_5_2.gemfile +8 -0
- data/gemfiles/activerecord_6_0_beta.gemfile +8 -0
- data/lib/deep_preloader/abstract_spec.rb +5 -0
- data/lib/deep_preloader/polymorphic_spec.rb +56 -0
- data/lib/deep_preloader/spec.rb +63 -0
- data/lib/deep_preloader/version.rb +3 -0
- data/lib/deep_preloader.rb +271 -0
- metadata +216 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 60cc928760d63a608b81e43c8888d1ba827bd82fe928ee8d3ece46f06306e885
|
4
|
+
data.tar.gz: 0dcec8c15e0dd6f51127ba9b8f868091ce775a67fc5585786b446ac673875b50
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e96854b66b2aeda406e0f7b6da7d7a61b6de93da257b1aeb4392b7710182cdc861574871336278c743e614f47c20bc15e10251538ea3214c03a89c09c6dc3307
|
7
|
+
data.tar.gz: 2aa7588b8d4ab4a2052866ef96b93a46c6f2ac0ed9c6c10aed86088b4b8a13a3d102da7734d240ac0ec33d38ed3bc4872724f44574c0c2fedc8f8b693d95f4e9
|
@@ -0,0 +1,103 @@
|
|
1
|
+
version: 2.1
|
2
|
+
|
3
|
+
executors:
|
4
|
+
ruby:
|
5
|
+
parameters:
|
6
|
+
ruby-version:
|
7
|
+
type: string
|
8
|
+
default: "2.6"
|
9
|
+
gemfile:
|
10
|
+
type: string
|
11
|
+
default: "Gemfile"
|
12
|
+
docker:
|
13
|
+
- image: circleci/ruby:<< parameters.ruby-version >>
|
14
|
+
environment:
|
15
|
+
BUNDLE_JOBS: 3
|
16
|
+
BUNDLE_RETRY: 3
|
17
|
+
BUNDLE_PATH: vendor/bundle
|
18
|
+
RAILS_ENV: test
|
19
|
+
BUNDLE_GEMFILE: << parameters.gemfile >>
|
20
|
+
|
21
|
+
jobs:
|
22
|
+
test:
|
23
|
+
parameters:
|
24
|
+
ruby-version:
|
25
|
+
type: string
|
26
|
+
gemfile:
|
27
|
+
type: string
|
28
|
+
executor:
|
29
|
+
name: ruby
|
30
|
+
ruby-version: << parameters.ruby-version >>
|
31
|
+
gemfile: << parameters.gemfile >>
|
32
|
+
parallelism: 1
|
33
|
+
steps:
|
34
|
+
- checkout
|
35
|
+
|
36
|
+
- run:
|
37
|
+
# Remove the non-appraisal gemfile for safety: we never want to use it.
|
38
|
+
name: Prepare bundler
|
39
|
+
command: bundle -v
|
40
|
+
|
41
|
+
- run:
|
42
|
+
name: Compute a gemfile lock
|
43
|
+
command: bundle lock && cp "${BUNDLE_GEMFILE}.lock" /tmp/gem-lock
|
44
|
+
|
45
|
+
- restore_cache:
|
46
|
+
keys:
|
47
|
+
- deep_preloader-<< parameters.ruby-version >>-{{ checksum "/tmp/gem-lock" }}
|
48
|
+
- deep_preloader-
|
49
|
+
|
50
|
+
- run:
|
51
|
+
name: Bundle Install
|
52
|
+
command: bundle check || bundle install
|
53
|
+
|
54
|
+
- save_cache:
|
55
|
+
key: deep_preloader-<< parameters.ruby-version >>-{{ checksum "/tmp/gem-lock" }}
|
56
|
+
paths:
|
57
|
+
- vendor/bundle
|
58
|
+
|
59
|
+
- run:
|
60
|
+
name: Run rspec
|
61
|
+
command: bundle exec rspec --profile 10 --format RspecJunitFormatter --out test_results/rspec.xml --format progress
|
62
|
+
|
63
|
+
- store_test_results:
|
64
|
+
path: test_results
|
65
|
+
|
66
|
+
publish:
|
67
|
+
executor: ruby
|
68
|
+
steps:
|
69
|
+
- checkout
|
70
|
+
- run:
|
71
|
+
name: Setup Rubygems
|
72
|
+
command: |
|
73
|
+
mkdir ~/.gem &&
|
74
|
+
echo -e "---\r\n:rubygems_api_key: $RUBYGEMS_API_KEY" > ~/.gem/credentials &&
|
75
|
+
chmod 0600 ~/.gem/credentials
|
76
|
+
- run:
|
77
|
+
name: Publish to Rubygems
|
78
|
+
command: |
|
79
|
+
gem build deep_preloader.gemspec
|
80
|
+
gem push deep_preloader-*.gem
|
81
|
+
|
82
|
+
workflows:
|
83
|
+
version: 2.1
|
84
|
+
build:
|
85
|
+
jobs:
|
86
|
+
- test:
|
87
|
+
name: 'ruby 2.5 ActiveRecord 5.2'
|
88
|
+
ruby-version: "2.5"
|
89
|
+
gemfile: gemfiles/activerecord_5_2.gemfile
|
90
|
+
- test:
|
91
|
+
name: 'ruby 2.6 ActiveRecord 5.2'
|
92
|
+
ruby-version: "2.6"
|
93
|
+
gemfile: gemfiles/activerecord_5_2.gemfile
|
94
|
+
- test:
|
95
|
+
name: 'ruby 2.6 ActiveRecord 6.0-beta'
|
96
|
+
ruby-version: "2.6"
|
97
|
+
gemfile: gemfiles/activerecord_6_0_beta.gemfile
|
98
|
+
- publish:
|
99
|
+
filters:
|
100
|
+
branches:
|
101
|
+
only: master
|
102
|
+
tags:
|
103
|
+
ignore: /.*/
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Appraisals
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 DMM.com
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "deep_preloader"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'deep_preloader/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "deep_preloader"
|
8
|
+
spec.version = DeepPreloader::VERSION
|
9
|
+
spec.authors = ["iKnow Team"]
|
10
|
+
spec.email = ["dev@iknow.jp"]
|
11
|
+
|
12
|
+
spec.summary = %q{Explicit preloader for ActiveRecord}
|
13
|
+
spec.homepage = "http://github.com/iknow/deep_preloader"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = "exe"
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "activerecord", '>= 5.0.0'
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler"
|
24
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
25
|
+
spec.add_development_dependency "rspec"
|
26
|
+
spec.add_development_dependency "database_cleaner"
|
27
|
+
spec.add_development_dependency "minitest"
|
28
|
+
|
29
|
+
spec.add_development_dependency "byebug"
|
30
|
+
spec.add_development_dependency "pry"
|
31
|
+
spec.add_development_dependency "method_source"
|
32
|
+
spec.add_development_dependency "appraisal"
|
33
|
+
spec.add_development_dependency "sqlite3"
|
34
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'deep_preloader/abstract_spec'
|
2
|
+
|
3
|
+
class DeepPreloader::PolymorphicSpec < DeepPreloader::AbstractSpec
|
4
|
+
attr_reader :specs_by_type
|
5
|
+
|
6
|
+
def self.parse(data)
|
7
|
+
if data.is_a?(Hash)
|
8
|
+
specs = data.each_with_object({}) do |(k, v), h|
|
9
|
+
h[k.to_s] = DeepPreloader::Spec.parse(v)
|
10
|
+
end
|
11
|
+
self.new(specs)
|
12
|
+
else
|
13
|
+
raise ArgumentError.new("Invalid polymorphic spec: '#{data.inspect}' is not a hash")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(specs_by_type = {})
|
18
|
+
@specs_by_type = specs_by_type
|
19
|
+
end
|
20
|
+
|
21
|
+
def for_type(clazz)
|
22
|
+
specs_by_type[clazz.name]
|
23
|
+
end
|
24
|
+
|
25
|
+
def merge!(other)
|
26
|
+
case other
|
27
|
+
when nil
|
28
|
+
return
|
29
|
+
when DeepPreloader::PolymorphicSpec
|
30
|
+
other.specs_by_type.each do |k, v|
|
31
|
+
if specs_by_type[k]
|
32
|
+
specs_by_type[k].merge!(v)
|
33
|
+
else
|
34
|
+
specs_by_type[k] = v
|
35
|
+
end
|
36
|
+
end
|
37
|
+
else
|
38
|
+
raise ArgumentError.new("Cannot merge #{other.class.name} into #{self.inspect}")
|
39
|
+
end
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
def hash
|
44
|
+
[self.class, self.specs_by_type].hash
|
45
|
+
end
|
46
|
+
|
47
|
+
def ==(other)
|
48
|
+
self.class == other.class && self.specs_by_type == other.specs_by_type
|
49
|
+
end
|
50
|
+
|
51
|
+
alias eql? ==
|
52
|
+
|
53
|
+
def inspect
|
54
|
+
"PolySpec#{specs_by_type.inspect}"
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'deep_preloader/abstract_spec'
|
2
|
+
|
3
|
+
class DeepPreloader::Spec < DeepPreloader::AbstractSpec
|
4
|
+
attr_reader :association_specs
|
5
|
+
|
6
|
+
def self.parse(data)
|
7
|
+
case data
|
8
|
+
when Array
|
9
|
+
data.inject(self.new) do |acc, v|
|
10
|
+
acc.merge!(parse(v))
|
11
|
+
end
|
12
|
+
when Hash
|
13
|
+
assoc_specs = data.each_with_object({}) do |(k, v), h|
|
14
|
+
h[k.to_sym] = parse(v)
|
15
|
+
end
|
16
|
+
self.new(assoc_specs)
|
17
|
+
when String, Symbol
|
18
|
+
self.new({ data.to_sym => nil })
|
19
|
+
when DeepPreloader::AbstractSpec
|
20
|
+
data
|
21
|
+
when nil
|
22
|
+
nil
|
23
|
+
else
|
24
|
+
raise ArgumentError.new("Cannot parse invalid hash preload spec: #{hash.inspect}")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(association_specs = {})
|
29
|
+
@association_specs = association_specs
|
30
|
+
end
|
31
|
+
|
32
|
+
def merge!(other)
|
33
|
+
case other
|
34
|
+
when nil
|
35
|
+
return
|
36
|
+
when DeepPreloader::Spec
|
37
|
+
other.association_specs.each do |k, v|
|
38
|
+
if association_specs[k]
|
39
|
+
association_specs[k].merge!(v)
|
40
|
+
else
|
41
|
+
association_specs[k] = v
|
42
|
+
end
|
43
|
+
end
|
44
|
+
else
|
45
|
+
raise ArgumentError.new("Cannot merge #{other.class.name} into #{self.inspect}")
|
46
|
+
end
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
def hash
|
51
|
+
[self.class, self.association_specs].hash
|
52
|
+
end
|
53
|
+
|
54
|
+
def ==(other)
|
55
|
+
self.class == other.class && self.association_specs == other.association_specs
|
56
|
+
end
|
57
|
+
|
58
|
+
alias eql? ==
|
59
|
+
|
60
|
+
def inspect
|
61
|
+
"Spec#{association_specs.inspect}"
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,271 @@
|
|
1
|
+
require "deep_preloader/version"
|
2
|
+
require "deep_preloader/spec"
|
3
|
+
require "deep_preloader/polymorphic_spec"
|
4
|
+
require "active_record"
|
5
|
+
|
6
|
+
DEBUG = ENV['DEBUG'].present?
|
7
|
+
|
8
|
+
class DeepPreloader
|
9
|
+
def self.preload(models, spec, lock: nil)
|
10
|
+
return if spec.nil? || models.blank?
|
11
|
+
|
12
|
+
worker = PreloadWorker.new(lock: lock)
|
13
|
+
spec = Spec.parse(spec) unless spec.is_a?(AbstractSpec)
|
14
|
+
|
15
|
+
models_by_class = Array.wrap(models).group_by(&:class)
|
16
|
+
|
17
|
+
case spec
|
18
|
+
when Spec
|
19
|
+
unless models_by_class.size == 1
|
20
|
+
raise ArgumentError.new("Provided multiple model types to non-polymorphic preload spec")
|
21
|
+
end
|
22
|
+
|
23
|
+
model_class, models = models_by_class.first
|
24
|
+
worker.add_associations_from_spec(models, model_class, spec)
|
25
|
+
when PolymorphicSpec
|
26
|
+
models_by_class.each do |model_class, models|
|
27
|
+
model_spec = spec.for_type(model_class)
|
28
|
+
next unless model_spec
|
29
|
+
worker.add_associations_from_spec(models, model_class, model_spec)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
worker.run!
|
34
|
+
models
|
35
|
+
end
|
36
|
+
|
37
|
+
class PreloadWorker
|
38
|
+
def initialize(lock:)
|
39
|
+
@lock = lock
|
40
|
+
@worklist = {}
|
41
|
+
end
|
42
|
+
|
43
|
+
def add_associations_from_spec(models, model_class, spec)
|
44
|
+
spec.association_specs.each do |association_name, child_spec|
|
45
|
+
association_reflection = model_class.reflect_on_association(association_name)
|
46
|
+
if association_reflection.nil?
|
47
|
+
raise ArgumentError.new("Preloading error: couldn't find association #{association_name} on model class #{model_class.name}")
|
48
|
+
end
|
49
|
+
|
50
|
+
if association_reflection.polymorphic?
|
51
|
+
add_polymorphic_association(models, association_reflection, child_spec)
|
52
|
+
else
|
53
|
+
add_association(models, association_reflection, child_spec)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def run!
|
59
|
+
while(@worklist.present?)
|
60
|
+
context, entries = @worklist.shift
|
61
|
+
ActiveRecord::Base.logger.debug("Preloading children in context #{context}") if DEBUG
|
62
|
+
|
63
|
+
loaded_entries, unloaded_entries = entries.partition(&:loaded?)
|
64
|
+
|
65
|
+
unloaded_keys = unloaded_entries.map(&:key).to_set.delete(nil)
|
66
|
+
|
67
|
+
ActiveRecord::Base.logger.debug("Need to load children for following keys: #{unloaded_keys.to_a}") if DEBUG
|
68
|
+
|
69
|
+
found_children = {}
|
70
|
+
|
71
|
+
if unloaded_keys.present?
|
72
|
+
# When :belongs_to, children could be shared with already loaded
|
73
|
+
# entries - use what we already have.
|
74
|
+
loaded_entries.each do |entry|
|
75
|
+
next unless entry.belongs_to?
|
76
|
+
|
77
|
+
if entry.belongs_to? && unloaded_keys.delete?(entry.key)
|
78
|
+
found_children[entry.key] = entry.children
|
79
|
+
end
|
80
|
+
end
|
81
|
+
ActiveRecord::Base.logger.debug("found loaded children for keys #{found_children.keys}") if DEBUG
|
82
|
+
end
|
83
|
+
|
84
|
+
if unloaded_keys.present?
|
85
|
+
fetched_children = context.load_children(unloaded_keys.to_a, lock: @lock)
|
86
|
+
ActiveRecord::Base.logger.debug("fetched children for keys #{fetched_children.keys}") if DEBUG
|
87
|
+
found_children.merge!(fetched_children)
|
88
|
+
end
|
89
|
+
|
90
|
+
unloaded_entries.each do |entry|
|
91
|
+
children = found_children.fetch(entry.key, [])
|
92
|
+
entry.children = children
|
93
|
+
end
|
94
|
+
|
95
|
+
entries.each do |entry|
|
96
|
+
children = entry.children
|
97
|
+
child_spec = entry.child_spec
|
98
|
+
next unless child_spec && children.present?
|
99
|
+
child_class = children.first.class # children of a given parent are all of the same type
|
100
|
+
add_associations_from_spec(children, child_class, child_spec)
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def add_polymorphic_association(models, association_reflection, polymorphic_child_spec)
|
110
|
+
assoc_name = association_reflection.name
|
111
|
+
|
112
|
+
# If a model belongs_to a polymorphic child, we know what type it is.
|
113
|
+
# Group models by the type of their associated child and add each
|
114
|
+
# separately.
|
115
|
+
models_by_child_class = models.group_by { |m| m.association(assoc_name).klass }
|
116
|
+
|
117
|
+
# For models with no child there's nothing to preload, but we still need
|
118
|
+
# to set the association target. Since we can't infer a class for
|
119
|
+
# `add_association`, set it up here.
|
120
|
+
models_by_child_class.delete(nil)&.each do |model|
|
121
|
+
model.association(assoc_name).loaded!
|
122
|
+
end
|
123
|
+
|
124
|
+
models_by_child_class.each do |child_class, child_class_models|
|
125
|
+
child_preload_spec = polymorphic_child_spec&.for_type(child_class)
|
126
|
+
add_association(child_class_models, association_reflection, child_preload_spec, type: child_class)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def add_association(models, association_reflection, child_preload_spec, type: association_reflection.klass)
|
131
|
+
key_col = child_key_column(association_reflection)
|
132
|
+
child_constraints = child_constraints(association_reflection)
|
133
|
+
|
134
|
+
context = WorklistContext.new(type, key_col, child_constraints)
|
135
|
+
models.each do |model|
|
136
|
+
entry = WorklistEntry.new(model, association_reflection, child_preload_spec)
|
137
|
+
worklist_add(context, entry)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def worklist_add(key, entry)
|
142
|
+
(@worklist[key] ||= []) << entry
|
143
|
+
end
|
144
|
+
|
145
|
+
def child_constraints(association_reflection)
|
146
|
+
constraints = []
|
147
|
+
if association_reflection.options[:as]
|
148
|
+
# each parent model is pointed to from a child type that could also belong to other types of parent. Constrain the search to this parent.
|
149
|
+
constraints << [association_reflection.type, association_reflection.active_record.base_class.sti_name]
|
150
|
+
end
|
151
|
+
|
152
|
+
unless association_reflection.constraints.blank?
|
153
|
+
raise ArgumentError.new("Preloading conditional associations not supported: #{association_reflection.name}")
|
154
|
+
end
|
155
|
+
|
156
|
+
unless association_reflection.scope.blank?
|
157
|
+
raise ArgumentError.new("Preloading scoped associations not supported: #{association_reflection.name}")
|
158
|
+
end
|
159
|
+
|
160
|
+
constraints
|
161
|
+
end
|
162
|
+
|
163
|
+
def child_key_column(association_reflection)
|
164
|
+
case association_reflection.macro
|
165
|
+
when :belongs_to
|
166
|
+
association_reflection.active_record_primary_key
|
167
|
+
when :has_one, :has_many
|
168
|
+
association_reflection.foreign_key
|
169
|
+
else
|
170
|
+
raise "Unsupported association type #{association_reflection.macro}"
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# entries need to be grouped by:
|
176
|
+
# child_type - look up in same table
|
177
|
+
# child_key - compare the same keys
|
178
|
+
# child_search_constraints - where constraints on child lookup such as polymorphic type or association scope.
|
179
|
+
WorklistContext = Struct.new(:child_type, :child_key_column, :child_constraints) do
|
180
|
+
def load_children(keys, lock: nil)
|
181
|
+
scope = child_constraints.inject(child_type.unscoped) do |sc, (col, val)|
|
182
|
+
sc.where(col => val)
|
183
|
+
end
|
184
|
+
|
185
|
+
if lock
|
186
|
+
scope = scope.order(:id).lock(lock)
|
187
|
+
end
|
188
|
+
|
189
|
+
scope.where(child_key_column => keys).group_by { |c| c.read_attribute(child_key_column) }
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
class WorklistEntry
|
194
|
+
attr_reader :model, :association_reflection, :child_spec
|
195
|
+
|
196
|
+
def initialize(model, association_reflection, child_spec)
|
197
|
+
@model = model
|
198
|
+
@association_reflection = association_reflection
|
199
|
+
@child_spec = child_spec
|
200
|
+
end
|
201
|
+
|
202
|
+
def association_name
|
203
|
+
@association_reflection.name
|
204
|
+
end
|
205
|
+
|
206
|
+
def association_macro
|
207
|
+
@association_reflection.macro
|
208
|
+
end
|
209
|
+
|
210
|
+
def loaded?
|
211
|
+
model.association(association_name).loaded?
|
212
|
+
end
|
213
|
+
|
214
|
+
def belongs_to?
|
215
|
+
association_macro == :belongs_to
|
216
|
+
end
|
217
|
+
|
218
|
+
def collection?
|
219
|
+
association_macro == :has_many
|
220
|
+
end
|
221
|
+
|
222
|
+
def key
|
223
|
+
model.read_attribute(parent_key_column)
|
224
|
+
end
|
225
|
+
|
226
|
+
# Conceal the difference between singular and collection associations so
|
227
|
+
# that `load_children` can always `group_by` the key
|
228
|
+
def children
|
229
|
+
target = model.association(association_name).target
|
230
|
+
|
231
|
+
if collection?
|
232
|
+
target
|
233
|
+
elsif target
|
234
|
+
[target]
|
235
|
+
else
|
236
|
+
[]
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def children=(targets)
|
241
|
+
if collection?
|
242
|
+
target = targets
|
243
|
+
else
|
244
|
+
if targets.size > 1
|
245
|
+
raise RuntimeError.new("Internal preloader error: attempted to attach multiple children to a singular association")
|
246
|
+
end
|
247
|
+
target = targets.first
|
248
|
+
end
|
249
|
+
|
250
|
+
ActiveRecord::Base.logger.debug("attaching children to #{model.inspect}.#{association_name}: #{targets}") if DEBUG
|
251
|
+
|
252
|
+
association = model.association(association_name)
|
253
|
+
association.loaded!
|
254
|
+
association.target = target
|
255
|
+
targets.each { |t| association.set_inverse_instance(t) }
|
256
|
+
targets
|
257
|
+
end
|
258
|
+
|
259
|
+
def parent_key_column
|
260
|
+
case association_macro
|
261
|
+
when :belongs_to
|
262
|
+
@association_reflection.foreign_key
|
263
|
+
when :has_one, :has_many
|
264
|
+
@association_reflection.active_record_primary_key
|
265
|
+
else
|
266
|
+
raise "Unsupported association type #{@association_reflection.macro}"
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
end
|
metadata
ADDED
@@ -0,0 +1,216 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: deep_preloader
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- iKnow Team
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-04-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 5.0.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 5.0.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: database_cleaner
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: minitest
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: byebug
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: pry
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: method_source
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: appraisal
|
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'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: sqlite3
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
description:
|
168
|
+
email:
|
169
|
+
- dev@iknow.jp
|
170
|
+
executables: []
|
171
|
+
extensions: []
|
172
|
+
extra_rdoc_files: []
|
173
|
+
files:
|
174
|
+
- ".circleci/config.yml"
|
175
|
+
- ".gitignore"
|
176
|
+
- ".rspec"
|
177
|
+
- ".travis.yml"
|
178
|
+
- Appraisals
|
179
|
+
- Gemfile
|
180
|
+
- LICENSE.txt
|
181
|
+
- README.md
|
182
|
+
- Rakefile
|
183
|
+
- bin/console
|
184
|
+
- bin/setup
|
185
|
+
- deep_preloader.gemspec
|
186
|
+
- gemfiles/activerecord_5_2.gemfile
|
187
|
+
- gemfiles/activerecord_6_0_beta.gemfile
|
188
|
+
- lib/deep_preloader.rb
|
189
|
+
- lib/deep_preloader/abstract_spec.rb
|
190
|
+
- lib/deep_preloader/polymorphic_spec.rb
|
191
|
+
- lib/deep_preloader/spec.rb
|
192
|
+
- lib/deep_preloader/version.rb
|
193
|
+
homepage: http://github.com/iknow/deep_preloader
|
194
|
+
licenses:
|
195
|
+
- MIT
|
196
|
+
metadata: {}
|
197
|
+
post_install_message:
|
198
|
+
rdoc_options: []
|
199
|
+
require_paths:
|
200
|
+
- lib
|
201
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
202
|
+
requirements:
|
203
|
+
- - ">="
|
204
|
+
- !ruby/object:Gem::Version
|
205
|
+
version: '0'
|
206
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
207
|
+
requirements:
|
208
|
+
- - ">="
|
209
|
+
- !ruby/object:Gem::Version
|
210
|
+
version: '0'
|
211
|
+
requirements: []
|
212
|
+
rubygems_version: 3.0.3
|
213
|
+
signing_key:
|
214
|
+
specification_version: 4
|
215
|
+
summary: Explicit preloader for ActiveRecord
|
216
|
+
test_files: []
|