eager_group 0.5.0 → 0.7.2
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 +5 -5
- data/CHANGELOG.md +21 -0
- data/README.md +101 -39
- data/Rakefile +1 -1
- data/benchmark.rb +16 -13
- data/bin/console +4 -3
- data/eager_group.gemspec +20 -18
- data/lib/active_record/with_eager_group.rb +31 -0
- data/lib/eager_group.rb +28 -20
- data/lib/eager_group/active_record_base.rb +4 -2
- data/lib/eager_group/active_record_relation.rb +5 -3
- data/lib/eager_group/definition.rb +18 -1
- data/lib/eager_group/preloader.rb +44 -30
- data/lib/eager_group/version.rb +3 -1
- metadata +42 -28
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 27ca5cd0d0757f48f272d4349a6dcad1f54992946d960f364bec0086a7646ccf
|
4
|
+
data.tar.gz: 27d7def8639caca795400ba7912789ba7098315578ec954dfb2610fe5a6262af
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 464cafc22781b01be11027b832219b1dc551f0b3c300f3b377296690c0b9e7d4e98dde5f87c7e9ad8f4f88792ed4b908e00347ab95042a66412a3ef1d31731b6
|
7
|
+
data.tar.gz: 81aaa99f6d856239dba7e1a06fe4d8e2fb2781e5cb98557d142eac440a658cb4ea2a90c354ea82b33b91dcd2f2fa9452a64cf6f2e0c749282937d682fee62618
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,26 @@
|
|
1
1
|
# Next Release
|
2
2
|
|
3
|
+
## 0.7.2 (10/10/2019)
|
4
|
+
|
5
|
+
* Simplify `association_klass` for `first_object` and `last_object`
|
6
|
+
|
7
|
+
## 0.7.1 (08/23/2019)
|
8
|
+
|
9
|
+
* Set `eager_group_definitions` by `mattr_accessor`
|
10
|
+
|
11
|
+
## 0.7.0 (08/22/2019)
|
12
|
+
|
13
|
+
* Add `first_object` and `last_object` aggregation
|
14
|
+
|
15
|
+
## 0.6.1 (03/05/2018)
|
16
|
+
|
17
|
+
* Skip preload when association is empty
|
18
|
+
|
19
|
+
## 0.6.0 (12/15/2018)
|
20
|
+
|
21
|
+
* Support hash as `eager_group` argument
|
22
|
+
* Support rails 5.x
|
23
|
+
|
3
24
|
## 0.5.0 (09/22/2016)
|
4
25
|
|
5
26
|
* Add magic method for one record
|
data/README.md
CHANGED
@@ -1,34 +1,44 @@
|
|
1
1
|
# EagerGroup
|
2
2
|
|
3
|
-
[](http://travis-ci.org/flyerhzm/eager_group)
|
4
|
+
[](https://awesomecode.io/repos/flyerhzm/eager_group)
|
4
6
|
|
5
|
-
[More explaination on our blog](http://blog.
|
7
|
+
[More explaination on our blog](http://blog.flyerhzm.com/2015/06/29/eager_group/)
|
6
8
|
|
7
9
|
Fix n+1 aggregate sql functions for rails, like
|
8
10
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
11
|
+
```
|
12
|
+
SELECT "posts".* FROM "posts";
|
13
|
+
SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = 1 AND "comments"."status" = 'approved'
|
14
|
+
SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = 2 AND "comments"."status" = 'approved'
|
15
|
+
SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = 3 AND "comments"."status" = 'approved'
|
16
|
+
```
|
13
17
|
|
14
18
|
=>
|
15
19
|
|
16
|
-
|
17
|
-
|
20
|
+
```
|
21
|
+
SELECT "posts".* FROM "posts";
|
22
|
+
SELECT COUNT(*) AS count_all, post_id AS post_id FROM "comments" WHERE "comments"."post_id" IN (1, 2, 3) AND "comments"."status" = 'approved' GROUP BY post_id;
|
23
|
+
```
|
18
24
|
|
19
25
|
or
|
20
26
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
27
|
+
```
|
28
|
+
SELECT "posts".* FROM "posts";
|
29
|
+
SELECT AVG("comments"."rating") AS avg_id FROM "comments" WHERE "comments"."post_id" = 1;
|
30
|
+
SELECT AVG("comments"."rating") AS avg_id FROM "comments" WHERE "comments"."post_id" = 2;
|
31
|
+
SELECT AVG("comments"."rating") AS avg_id FROM "comments" WHERE "comments"."post_id" = 3;
|
32
|
+
```
|
25
33
|
|
26
34
|
=>
|
27
35
|
|
28
|
-
|
29
|
-
|
36
|
+
```
|
37
|
+
SELECT "posts".* FROM "posts";
|
38
|
+
SELECT AVG("comments"."rating") AS average_comments_rating, post_id AS post_id FROM "comments" WHERE "comments"."post_id" IN (1, 2, 3) GROUP BY post_id;
|
39
|
+
```
|
30
40
|
|
31
|
-
It
|
41
|
+
It supports Rails 4.x, Rails 5.x and Rails 6.x
|
32
42
|
|
33
43
|
## Installation
|
34
44
|
|
@@ -40,29 +50,35 @@ gem 'eager_group'
|
|
40
50
|
|
41
51
|
And then execute:
|
42
52
|
|
43
|
-
|
53
|
+
```
|
54
|
+
$ bundle
|
55
|
+
```
|
44
56
|
|
45
57
|
Or install it yourself as:
|
46
58
|
|
47
|
-
|
59
|
+
```
|
60
|
+
$ gem install eager_group
|
61
|
+
```
|
48
62
|
|
49
63
|
## Usage
|
50
64
|
|
51
65
|
First you need to define what aggregate function you want to eager
|
52
66
|
load.
|
53
67
|
|
54
|
-
|
55
|
-
|
68
|
+
```ruby
|
69
|
+
class Post < ActiveRecord::Base
|
70
|
+
has_many :comments
|
56
71
|
|
57
|
-
|
58
|
-
|
59
|
-
|
72
|
+
define_eager_group :comments_average_rating, :comments, :average, :rating
|
73
|
+
define_eager_group :approved_comments_count, :comments, :count, :*, -> { approved }
|
74
|
+
end
|
60
75
|
|
61
|
-
|
62
|
-
|
76
|
+
class Comment < ActiveRecord::Base
|
77
|
+
belongs_to :post
|
63
78
|
|
64
|
-
|
65
|
-
|
79
|
+
scope :approved, -> { where(status: 'approved') }
|
80
|
+
end
|
81
|
+
```
|
66
82
|
|
67
83
|
The parameters for `define_eager_group` are as follows
|
68
84
|
|
@@ -71,29 +87,75 @@ method, it also generates a method with the same name to fetch the
|
|
71
87
|
result.
|
72
88
|
* `association`, association name you want to aggregate.
|
73
89
|
* `aggregate_function`, aggregate sql function, can be one of `average`,
|
74
|
-
`count`, `maximum`, `minimum`, `sum
|
90
|
+
`count`, `maximum`, `minimum`, `sum`, I define 2 additional aggregate
|
91
|
+
function `first_object` and `last_object` to eager load first and last
|
92
|
+
association objects.
|
75
93
|
* `column_name`, aggregate column name, it can be `:*` for `count`
|
76
94
|
* `scope`, scope is optional, it's used to filter data for aggregation.
|
77
95
|
|
78
96
|
Then you can use `eager_group` to fix n+1 aggregate sql functions
|
79
97
|
when querying
|
80
98
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
99
|
+
```ruby
|
100
|
+
posts = Post.all.eager_group(:comments_average_rating, :approved_comments_count)
|
101
|
+
posts.each do |post|
|
102
|
+
post.comments_average_rating
|
103
|
+
post.approved_comments_count
|
104
|
+
end
|
105
|
+
```
|
86
106
|
|
87
107
|
EagerGroup will execute `GROUP BY` sqls for you then set the value of
|
88
108
|
attributes.
|
89
109
|
|
90
|
-
`define_eager_group` will define a method in model.
|
91
|
-
You can call the `definition_name` directly for convenience,
|
110
|
+
`define_eager_group` will define a method in model.
|
111
|
+
You can call the `definition_name` directly for convenience,
|
92
112
|
but it would not help you to fix n+1 aggregate sql issue.
|
93
113
|
|
94
|
-
|
95
|
-
|
96
|
-
|
114
|
+
```
|
115
|
+
post = Post.first
|
116
|
+
post.commets_average_rating
|
117
|
+
post.approved_comments_count
|
118
|
+
```
|
119
|
+
|
120
|
+
## Advanced
|
121
|
+
|
122
|
+
`eager_group` through association
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
User.limit(10).includes(:posts).eager_group(posts: [:comments_average_rating, :approved_comments_count])
|
126
|
+
```
|
127
|
+
|
128
|
+
pass parameter to scope
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
class Post < ActiveRecord::Base
|
132
|
+
has_many :comments
|
133
|
+
|
134
|
+
define_eager_group :comments_average_rating_by_author, :comments, :average, :rating, ->(author, ignore) { by_author(author, ignore) }
|
135
|
+
end
|
136
|
+
|
137
|
+
posts = Post.all.eager_group([:comments_average_rating_by_author, author, true])
|
138
|
+
posts.each { |post| post.comments_average_rating_by_author }
|
139
|
+
```
|
140
|
+
|
141
|
+
`first_object` and `last_object` aggregation to eager load first and
|
142
|
+
last association objects.
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
class Post < ActiveRecord::Base
|
146
|
+
has_many :comments
|
147
|
+
|
148
|
+
define_eager_group :first_comment, :comments, :first_object, :id
|
149
|
+
define_eager_group :last_comment, :comments, :last_object, :id
|
150
|
+
end
|
151
|
+
|
152
|
+
posts = Post.all.eager_group(:first_comment, :last_comment)
|
153
|
+
posts.each do |post|
|
154
|
+
post.first_comment
|
155
|
+
post.last_comment
|
156
|
+
end
|
157
|
+
```
|
158
|
+
|
97
159
|
|
98
160
|
## Benchmark
|
99
161
|
|
@@ -103,6 +165,6 @@ times faster, WOW!
|
|
103
165
|
|
104
166
|
## Contributing
|
105
167
|
|
106
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
168
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/flyerhzm/eager_group.
|
107
169
|
|
108
|
-
[1]: https://github.com/
|
170
|
+
[1]: https://github.com/flyerhzm/eager_group/blob/master/benchmark.rb
|
data/Rakefile
CHANGED
@@ -1 +1 @@
|
|
1
|
-
require
|
1
|
+
require 'bundler/gem_tasks'
|
data/benchmark.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# Calculating -------------------------------------
|
2
4
|
# Without EagerGroup 2.000 i/100ms
|
3
5
|
# With EagerGroup 28.000 i/100ms
|
@@ -28,22 +30,22 @@ class Comment < ActiveRecord::Base
|
|
28
30
|
end
|
29
31
|
|
30
32
|
# create database eager_group_benchmark;
|
31
|
-
ActiveRecord::Base.establish_connection(
|
33
|
+
ActiveRecord::Base.establish_connection(
|
34
|
+
adapter: 'mysql2', database: 'eager_group_benchmark', server: '/tmp/mysql.socket', username: 'root'
|
35
|
+
)
|
32
36
|
|
33
|
-
ActiveRecord::Base.connection.tables.each
|
34
|
-
ActiveRecord::Base.connection.drop_table(table)
|
35
|
-
end
|
37
|
+
ActiveRecord::Base.connection.tables.each { |table| ActiveRecord::Base.connection.drop_table(table) }
|
36
38
|
|
37
39
|
ActiveRecord::Schema.define do
|
38
40
|
self.verbose = false
|
39
41
|
|
40
|
-
create_table :posts, :
|
42
|
+
create_table :posts, force: true do |t|
|
41
43
|
t.string :title
|
42
44
|
t.string :body
|
43
45
|
t.timestamps null: false
|
44
46
|
end
|
45
47
|
|
46
|
-
create_table :comments, :
|
48
|
+
create_table :comments, force: true do |t|
|
47
49
|
t.string :body
|
48
50
|
t.string :status
|
49
51
|
t.integer :rating
|
@@ -53,30 +55,31 @@ ActiveRecord::Schema.define do
|
|
53
55
|
end
|
54
56
|
|
55
57
|
posts_size = 100
|
56
|
-
comments_size =
|
58
|
+
comments_size = 1_000
|
57
59
|
|
58
60
|
posts = []
|
59
|
-
posts_size.times
|
60
|
-
posts << Post.new(:title => "Title #{i}", :body => "Body #{i}")
|
61
|
-
end
|
61
|
+
posts_size.times { |i| posts << Post.new(title: "Title #{i}", body: "Body #{i}") }
|
62
62
|
Post.import posts
|
63
63
|
post_ids = Post.all.pluck(:id)
|
64
64
|
|
65
65
|
comments = []
|
66
66
|
comments_size.times do |i|
|
67
|
-
comments <<
|
67
|
+
comments <<
|
68
|
+
Comment.new(
|
69
|
+
body: "Comment #{i}", post_id: post_ids[i % 100], status: %w[approved deleted][i % 2], rating: i % 5 + 1
|
70
|
+
)
|
68
71
|
end
|
69
72
|
Comment.import comments
|
70
73
|
|
71
74
|
Benchmark.ips do |x|
|
72
|
-
x.report(
|
75
|
+
x.report('Without EagerGroup') do
|
73
76
|
Post.limit(20).each do |post|
|
74
77
|
post.comments.approved.count
|
75
78
|
post.comments.approved.average('rating')
|
76
79
|
end
|
77
80
|
end
|
78
81
|
|
79
|
-
x.report(
|
82
|
+
x.report('With EagerGroup') do
|
80
83
|
Post.eager_group(:approved_comments_count, :comments_average_rating).limit(20).each do |post|
|
81
84
|
post.approved_comments_count
|
82
85
|
post.comments_average_rating
|
data/bin/console
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
|
-
require
|
4
|
-
require
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'eager_group'
|
5
6
|
|
6
7
|
# You can add fixtures and/or initialization code here to make experimenting
|
7
8
|
# with your gem easier. You can also use a different console, if you like.
|
@@ -10,5 +11,5 @@ require "eager_group"
|
|
10
11
|
# require "pry"
|
11
12
|
# Pry.start
|
12
13
|
|
13
|
-
require
|
14
|
+
require 'irb'
|
14
15
|
IRB.start
|
data/eager_group.gemspec
CHANGED
@@ -1,30 +1,32 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
5
|
require 'eager_group/version'
|
5
6
|
|
6
7
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
8
|
+
spec.name = 'eager_group'
|
8
9
|
spec.version = EagerGroup::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
10
|
+
spec.authors = ['Richard Huang']
|
11
|
+
spec.email = ['flyerhzm@gmail.com']
|
11
12
|
|
12
|
-
spec.summary =
|
13
|
-
spec.description =
|
14
|
-
spec.homepage =
|
13
|
+
spec.summary = 'Fix n+1 aggregate sql functions'
|
14
|
+
spec.description = 'Fix n+1 aggregate sql functions for rails'
|
15
|
+
spec.homepage = 'https://github.com/flyerhzm/eager_group'
|
15
16
|
|
16
|
-
spec.license
|
17
|
+
spec.license = 'MIT'
|
17
18
|
|
18
19
|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
19
|
-
spec.bindir =
|
20
|
+
spec.bindir = 'exe'
|
20
21
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
|
-
spec.require_paths = [
|
22
|
+
spec.require_paths = ['lib']
|
22
23
|
|
23
|
-
spec.add_development_dependency
|
24
|
-
spec.add_development_dependency
|
25
|
-
spec.add_development_dependency
|
26
|
-
spec.add_development_dependency
|
27
|
-
spec.add_development_dependency
|
28
|
-
spec.add_development_dependency
|
29
|
-
spec.add_development_dependency
|
24
|
+
spec.add_development_dependency 'activerecord'
|
25
|
+
spec.add_development_dependency 'activerecord-import'
|
26
|
+
spec.add_development_dependency 'activesupport'
|
27
|
+
spec.add_development_dependency 'benchmark-ips'
|
28
|
+
spec.add_development_dependency 'bundler'
|
29
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
30
|
+
spec.add_development_dependency 'rspec', '~> 3.3'
|
31
|
+
spec.add_development_dependency 'sqlite3'
|
30
32
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module WithEagerGroup
|
5
|
+
def exec_queries
|
6
|
+
records = super
|
7
|
+
EagerGroup::Preloader.new(klass, records, eager_group_values).run if eager_group_values.present?
|
8
|
+
records
|
9
|
+
end
|
10
|
+
|
11
|
+
def eager_group(*args)
|
12
|
+
check_if_method_has_arguments!('eager_group', args)
|
13
|
+
spawn.eager_group!(*args)
|
14
|
+
end
|
15
|
+
|
16
|
+
def eager_group!(*args)
|
17
|
+
self.eager_group_values += args
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def eager_group_values
|
22
|
+
@values[:eager_group] || []
|
23
|
+
end
|
24
|
+
|
25
|
+
def eager_group_values=(values)
|
26
|
+
raise ImmutableRelation if @loaded
|
27
|
+
|
28
|
+
@values[:eager_group] = values
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/eager_group.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require '
|
4
|
-
require 'eager_group/
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/module/attribute_accessors'
|
4
|
+
require 'eager_group/version'
|
5
5
|
|
6
6
|
module EagerGroup
|
7
7
|
autoload :Preloader, 'eager_group/preloader'
|
@@ -12,36 +12,44 @@ module EagerGroup
|
|
12
12
|
end
|
13
13
|
|
14
14
|
module ClassMethods
|
15
|
-
|
15
|
+
mattr_accessor :eager_group_definitions, default: {}
|
16
16
|
|
17
17
|
# class Post
|
18
18
|
# define_eager_group :comments_avergage_rating, :comments, :average, :rating
|
19
19
|
# define_eager_group :approved_comments_count, :comments, :count, :*, -> { approved }
|
20
20
|
# end
|
21
21
|
def define_eager_group(attr, association, aggregate_function, column_name, scope = nil)
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
22
|
+
send :attr_accessor, attr
|
23
|
+
eager_group_definitions[attr] = Definition.new(association, aggregate_function, column_name, scope)
|
24
|
+
|
25
|
+
define_method attr,
|
26
|
+
lambda { |*args|
|
27
|
+
query_result_cache = instance_variable_get("@#{attr}")
|
28
|
+
return query_result_cache if args.blank? && query_result_cache.present?
|
29
|
+
|
30
|
+
preload_eager_group(attr, *args)
|
31
|
+
instance_variable_get("@#{attr}")
|
32
|
+
}
|
33
|
+
|
35
34
|
define_method "#{attr}=" do |val|
|
36
35
|
instance_variable_set("@#{attr}", val)
|
37
36
|
end
|
38
37
|
end
|
39
38
|
end
|
40
|
-
|
39
|
+
|
41
40
|
private
|
41
|
+
|
42
42
|
def preload_eager_group(*eager_group_value)
|
43
43
|
EagerGroup::Preloader.new(self.class, [self], [eager_group_value]).run
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
47
|
-
|
47
|
+
require 'active_record'
|
48
|
+
ActiveRecord::Base.class_eval do
|
49
|
+
include EagerGroup
|
50
|
+
class << self
|
51
|
+
delegate :eager_group, to: :all
|
52
|
+
end
|
53
|
+
end
|
54
|
+
require 'active_record/with_eager_group'
|
55
|
+
ActiveRecord::Relation.send :prepend, ActiveRecord::WithEagerGroup
|
@@ -1,13 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class ActiveRecord::Relation
|
2
4
|
# Post.all.eager_group(:approved_comments_count, :comments_average_rating)
|
3
5
|
|
4
6
|
def exec_queries_with_eager_group
|
5
7
|
records = exec_queries_without_eager_group
|
6
|
-
if eager_group_values.present?
|
7
|
-
EagerGroup::Preloader.new(self.klass, records, eager_group_values).run
|
8
|
-
end
|
8
|
+
EagerGroup::Preloader.new(klass, records, eager_group_values).run if eager_group_values.present?
|
9
9
|
records
|
10
10
|
end
|
11
|
+
|
11
12
|
alias_method_chain :exec_queries, :eager_group
|
12
13
|
|
13
14
|
def eager_group(*args)
|
@@ -26,6 +27,7 @@ class ActiveRecord::Relation
|
|
26
27
|
|
27
28
|
def eager_group_values=(values)
|
28
29
|
raise ImmutableRelation if @loaded
|
30
|
+
|
29
31
|
@values[:eager_group] = values
|
30
32
|
end
|
31
33
|
end
|
@@ -1,6 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module EagerGroup
|
2
4
|
class Definition
|
3
|
-
attr_reader :association, :
|
5
|
+
attr_reader :association, :column_name, :scope
|
4
6
|
|
5
7
|
def initialize(association, aggregate_function, column_name, scope)
|
6
8
|
@association = association
|
@@ -8,5 +10,20 @@ module EagerGroup
|
|
8
10
|
@column_name = column_name
|
9
11
|
@scope = scope
|
10
12
|
end
|
13
|
+
|
14
|
+
def aggregation_function
|
15
|
+
return :maximum if @aggregate_function.to_sym == :last_object
|
16
|
+
return :minimum if @aggregate_function.to_sym == :first_object
|
17
|
+
|
18
|
+
@aggregate_function
|
19
|
+
end
|
20
|
+
|
21
|
+
def need_load_object
|
22
|
+
%i[first_object last_object].include?(@aggregate_function.to_sym)
|
23
|
+
end
|
24
|
+
|
25
|
+
def default_value
|
26
|
+
%i[first_object last_object].include?(@aggregate_function.to_sym) ? nil : 0
|
27
|
+
end
|
11
28
|
end
|
12
29
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module EagerGroup
|
2
4
|
class Preloader
|
3
5
|
def initialize(klass, records, eager_group_values)
|
@@ -9,40 +11,52 @@ module EagerGroup
|
|
9
11
|
# Preload aggregate functions
|
10
12
|
def run
|
11
13
|
primary_key = @klass.primary_key
|
12
|
-
record_ids = @records.map { |record| record.send primary_key }
|
13
14
|
@eager_group_values.each do |eager_group_value|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
}
|
15
|
+
definition_key, arguments =
|
16
|
+
eager_group_value.is_a?(Array) ? [eager_group_value.shift, eager_group_value] : [eager_group_value, nil]
|
17
|
+
if definition_key.is_a?(Hash)
|
18
|
+
association_name, definition_key = *definition_key.first
|
19
|
+
@records = @records.flat_map { |record| record.send(association_name) }
|
20
|
+
next if @records.empty?
|
21
|
+
|
22
|
+
@klass = @records.first.class
|
23
|
+
end
|
24
|
+
record_ids = @records.map { |record| record.send(primary_key) }
|
25
|
+
next unless definition = @klass.eager_group_definitions[definition_key]
|
26
26
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
27
|
+
reflection = @klass.reflect_on_association(definition.association)
|
28
|
+
association_class = reflection.klass
|
29
|
+
association_class = association_class.instance_exec(*arguments, &definition.scope) if definition.scope
|
30
|
+
|
31
|
+
if reflection.through_reflection
|
32
|
+
foreign_key = "#{reflection.through_reflection.name}.#{reflection.through_reflection.foreign_key}"
|
33
|
+
aggregate_hash =
|
34
|
+
association_class.joins(reflection.through_reflection.name).where(foreign_key => record_ids).where(
|
35
|
+
polymophic_as_condition(reflection.through_reflection)
|
36
|
+
)
|
37
|
+
.group(foreign_key)
|
38
|
+
.send(definition.aggregation_function, definition.column_name)
|
39
|
+
else
|
40
|
+
aggregate_hash =
|
41
|
+
association_class.where(reflection.foreign_key => record_ids).where(polymophic_as_condition(reflection))
|
42
|
+
.group(reflection.foreign_key)
|
43
|
+
.send(definition.aggregation_function, definition.column_name)
|
44
|
+
end
|
45
|
+
if definition.need_load_object
|
46
|
+
aggregate_objects = reflection.klass.find(aggregate_hash.values).each_with_object({}) { |o, h| h[o.id] = o }
|
47
|
+
aggregate_hash.keys.each { |key| aggregate_hash[key] = aggregate_objects[aggregate_hash[key]] }
|
48
|
+
end
|
49
|
+
@records.each do |record|
|
50
|
+
id = record.send(primary_key)
|
51
|
+
record.send("#{definition_key}=", aggregate_hash[id] || definition.default_value)
|
44
52
|
end
|
45
53
|
end
|
46
54
|
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def polymophic_as_condition(reflection)
|
59
|
+
reflection.type ? { reflection.name => { reflection.type => @klass.base_class.name } } : []
|
60
|
+
end
|
47
61
|
end
|
48
62
|
end
|
data/lib/eager_group/version.rb
CHANGED
metadata
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: eager_group
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Richard Huang
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2019-10-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: activerecord
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
@@ -25,49 +25,49 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: activerecord-import
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: activesupport
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- - "
|
45
|
+
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
47
|
+
version: '0'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- - "
|
52
|
+
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
54
|
+
version: '0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
56
|
+
name: benchmark-ips
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- - "
|
59
|
+
- - ">="
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '
|
61
|
+
version: '0'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- - "
|
66
|
+
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: '
|
68
|
+
version: '0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
70
|
+
name: bundler
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - ">="
|
@@ -81,21 +81,35 @@ dependencies:
|
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
84
|
+
name: rake
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
|
-
- - "
|
87
|
+
- - "~>"
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version: '0'
|
89
|
+
version: '10.0'
|
90
90
|
type: :development
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
|
-
- - "
|
94
|
+
- - "~>"
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version: '0'
|
96
|
+
version: '10.0'
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
|
-
name:
|
98
|
+
name: rspec
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '3.3'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '3.3'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: sqlite3
|
99
113
|
requirement: !ruby/object:Gem::Requirement
|
100
114
|
requirements:
|
101
115
|
- - ">="
|
@@ -126,13 +140,14 @@ files:
|
|
126
140
|
- bin/console
|
127
141
|
- bin/setup
|
128
142
|
- eager_group.gemspec
|
143
|
+
- lib/active_record/with_eager_group.rb
|
129
144
|
- lib/eager_group.rb
|
130
145
|
- lib/eager_group/active_record_base.rb
|
131
146
|
- lib/eager_group/active_record_relation.rb
|
132
147
|
- lib/eager_group/definition.rb
|
133
148
|
- lib/eager_group/preloader.rb
|
134
149
|
- lib/eager_group/version.rb
|
135
|
-
homepage: https://github.com/
|
150
|
+
homepage: https://github.com/flyerhzm/eager_group
|
136
151
|
licenses:
|
137
152
|
- MIT
|
138
153
|
metadata: {}
|
@@ -151,8 +166,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
151
166
|
- !ruby/object:Gem::Version
|
152
167
|
version: '0'
|
153
168
|
requirements: []
|
154
|
-
|
155
|
-
rubygems_version: 2.5.1
|
169
|
+
rubygems_version: 3.0.3
|
156
170
|
signing_key:
|
157
171
|
specification_version: 4
|
158
172
|
summary: Fix n+1 aggregate sql functions
|