eager_group 0.8.1 → 0.9.0

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
  SHA256:
3
- metadata.gz: 88912c5696afe9332d53ac15b2365d2e2d7112391672a2cba5db62012876a138
4
- data.tar.gz: a18a2d21cd4299a68774f3dbb62556c71f3096b3ebac28bebf7a767a875fc00f
3
+ metadata.gz: c8bad88cf4f0de57942e0477ee107ce8f40eeeded25b37def08ef560907ef143
4
+ data.tar.gz: f14470d5456dbce04be8fac648623fbc0e1dbb25a3e89af5ebe7bde9dd6a61d5
5
5
  SHA512:
6
- metadata.gz: af269ea9b3be9f9a613f56cf14e52a7b183b6d041583f02aa88945f65150f98147ed7271523b9f8ec0d36c622f42ae257f7137e9cf842b381f11cbd7cd6e50bf
7
- data.tar.gz: 2878aaefb0b9420b13531927cd46ad47894954c2999fe81d9bf853d7f2255ae97b42df8e56c43a840b5812d71ed1f535c391ccf605fbed99f08ed0769959eab8
6
+ metadata.gz: 6e026cf0d9f8a9839b498b995ba7d48ab24afe2dc48f3e7d41cba16069ac4bc9b3c15d202e7b9730859aa81f24c8cb77a9e2bc8572e720a58457cc7f89f171a0
7
+ data.tar.gz: 0033033e6b427479d0a49c2e2e1b1149f40a63a46f12aa42aa49f54fb91b27f84ee045aa96014c37b745fbad9b5ca84fab20b014ef521e478cbce058f681f803
@@ -0,0 +1,18 @@
1
+ name: test
2
+ on:
3
+ push:
4
+ branches:
5
+ - main
6
+ jobs:
7
+ test:
8
+ name: test
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v2
12
+ - name: Set up Ruby
13
+ uses: ruby/setup-ruby@v1
14
+ with:
15
+ ruby-version: 3.1
16
+ bundler-cache: true
17
+ - name: Run tests
18
+ run: bundle exec rspec
data/.gitignore CHANGED
@@ -7,3 +7,5 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ .idea
11
+ .tool-versions
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Next Release
2
2
 
3
+ ## 0.9.0 (12/04/2022)
4
+
5
+ * Support `has_many` through `belongs_to`
6
+
7
+ ## 0.8.2 (09/22/2022)
8
+
9
+ * Add MIT-LICENSE
10
+
3
11
  ## 0.8.1 (10/25/2019)
4
12
 
5
13
  * Fix for `has_many :through`
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2015 - 2022 Richard Huang (flyerhzm@gmail.com)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/eager_group.gemspec CHANGED
@@ -29,4 +29,5 @@ Gem::Specification.new do |spec|
29
29
  spec.add_development_dependency 'rake', '~> 10.0'
30
30
  spec.add_development_dependency 'rspec', '~> 3.3'
31
31
  spec.add_development_dependency 'sqlite3'
32
+ spec.add_development_dependency 'pry'
32
33
  end
@@ -9,12 +9,13 @@ module ActiveRecord
9
9
  end
10
10
 
11
11
  def eager_group(*args)
12
- check_if_method_has_arguments!('eager_group', args)
12
+ check_if_method_has_arguments!(__callee__, args)
13
+
13
14
  spawn.eager_group!(*args)
14
15
  end
15
16
 
16
17
  def eager_group!(*args)
17
- self.eager_group_values += args
18
+ self.eager_group_values |= args
18
19
  self
19
20
  end
