eager_group 0.8.2 → 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +18 -0
- data/.gitignore +2 -0
- data/CHANGELOG.md +8 -0
- data/eager_group.gemspec +1 -0
- data/lib/active_record/with_eager_group.rb +13 -2
- data/lib/eager_group/preloader/aggregation_finder.rb +39 -0
- data/lib/eager_group/preloader/has_many.rb +20 -0
- data/lib/eager_group/preloader/has_many_through_belongs_to.rb +28 -0
- data/lib/eager_group/preloader/has_many_through_many.rb +20 -0
- data/lib/eager_group/preloader/many_to_many.rb +20 -0
- data/lib/eager_group/preloader.rb +40 -29
- data/lib/eager_group/version.rb +1 -1
- metadata +23 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 119ff48ee90c0324a8e7f55e521a3606485ad1728ec27eaefea3df1f7c153173
|
4
|
+
data.tar.gz: 7415661b2a4ced20c7673c8d90bfb977fbef22c203062f0bb60a8be5fdd1970a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f3b85835d577315809f1a699c7a717d17b1f61c14ae0e0371312c12615a5088c45a266028fc3f091801639298e9dd6a1ca1e8d6beb37889a6f5e63aacbdc8fe6
|
7
|
+
data.tar.gz: 2475f41755b2b0161bcc4fe01fd04a5cb1b9f69728edd0e165e8596773306e421426efc191aa34eba812912c1e6b8cc608386a3fc29ca490a068c15b2335e337
|
@@ -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
data/CHANGELOG.md
CHANGED
data/eager_group.gemspec
CHANGED
@@ -9,12 +9,16 @@ module ActiveRecord
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def eager_group(*args)
|
12
|
-
check_if_method_has_arguments
|
12
|
+
# we does not use the `check_if_method_has_arguments!` here because it would flatten all the arguments,
|
13
|
+
# which would cause `[:eager_group_definition, scope_arg1, scope_arg2]` not able to preload together with other `eager_group_definitions`.
|
14
|
+
# e.g. `Post.eager_group(:approved_comments_count, [:comments_average_rating_by_author, students[0], true])`
|
15
|
+
check_argument_not_blank!(args)
|
16
|
+
|
13
17
|
spawn.eager_group!(*args)
|
14
18
|
end
|
15
19
|
|
16
20
|
def eager_group!(*args)
|
17
|
-
self.eager_group_values
|
21
|
+
self.eager_group_values |= args
|
18
22
|
self
|
19
23
|
end
|
20
24
|
|
@@ -27,5 +31,12 @@ module ActiveRecord
|
|
27
31
|
|
28
32
|
@values[:eager_group] = values
|
29
33
|
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def check_argument_not_blank!(args)
|
38
|
+
raise ArgumentError, "The method .eager_group() must contain arguments." if args.blank?
|
39
|
+
args.compact_blank!
|
40
|
+
end
|
30
41
|
end
|
31
42
|
end
|
@@ -0,0 +1,39 @@
|
|
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
|
+
def aggregate_hash
|
29
|
+
raise NotImplementedError, 'Method "aggregate_hash" must be implemented in subclass'
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def polymophic_as_condition
|
35
|
+
reflection.type ? { reflection.name => { reflection.type => @klass.base_class.name } } : []
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
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,6 +2,12 @@
|
|
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
|
@@ -10,7 +16,6 @@ module EagerGroup
|
|
10
16
|
|
11
17
|
# Preload aggregate functions
|
12
18
|
def run
|
13
|
-
primary_key = @klass.primary_key
|
14
19
|
@eager_group_values.each do |eager_group_value|
|
15
20
|
definition_key, arguments =
|
16
21
|
eager_group_value.is_a?(Array) ? [eager_group_value.shift, eager_group_value] : [eager_group_value, nil]
|
@@ -21,38 +26,44 @@ module EagerGroup
|
|
21
26
|
next if @records.empty?
|
22
27
|
|
23
28
|
@klass = @records.first.class
|
24
|
-
|
25
|
-
record_ids = @records.map { |record| record.send(primary_key) }
|
26
|
-
unless definition = @klass.eager_group_definitions[definition_key]
|
27
|
-
next
|
29
|
+
self.class.new(@klass, @records, Array.wrap(definition_key)).run
|
28
30
|
end
|
29
31
|
|
30
|
-
|
31
|
-
|
32
|
-
|
32
|
+
find_aggregate_values_per_definition!(definition_key, arguments)
|
33
|
+
end
|
34
|
+
end
|
33
35
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
aggregate_objects = reflection.klass.find(aggregate_hash.values).each_with_object({}) { |o, h| h[o.id] = o }
|
50
|
-
aggregate_hash.keys.each { |key| aggregate_hash[key] = aggregate_objects[aggregate_hash[key]] }
|
51
|
-
end
|
52
|
-
@records.each do |record|
|
53
|
-
id = record.send(primary_key)
|
54
|
-
record.send("#{definition_key}=", aggregate_hash[id] || definition.default_value)
|
36
|
+
def find_aggregate_values_per_definition!(definition_key, arguments)
|
37
|
+
unless definition = @klass.eager_group_definitions[definition_key]
|
38
|
+
return
|
39
|
+
end
|
40
|
+
|
41
|
+
reflection = @klass.reflect_on_association(definition.association)
|
42
|
+
return if reflection.blank?
|
43
|
+
|
44
|
+
aggregation_finder_class = if reflection.is_a?(ActiveRecord::Reflection::HasAndBelongsToManyReflection)
|
45
|
+
ManyToMany
|
46
|
+
elsif reflection.through_reflection
|
47
|
+
if reflection.through_reflection.is_a?(ActiveRecord::Reflection::BelongsToReflection)
|
48
|
+
HasManyThroughBelongsTo
|
49
|
+
else
|
50
|
+
HasManyThroughMany
|
55
51
|
end
|
52
|
+
else
|
53
|
+
HasMany
|
54
|
+
end
|
55
|
+
|
56
|
+
aggregation_finder = aggregation_finder_class.new(@klass, definition, arguments, @records)
|
57
|
+
aggregate_hash = aggregation_finder.aggregate_hash
|
58
|
+
|
59
|
+
if definition.need_load_object
|
60
|
+
aggregate_objects = reflection.klass.find(aggregate_hash.values).each_with_object({}) { |o, h| h[o.id] = o }
|
61
|
+
aggregate_hash.keys.each { |key| aggregate_hash[key] = aggregate_objects[aggregate_hash[key]] }
|
62
|
+
end
|
63
|
+
|
64
|
+
@records.each do |record|
|
65
|
+
id = record.send(aggregation_finder.group_by_key)
|
66
|
+
record.send("#{definition_key}=", aggregate_hash[id] || definition.default_value)
|
56
67
|
end
|
57
68
|
end
|
58
69
|
|
data/lib/eager_group/version.rb
CHANGED
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.
|
4
|
+
version: 0.9.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Richard Huang
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-12-15 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,6 +143,7 @@ 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"
|
@@ -145,6 +160,11 @@ files:
|
|
145
160
|
- lib/eager_group.rb
|
146
161
|
- lib/eager_group/definition.rb
|
147
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
|
148
168
|
- lib/eager_group/version.rb
|
149
169
|
homepage: https://github.com/flyerhzm/eager_group
|
150
170
|
licenses:
|
@@ -165,7 +185,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
165
185
|
- !ruby/object:Gem::Version
|
166
186
|
version: '0'
|
167
187
|
requirements: []
|
168
|
-
rubygems_version: 3.3.
|
188
|
+
rubygems_version: 3.3.26
|
169
189
|
signing_key:
|
170
190
|
specification_version: 4
|
171
191
|
summary: Fix n+1 aggregate sql functions
|