batch-loader-active-record 0.3.0 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|