20
21
 
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EagerGroup
4
+ class Preloader
5
+ class AggregationFinder
6
+ attr_reader :klass, :reflection, :definition, :arguments, :record_ids
7
+
8
+ def initialize(klass, definition, arguments, records)
9
+ @klass = klass
10
+ @definition = definition
11
+ @reflection = @klass.reflect_on_association(definition.association)
12
+ @arguments = arguments
13
+ @records = records
14
+ end
15
+
16
+ def definition_scope
17
+ reflection.klass.instance_exec(*arguments, &definition.scope) if definition.scope
18
+ end
19
+
20
+ def record_ids
21
+ @record_ids ||= @records.map { |record| record.send(group_by_key) }
22
+ end
23
+
24
+ def group_by_key
25
+ @klass.primary_key
26
+ end
27
+
28
+ private
29
+
30
+ def polymophic_as_condition
31
+ reflection.type ? { reflection.name => { reflection.type => @klass.base_class.name } } : []
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EagerGroup
4
+ class Preloader
5
+ class HasMany < AggregationFinder
6
+ def group_by_foreign_key
7
+ reflection.foreign_key
8
+ end
9
+
10
+ def aggregate_hash
11
+ scope = reflection.klass.all.tap{|query| query.merge!(definition_scope) if definition_scope }
12
+
13
+ scope.where(group_by_foreign_key => record_ids).
14
+ where(polymophic_as_condition).
15
+ group(group_by_foreign_key).
16
+ send(definition.aggregation_function, definition.column_name)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EagerGroup
4
+ class Preloader
5
+ class HasManyThroughBelongsTo < AggregationFinder
6
+ def group_by_foreign_key
7
+ "#{reflection.table_name}.#{reflection.through_reflection.klass.reflect_on_association(reflection.name).foreign_key}"
8
+ end
9
+
10
+ def aggregate_hash
11
+ scope = reflection.klass.all.tap{|query| query.merge!(definition_scope) if definition_scope }
12
+
13
+ scope.where(group_by_foreign_key => record_ids).
14
+ where(polymophic_as_condition).
15
+ group(group_by_foreign_key).
16
+ send(definition.aggregation_function, definition.column_name)
17
+ end
18
+
19
+ def group_by_key
20
+ reflection.through_reflection.foreign_key
21
+ end
22
+
23
+ def polymophic_as_condition
24
+ reflection.type ? { reflection.name => { reflection.type => reflection.through_reflection.klass.base_class.name } } : []
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EagerGroup
4
+ class Preloader
5
+ class HasManyThroughMany < AggregationFinder
6
+ def group_by_foreign_key
7
+ "#{reflection.through_reflection.name}.#{reflection.through_reflection.foreign_key}"
8
+ end
9
+
10
+ def aggregate_hash
11
+ scope = klass.joins(reflection.name).tap{|query| query.merge!(definition_scope) if definition_scope }
12
+
13
+ scope.where(group_by_foreign_key => record_ids).
14
+ where(polymophic_as_condition).
15
+ group(group_by_foreign_key).
16
+ send(definition.aggregation_function, definition.column_name)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EagerGroup
4
+ class Preloader
5
+ class ManyToMany < AggregationFinder
6
+ def group_by_foreign_key
7
+ "#{reflection.join_table}.#{reflection.foreign_key}"
8
+ end
9
+
10
+ def aggregate_hash
11
+ scope = klass.joins(reflection.name).tap{|query| query.merge!(definition_scope) if definition_scope}
12
+
13
+ scope.where(group_by_foreign_key => record_ids).
14
+ where(polymophic_as_condition).
15
+ group(group_by_foreign_key).
16
+ send(definition.aggregation_function, definition.column_name)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -2,15 +2,21 @@
2
2
 
3
3
  module EagerGroup
4
4
  class Preloader
5
+ autoload :AggregationFinder, 'eager_group/preloader/aggregation_finder'
6
+ autoload :HasMany, 'eager_group/preloader/has_many'
7
+ autoload :HasManyThroughBelongsTo, 'eager_group/preloader/has_many_through_belongs_to'
8
+ autoload :HasManyThroughMany, 'eager_group/preloader/has_many_through_many'
9
+ autoload :ManyToMany, 'eager_group/preloader/many_to_many'
10
+
5
11
  def initialize(klass, records, eager_group_values)
6
12
  @klass = klass
7
13
  @records = Array.wrap(records).compact.uniq
8
- @eager_group_values = eager_group_values
14
+ eager_group_definitions = @klass.eager_group_definitions
15
+ @eager_group_values = eager_group_values.all? { |value| eager_group_definitions.key?(value) } ? eager_group_values : [eager_group_values]
9
16
  end
10
17
 
11
18
  # Preload aggregate functions
12
19
  def run
13
- primary_key = @klass.primary_key
14
20
  @eager_group_values.each do |eager_group_value|
15
21
  definition_key, arguments =
16
22
  eager_group_value.is_a?(Array) ? [eager_group_value.shift, eager_group_value] : [eager_group_value, nil]
@@ -22,35 +28,44 @@ module EagerGroup
22
28
 
23
29
  @klass = @records.first.class
24
30
  end
25
- record_ids = @records.map { |record| record.send(primary_key) }
26
- unless definition = @klass.eager_group_definitions[definition_key]
27
- next
28
- end
29
31
 
30
- reflection = @klass.reflect_on_association(definition.association)
31
- association_class = reflection.klass
32
- association_class = association_class.instance_exec(*arguments, &definition.scope) if definition.scope
33
-
34
- foreign_key, aggregate_hash =
35
- if reflection.is_a?(ActiveRecord::Reflection::HasAndBelongsToManyReflection)
36
- ["#{reflection.join_table}.#{reflection.foreign_key}", @klass.joins(reflection.name)]
37
- elsif reflection.through_reflection
38
- ["#{reflection.through_reflection.name}.#{reflection.through_reflection.foreign_key}", @klass.joins(reflection.name)]
39
- else
40
- [reflection.foreign_key, association_class]
41
- end
42
- aggregate_hash = aggregate_hash.where(foreign_key => record_ids)
43
- .where(polymophic_as_condition(reflection))
44
- .group(foreign_key)
45
- .send(definition.aggregation_function, definition.column_name)
46
- if definition.need_load_object
47
- aggregate_objects = reflection.klass.find(aggregate_hash.values).each_with_object({}) { |o, h| h[o.id] = o }
48
- aggregate_hash.keys.each { |key| aggregate_hash[key] = aggregate_objects[aggregate_hash[key]] }
32
+ Array.wrap(definition_key).each do |key|
33
+ find_aggregate_values_per_definition!(key, arguments)
49
34
  end
