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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 21895ec7eb240edbb662157510b7c1f2e0e09ece
4
- data.tar.gz: 73a9d404f37d37432b74dc34156614f591cfd4c5
3
+ metadata.gz: 20d915e2884c4153776b3acac5c148360e66209b
4
+ data.tar.gz: 7580228d4074580c688d5561ad4a3c69e599738e
5
5
  SHA512:
6
- metadata.gz: 5917a41861fd8b5bced3f6b22f5d981028615fb7e6a4a0f674fde403261d6d29da4dbb8d2327a873cae3eb750f5ffee034d819507ca004e67b44d3d4f2b9ebeb
7
- data.tar.gz: d03ff1d186f23a10d3f65766ab8ac57555e91b0bd2e7647751e3d3f0ece000c221a5313050abc2a9633f79a460056e4b2564e023053bd5fb9ce824a3fc85404b
6
+ metadata.gz: 923e068adecf32ee7df60441628672ec324898ba36d5c29f866042d8ec4e6af692b19a0fa9b8c483dfdeee04066d7cf455f22c279023dd10adc997a8fa867e40
7
+ data.tar.gz: f505bc5a8cfff63725a04b756f8f022c6bc95543ae734a6c8fe49e86169fb6ec18655122954cef56b5724b4ae5fe8a3079bbebc13e10bc5732bcd6cfc2fec8d7
@@ -2,7 +2,11 @@
2
2
 
3
3
  Unreleased
4
4
 
5
- * nothing yet
5
+ * none
6
+
7
+ v0.3.1
8
+
9
+ * allow to decouple declaring the assocation with Active Record DSL and generate a lazy association accessor with `association_accessor`
6
10
 
7
11
  v0.3.0
8
12
 
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- batch-loader-active-record (0.3.0)
4
+ batch-loader-active-record (0.3.1)
5
5
  activerecord (>= 4.2.0, < 5.2.0)
6
6
  activesupport (>= 4.2.0, < 5.2.0)
7
7
  batch-loader (~> 1.2.0)
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 is a very simple gem which is basically a mixin containg replacement macros for the 3 active record association macros (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):
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
- Those are intended to use in replacement of the original Active Record class methods when you also want to generate a method to avoid N+1 calls to the database when accessed for the elements of a collection. For more details on why N+1 queries are common when fetching associations read [the batch-loader gem README](https://github.com/exAspArk/batch-loader/#why).
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
- has_many_lazy :comments
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
- reflect = reflections.values.last
15
- assert_not_polymorphic(reflect)
16
- batch_key = [table_name, reflect.name]
17
- define_method(:"#{reflect.name}_lazy") do
18
- assoc_scope = if reflect.scope.nil?
19
- reflect.klass
20
- else
21
- reflect.klass.instance_eval(&reflect.scope)
22
- end
23
- foreign_key_value = send(reflect.foreign_key) or return nil
24
- BatchLoader.for(foreign_key_value).batch(key: batch_key) do |foreign_key_values, loader|
25
- assoc_scope.where(id: foreign_key_values).each { |instance| loader.call(instance.id, instance) }
26
- end
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
- reflect = reflections.values.last
34
- assert_not_polymorphic(reflect)
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
- reflect = reflections.values.last
54
- assert_not_polymorphic(reflect)
55
- base_key = [table_name, reflect.name]
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
@@ -1,3 +1,3 @@
1
1
  module BatchLoaderActiveRecord
2
- VERSION = "0.3.0"
2
+ VERSION = "0.3.1"
3
3
  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.0
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-24 00:00:00.000000000 Z
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: