batch-loader-active-record 0.3.0 → 0.3.1
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 +4 -4
- data/CHANGELOG.md +5 -1
- data/Gemfile.lock +1 -1
- data/README.md +9 -3
- data/lib/batch_loader_active_record.rb +25 -112
- data/lib/batch_loader_active_record/association_manager.rb +119 -0
- data/lib/batch_loader_active_record/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 20d915e2884c4153776b3acac5c148360e66209b
|
4
|
+
data.tar.gz: 7580228d4074580c688d5561ad4a3c69e599738e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 923e068adecf32ee7df60441628672ec324898ba36d5c29f866042d8ec4e6af692b19a0fa9b8c483dfdeee04066d7cf455f22c279023dd10adc997a8fa867e40
|
7
|
+
data.tar.gz: f505bc5a8cfff63725a04b756f8f022c6bc95543ae734a6c8fe49e86169fb6ec18655122954cef56b5724b4ae5fe8a3079bbebc13e10bc5732bcd6cfc2fec8d7
|
data/CHANGELOG.md
CHANGED
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -12,19 +12,25 @@ It is not intended to be used for all associations though, but only where necess
|
|
12
12
|
|
13
13
|
## Description
|
14
14
|
|
15
|
-
This
|
15
|
+
This gem has a very simple implementation and delegates all batch loading responsibilities (used to avoid N+1 calls to the database) to the [batch-loader gem](https://github.com/exAspArk/batch-loader). It allows to generate a lazy association accessor with a simple statement: `association_accessor :association_name`.
|
16
|
+
|
17
|
+
Note that it doesn't yet support all cases handled by Active Record associations, refer to the [CHANGELOG](https://github.com/mathieul/batch-loader-active-record/blob/master/CHANGELOG.md) to know what is supported and what is not.
|
18
|
+
|
19
|
+
It is also possible to use one of the macros below in replacement of the original Active Record macro to both declare the association and trigger a lazy association accessort in a single statement.
|
16
20
|
|
17
21
|
* `belongs_to_lazy`
|
18
22
|
* `has_one_lazy`
|
19
23
|
* `has_many_lazy`
|
20
24
|
|
21
|
-
|
25
|
+
As soon as your lazy association accessor needs to do more than fetch all records of an association (using a scope or not), you're going to want to directly use the batch-loader gem. For more details on N+1 queries read the [batch-loader gem README](https://github.com/exAspArk/batch-loader/#why).
|
22
26
|
|
23
27
|
For example let's imagine a post which can have many comments:
|
24
28
|
|
25
29
|
```ruby
|
26
30
|
class Post < ActiveRecord::Base
|
27
|
-
|
31
|
+
include BatchLoaderActiveRecord
|
32
|
+
has_many :comments
|
33
|
+
association_accessor :comments
|
28
34
|
end
|
29
35
|
|
30
36
|
class Comment < ActiveRecord::Base
|
@@ -1,7 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "batch_loader_active_record/version"
|
4
3
|
require "batch-loader"
|
4
|
+
require "batch_loader_active_record/version"
|
5
|
+
require "batch_loader_active_record/association_manager"
|
5
6
|
|
6
7
|
module BatchLoaderActiveRecord
|
7
8
|
def self.included(base)
|
@@ -11,130 +12,42 @@ module BatchLoaderActiveRecord
|
|
11
12
|
module ClassMethods
|
12
13
|
def belongs_to_lazy(*args)
|
13
14
|
belongs_to(*args).tap do |reflections|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
15
|
+
manager = AssociationManager.new(model: self, reflection: reflections.values.last)
|
16
|
+
define_method(manager.accessor_name) { manager.belongs_to_batch_loader(self) }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def association_accessor(name)
|
21
|
+
reflection = reflect_on_association(name) or raise "Can't find association #{name.inspect}"
|
22
|
+
manager = AssociationManager.new(model: self, reflection: reflection)
|
23
|
+
case reflection.macro
|
24
|
+
when :belongs_to
|
25
|
+
define_method(manager.accessor_name) { manager.belongs_to_batch_loader(self) }
|
26
|
+
when :has_one
|
27
|
+
define_method(manager.accessor_name) { manager.has_one_to_batch_loader(self) }
|
28
|
+
when :has_many
|
29
|
+
define_method(manager.accessor_name) do |instance_scope = nil|
|
30
|
+
manager.has_many_to_batch_loader(self, instance_scope)
|
27
31
|
end
|
32
|
+
else
|
33
|
+
raise NotImplementedError, "association kind #{reflection.macro.inspect} is not yet supported"
|
28
34
|
end
|
29
35
|
end
|
30
36
|
|
31
37
|
def has_one_lazy(*args)
|
32
38
|
has_one(*args).tap do |reflections|
|
33
|
-
|
34
|
-
|
35
|
-
batch_key = [table_name, reflect.name]
|
36
|
-
define_method(:"#{reflect.name}_lazy") do
|
37
|
-
assoc_scope = if reflect.scope.nil?
|
38
|
-
reflect.klass
|
39
|
-
else
|
40
|
-
reflect.klass.instance_eval(&reflect.scope)
|
41
|
-
end
|
42
|
-
BatchLoader.for(id).batch(key: batch_key) do |model_ids, loader|
|
43
|
-
assoc_scope.where(reflect.foreign_key => model_ids).each do |instance|
|
44
|
-
loader.call(instance.public_send(reflect.foreign_key), instance)
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
39
|
+
manager = AssociationManager.new(model: self, reflection: reflections.values.last)
|
40
|
+
define_method(manager.accessor_name) { manager.has_one_to_batch_loader(self) }
|
48
41
|
end
|
49
42
|
end
|
50
43
|
|
51
44
|
def has_many_lazy(*args)
|
52
45
|
has_many(*args).tap do |reflections|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
define_method(:"#{reflect.name}_lazy") do |instance_scope = nil|
|
57
|
-
batch_key = base_key
|
58
|
-
batch_key += [instance_scope.to_sql.hash] unless instance_scope.nil?
|
59
|
-
assoc_scope = if reflect.scope.nil?
|
60
|
-
reflect.klass
|
61
|
-
else
|
62
|
-
reflect.klass.instance_eval(&reflect.scope)
|
63
|
-
end
|
64
|
-
BatchLoader.for(id).batch(default_value: [], key: batch_key) do |model_ids, loader|
|
65
|
-
relation = if instance_scope.nil?
|
66
|
-
assoc_scope
|
67
|
-
else
|
68
|
-
assoc_scope.instance_eval { instance_scope }
|
69
|
-
end
|
70
|
-
if reflect.through_reflection?
|
71
|
-
instances = self.class.fetch_for_model_ids(model_ids, relation: relation, reflection: reflect)
|
72
|
-
instances.each do |instance|
|
73
|
-
loader.call(instance.public_send(:_instance_id)) { |value| value << instance }
|
74
|
-
end
|
75
|
-
else
|
76
|
-
relation.where(reflect.foreign_key => model_ids).each do |instance|
|
77
|
-
loader.call(instance.public_send(reflect.foreign_key)) { |value| value << instance }
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
def fetch_for_model_ids(ids, relation:, reflection:)
|
86
|
-
instance_id_path = "#{reflection.active_record.table_name}.#{reflection.active_record.primary_key}"
|
87
|
-
model_class = reflection.active_record
|
88
|
-
reflections = reflection_chain(reflection)
|
89
|
-
join_strings = [reflection_join(reflections.first, relation)]
|
90
|
-
reflections.each_cons(2) do |previous, current|
|
91
|
-
join_strings << reflection_join(current, previous.active_record)
|
92
|
-
end
|
93
|
-
select_relation = join_strings.reduce(relation) do |select_relation, join_string|
|
94
|
-
select_relation.joins(join_string)
|
95
|
-
end
|
96
|
-
select_relation
|
97
|
-
.where("#{model_class.table_name}.#{model_class.primary_key} IN (?)", ids)
|
98
|
-
.select("#{relation.table_name}.*, #{instance_id_path} AS _instance_id")
|
99
|
-
end
|
100
|
-
|
101
|
-
private
|
102
|
-
|
103
|
-
def assert_not_polymorphic(reflection)
|
104
|
-
if reflection.polymorphic? || reflection.options.has_key?(:as) || reflection.options.has_key?(:source_type)
|
105
|
-
raise NotImplementedError, "polymorphic associations are not yet supported (#{reflection.name})"
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
def reflection_chain(reflection)
|
110
|
-
reflections = [reflection]
|
111
|
-
begin
|
112
|
-
previous = reflection
|
113
|
-
reflection = previous.source_reflection
|
114
|
-
if reflection && reflection != previous
|
115
|
-
reflections << reflection
|
116
|
-
else
|
117
|
-
reflection = nil
|
46
|
+
manager = AssociationManager.new(model: self, reflection: reflections.values.last)
|
47
|
+
define_method(manager.accessor_name) do |instance_scope = nil|
|
48
|
+
manager.has_many_to_batch_loader(self, instance_scope)
|
118
49
|
end
|
119
|
-
end while reflection
|
120
|
-
reflections.reverse
|
121
|
-
end
|
122
|
-
|
123
|
-
def reflection_join(orig_reflection, model_class)
|
124
|
-
reflection = orig_reflection.through_reflection? ? orig_reflection.through_reflection : orig_reflection
|
125
|
-
id_path = id_path_for(reflection, model_class)
|
126
|
-
table_name = reflection.active_record.table_name
|
127
|
-
id_column = reflection.belongs_to? ? reflection.foreign_key : reflection.active_record.primary_key
|
128
|
-
"INNER JOIN #{table_name} ON #{table_name}.#{id_column} = #{id_path}"
|
129
|
-
end
|
130
|
-
|
131
|
-
def id_path_for(reflection, model_class)
|
132
|
-
id_column = if reflection.belongs_to?
|
133
|
-
model_class.primary_key
|
134
|
-
else
|
135
|
-
reflection.foreign_key
|
136
50
|
end
|
137
|
-
"#{model_class.table_name}.#{id_column}"
|
138
51
|
end
|
139
52
|
end
|
140
53
|
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
module BatchLoaderActiveRecord
|
2
|
+
class AssociationManager
|
3
|
+
attr_reader :model, :reflection
|
4
|
+
|
5
|
+
def initialize(model:, reflection:)
|
6
|
+
@model = model
|
7
|
+
@reflection = reflection
|
8
|
+
assert_not_polymorphic
|
9
|
+
end
|
10
|
+
|
11
|
+
def accessor_name
|
12
|
+
:"#{reflection.name}_lazy"
|
13
|
+
end
|
14
|
+
|
15
|
+
def belongs_to_batch_loader(instance)
|
16
|
+
foreign_key_value = instance.send(reflection.foreign_key) or return nil
|
17
|
+
BatchLoader.for(foreign_key_value).batch(key: batch_key) do |foreign_key_values, loader|
|
18
|
+
target_scope.where(id: foreign_key_values).each { |instance| loader.call(instance.id, instance) }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def has_one_to_batch_loader(instance)
|
23
|
+
BatchLoader.for(instance.id).batch(key: batch_key) do |model_ids, loader|
|
24
|
+
target_scope.where(reflection.foreign_key => model_ids).each do |instance|
|
25
|
+
loader.call(instance.public_send(reflection.foreign_key), instance)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def has_many_to_batch_loader(instance, instance_scope)
|
31
|
+
custom_key = batch_key
|
32
|
+
custom_key += [instance_scope.to_sql.hash] unless instance_scope.nil?
|
33
|
+
BatchLoader.for(instance.id).batch(default_value: [], key: custom_key) do |model_ids, loader|
|
34
|
+
relation = if instance_scope.nil?
|
35
|
+
target_scope
|
36
|
+
else
|
37
|
+
target_scope.instance_eval { instance_scope }
|
38
|
+
end
|
39
|
+
if reflection.through_reflection?
|
40
|
+
instances = fetch_for_model_ids(model_ids, relation: relation)
|
41
|
+
instances.each do |instance|
|
42
|
+
loader.call(instance.public_send(:_instance_id)) { |value| value << instance }
|
43
|
+
end
|
44
|
+
else
|
45
|
+
relation.where(reflection.foreign_key => model_ids).each do |instance|
|
46
|
+
loader.call(instance.public_send(reflection.foreign_key)) { |value| value << instance }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def target_scope
|
55
|
+
@target_scope ||= if reflection.scope.nil?
|
56
|
+
reflection.klass
|
57
|
+
else
|
58
|
+
reflection.klass.instance_eval(&reflection.scope)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def batch_key
|
63
|
+
@batch_key ||= [model.table_name, reflection.name].freeze
|
64
|
+
end
|
65
|
+
|
66
|
+
def assert_not_polymorphic
|
67
|
+
if reflection.polymorphic? || reflection.options.has_key?(:as) || reflection.options.has_key?(:source_type)
|
68
|
+
raise NotImplementedError, "polymorphic associations are not yet supported (#{reflection.name})"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def fetch_for_model_ids(ids, relation:)
|
73
|
+
instance_id_path = "#{reflection.active_record.table_name}.#{reflection.active_record.primary_key}"
|
74
|
+
model_class = reflection.active_record
|
75
|
+
reflections = reflection_chain(reflection)
|
76
|
+
join_strings = [reflection_join(reflections.first, relation)]
|
77
|
+
reflections.each_cons(2) do |previous, current|
|
78
|
+
join_strings << reflection_join(current, previous.active_record)
|
79
|
+
end
|
80
|
+
select_relation = join_strings.reduce(relation) do |select_relation, join_string|
|
81
|
+
select_relation.joins(join_string)
|
82
|
+
end
|
83
|
+
select_relation
|
84
|
+
.where("#{model_class.table_name}.#{model_class.primary_key} IN (?)", ids)
|
85
|
+
.select("#{relation.table_name}.*, #{instance_id_path} AS _instance_id")
|
86
|
+
end
|
87
|
+
|
88
|
+
def reflection_chain(reflection)
|
89
|
+
reflections = [reflection]
|
90
|
+
begin
|
91
|
+
previous = reflection
|
92
|
+
reflection = previous.source_reflection
|
93
|
+
if reflection && reflection != previous
|
94
|
+
reflections << reflection
|
95
|
+
else
|
96
|
+
reflection = nil
|
97
|
+
end
|
98
|
+
end while reflection
|
99
|
+
reflections.reverse
|
100
|
+
end
|
101
|
+
|
102
|
+
def reflection_join(orig_reflection, model_class)
|
103
|
+
reflection = orig_reflection.through_reflection? ? orig_reflection.through_reflection : orig_reflection
|
104
|
+
id_path = id_path_for(reflection, model_class)
|
105
|
+
table_name = reflection.active_record.table_name
|
106
|
+
id_column = reflection.belongs_to? ? reflection.foreign_key : reflection.active_record.primary_key
|
107
|
+
"INNER JOIN #{table_name} ON #{table_name}.#{id_column} = #{id_path}"
|
108
|
+
end
|
109
|
+
|
110
|
+
def id_path_for(reflection, model_class)
|
111
|
+
id_column = if reflection.belongs_to?
|
112
|
+
model_class.primary_key
|
113
|
+
else
|
114
|
+
reflection.foreign_key
|
115
|
+
end
|
116
|
+
"#{model_class.table_name}.#{id_column}"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: batch-loader-active-record
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- mathieul
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-11-
|
11
|
+
date: 2017-11-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: batch-loader
|
@@ -157,6 +157,7 @@ files:
|
|
157
157
|
- bin/setup
|
158
158
|
- lib/batch-loader-active-record.rb
|
159
159
|
- lib/batch_loader_active_record.rb
|
160
|
+
- lib/batch_loader_active_record/association_manager.rb
|
160
161
|
- lib/batch_loader_active_record/version.rb
|
161
162
|
homepage: https://github.com/mathieul/batch-loader-active-record
|
162
163
|
licenses:
|