50
- @records.each do |record|
51
- id = record.send(primary_key)
52
- record.send("#{definition_key}=", aggregate_hash[id] || definition.default_value)
35
+ end
36
+ end
37
+
38
+ def find_aggregate_values_per_definition!(definition_key, arguments)
39
+ unless definition = @klass.eager_group_definitions[definition_key]
40
+ return
41
+ end
42
+
43
+ reflection = @klass.reflect_on_association(definition.association)
44
+ return if reflection.blank?
45
+
46
+ aggregation_finder_class = if reflection.is_a?(ActiveRecord::Reflection::HasAndBelongsToManyReflection)
47
+ ManyToMany
48
+ elsif reflection.through_reflection
49
+ if reflection.through_reflection.is_a?(ActiveRecord::Reflection::BelongsToReflection)
50
+ HasManyThroughBelongsTo
51
+ else
52
+ HasManyThroughMany
53
53
  end
54
+ else
55
+ HasMany
56
+ end
57
+
58
+ aggregation_finder = aggregation_finder_class.new(@klass, definition, arguments, @records)
59
+ aggregate_hash = aggregation_finder.aggregate_hash
60
+
61
+ if definition.need_load_object
62
+ aggregate_objects = reflection.klass.find(aggregate_hash.values).each_with_object({}) { |o, h| h[o.id] = o }
63
+ aggregate_hash.keys.each { |key| aggregate_hash[key] = aggregate_objects[aggregate_hash[key]] }
64
+ end
65
+
66
+ @records.each do |record|
67
+ id = record.send(aggregation_finder.group_by_key)
68
+ record.send("#{definition_key}=", aggregate_hash[id] || definition.default_value)
54
69
  end
55
70
  end
56
71
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EagerGroup
4
- VERSION = '0.8.1'
4
+ VERSION = '0.9.0'
5
5
  end
data/lib/eager_group.rb CHANGED
@@ -40,7 +40,7 @@ module EagerGroup
40
40
  private
41
41
 
42
42
  def preload_eager_group(*eager_group_value)
43
- EagerGroup::Preloader.new(self.class, [self], [eager_group_value]).run
43
+ EagerGroup::Preloader.new(self.class, [self], eager_group_value).run
44
44
  end
45
45
  end
46
46
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eager_group
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.1
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Huang
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-10-25 00:00:00.000000000 Z
11
+ date: 2022-12-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -122,6 +122,20 @@ dependencies:
122
122
  - - ">="
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: pry
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'
125
139
  description: Fix n+1 aggregate sql functions for rails
126
140
  email:
127
141
  - flyerhzm@gmail.com
@@ -129,11 +143,13 @@ executables: []
129
143
  extensions: []
130
144
  extra_rdoc_files: []
131
145
  files:
146
+ - ".github/workflows/main.yml"
132
147
  - ".gitignore"
133
148
  - ".rspec"
134
149
  - ".travis.yml"
135
150
  - CHANGELOG.md
136
151
  - Gemfile
152
+ - MIT-LICENSE
137
153
  - README.md
138
154
  - Rakefile
139
155
  - benchmark.rb
@@ -144,12 +160,17 @@ files:
144
160
  - lib/eager_group.rb
145
161
  - lib/eager_group/definition.rb
146
162
  - lib/eager_group/preloader.rb
163
+ - lib/eager_group/preloader/aggregation_finder.rb
164
+ - lib/eager_group/preloader/has_many.rb
165
+ - lib/eager_group/preloader/has_many_through_belongs_to.rb
166
+ - lib/eager_group/preloader/has_many_through_many.rb
167
+ - lib/eager_group/preloader/many_to_many.rb
147
168
  - lib/eager_group/version.rb
148
169
  homepage: https://github.com/flyerhzm/eager_group
149
170
  licenses:
150
171
  - MIT
151
172
  metadata: {}
152
- post_install_message:
173
+ post_install_message:
153
174
  rdoc_options: []
154
175
  require_paths:
155
176
  - lib
@@ -164,8 +185,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
164
185
  - !ruby/object:Gem::Version
165
186
  version: '0'
166
187
  requirements: []
167
- rubygems_version: 3.0.3
168
- signing_key:
188
+ rubygems_version: 3.3.26
189
+ signing_key:
169
190
  specification_version: 4
170
191
  summary: Fix n+1 aggregate sql functions
171
192
  test_files: []