eager_group 0.5.0 → 0.7.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
[![Build Status](https://secure.travis-ci.org/
|
3
|
+
[![Build Status](https://secure.travis-ci.org/flyerhzm/eager_group.png)](http://travis-ci.org/flyerhzm/eager_group)
|
4
|
+
[![AwesomeCode Status for
|
5
|
+
flyerhzm/eager_group](https://awesomecode.io/projects/e5386790-9420-4003-831a-c9a8c8a48108/status)](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
|