eager_group 0.6.1 → 0.8.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
  SHA256:
3
- metadata.gz: 36652bc032832f5a84dc02733286cd156a3253b8c1ba3b1b9289fec7686745d7
4
- data.tar.gz: da2a878d043e2a02abb5f64c6d1c679dd02fe02220ba6399f5312e1ee096f953
3
+ metadata.gz: 88912c5696afe9332d53ac15b2365d2e2d7112391672a2cba5db62012876a138
4
+ data.tar.gz: a18a2d21cd4299a68774f3dbb62556c71f3096b3ebac28bebf7a767a875fc00f
5
5
  SHA512:
6
- metadata.gz: b7b4aacb136f425da49b81bf4d34c4bb467abc940b05fb936bd364b6884a322330acdec85449008de6f64716adc461ffebcad7a4b760f58fb44187b6abd2f3ba
7
- data.tar.gz: 550d2eb69a366f90227c15382e25044078731610caae901982478baef95935dd581fc3b2a3affa04c48cab92a340d84744c18aeb5cb0a865a083ea78cee7bd69
6
+ metadata.gz: af269ea9b3be9f9a613f56cf14e52a7b183b6d041583f02aa88945f65150f98147ed7271523b9f8ec0d36c622f42ae257f7137e9cf842b381f11cbd7cd6e50bf
7
+ data.tar.gz: 2878aaefb0b9420b13531927cd46ad47894954c2999fe81d9bf853d7f2255ae97b42df8e56c43a840b5812d71ed1f535c391ccf605fbed99f08ed0769959eab8
@@ -1,5 +1,25 @@
1
1
  # Next Release
2
2
 
3
+ ## 0.8.1 (10/25/2019)
4
+
5
+ * Fix for `has_many :through`
6
+
7
+ ## 0.8.0 (10/21/2019)
8
+
9
+ * Support `has_and_belongs_to_many`
10
+
11
+ ## 0.7.2 (10/10/2019)
12
+
13
+ * Simplify `association_klass` for `first_object` and `last_object`
14
+
15
+ ## 0.7.1 (08/23/2019)
16
+
17
+ * Set `eager_group_definitions` by `mattr_accessor`
18
+
19
+ ## 0.7.0 (08/22/2019)
20
+
21
+ * Add `first_object` and `last_object` aggregation
22
+
3
23
  ## 0.6.1 (03/05/2018)
4
24
 
5
25
  * Skip preload when association is empty
data/README.md CHANGED
@@ -8,29 +8,37 @@ flyerhzm/eager_group](https://awesomecode.io/projects/e5386790-9420-4003-831a-c9
8
8
 
9
9
  Fix n+1 aggregate sql functions for rails, like
10
10
 
11
- SELECT "posts".* FROM "posts";
12
- SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = 1 AND "comments"."status" = 'approved'
13
- SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = 2 AND "comments"."status" = 'approved'
14
- SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = 3 AND "comments"."status" = 'approved'
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
+ ```
15
17
 
16
18
  =>
17
19
 
18
- SELECT "posts".* FROM "posts";
19
- 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;
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
+ ```
20
24
 
21
25
  or
22
26
 
23
- SELECT "posts".* FROM "posts";
24
- SELECT AVG("comments"."rating") AS avg_id FROM "comments" WHERE "comments"."post_id" = 1;
25
- SELECT AVG("comments"."rating") AS avg_id FROM "comments" WHERE "comments"."post_id" = 2;
26
- SELECT AVG("comments"."rating") AS avg_id FROM "comments" WHERE "comments"."post_id" = 3;
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
+ ```
27
33
 
28
34
  =>
29
35
 
30
- SELECT "posts".* FROM "posts";
31
- 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;
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
+ ```
32
40
 
33
- It supports Rails 4.x and Rails 5.x
41
+ It supports Rails 4.x, Rails 5.x and Rails 6.x
34
42
 
35
43
  ## Installation
36
44
 
@@ -42,29 +50,35 @@ gem 'eager_group'
42
50
 
43
51
  And then execute:
44
52
 
45
- $ bundle
53
+ ```
54
+ $ bundle
55
+ ```
46
56
 
47
57
  Or install it yourself as:
48
58
 
49
- $ gem install eager_group
59
+ ```
60
+ $ gem install eager_group
61
+ ```
50
62
 
51
63
  ## Usage
52
64
 
53
65
  First you need to define what aggregate function you want to eager
54
66
  load.
55
67
 
56
- class Post < ActiveRecord::Base
57
- has_many :comments
68
+ ```ruby
69
+ class Post < ActiveRecord::Base
70
+ has_many :comments
58
71
 
59
- define_eager_group :comments_average_rating, :comments, :average, :rating
60
- define_eager_group :approved_comments_count, :comments, :count, :*, -> { approved }
61
- end
72
+ define_eager_group :comments_average_rating, :comments, :average, :rating
73
+ define_eager_group :approved_comments_count, :comments, :count, :*, -> { approved }
74
+ end
62
75
 
63
- class Comment < ActiveRecord::Base
64
- belongs_to :post
76
+ class Comment < ActiveRecord::Base
77
+ belongs_to :post
65
78
 
66
- scope :approved, -> { where(status: 'approved') }
67
- end
79
+ scope :approved, -> { where(status: 'approved') }
80
+ end
81
+ ```
68
82
 
69
83
  The parameters for `define_eager_group` are as follows
70
84
 
@@ -73,18 +87,22 @@ method, it also generates a method with the same name to fetch the
73
87
  result.
74
88
  * `association`, association name you want to aggregate.
75
89
  * `aggregate_function`, aggregate sql function, can be one of `average`,
76
- `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.
77
93
  * `column_name`, aggregate column name, it can be `:*` for `count`
78
94
  * `scope`, scope is optional, it's used to filter data for aggregation.
79
95
 
80
96
  Then you can use `eager_group` to fix n+1 aggregate sql functions
81
97
  when querying
82
98
 
83
- posts = Post.all.eager_group(:comments_average_rating, :approved_comments_count)
84
- posts.each do |post|
85
- post.comments_average_rating
86
- post.approved_comments_count
87
- end
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
+ ```
88
106
 
89
107
  EagerGroup will execute `GROUP BY` sqls for you then set the value of
90
108
  attributes.
@@ -93,16 +111,51 @@ attributes.
93
111
  You can call the `definition_name` directly for convenience,
94
112
  but it would not help you to fix n+1 aggregate sql issue.
95
113
 
96
- post = Post.first
97
- post.commets_average_rating
98
- post.approved_comments_count
114
+ ```
115
+ post = Post.first
116
+ post.commets_average_rating
117
+ post.approved_comments_count
118
+ ```
99
119
 
100
120
  ## Advanced
101
121
 
122
+ `eager_group` through association
123
+
102
124
  ```ruby
103
125
  User.limit(10).includes(:posts).eager_group(posts: [:comments_average_rating, :approved_comments_count])
104
126
  ```
105
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
+
106
159
 
107
160
  ## Benchmark
108
161
 
@@ -30,11 +30,11 @@ class Comment < ActiveRecord::Base
30
30
  end
31
31
 
32
32
  # create database eager_group_benchmark;
33
- ActiveRecord::Base.establish_connection(adapter: 'mysql2', database: 'eager_group_benchmark', server: '/tmp/mysql.socket', username: 'root')
33
+ ActiveRecord::Base.establish_connection(
34
+ adapter: 'mysql2', database: 'eager_group_benchmark', server: '/tmp/mysql.socket', username: 'root'
35
+ )
34
36
 
35
- ActiveRecord::Base.connection.tables.each do |table|
36
- ActiveRecord::Base.connection.drop_table(table)
37
- end
37
+ ActiveRecord::Base.connection.tables.each { |table| ActiveRecord::Base.connection.drop_table(table) }
38
38
 
39
39
  ActiveRecord::Schema.define do
40
40
  self.verbose = false
@@ -55,18 +55,19 @@ ActiveRecord::Schema.define do
55
55
  end
56
56
 
57
57
  posts_size = 100
58
- comments_size = 1000
58
+ comments_size = 1_000
59
59
 
60
60
  posts = []
61
- posts_size.times do |i|
62
- posts << Post.new(title: "Title #{i}", body: "Body #{i}")
63
- end
61
+ posts_size.times { |i| posts << Post.new(title: "Title #{i}", body: "Body #{i}") }
64
62
  Post.import posts
65
63
  post_ids = Post.all.pluck(:id)
66
64
 
67
65
  comments = []
68
66
  comments_size.times do |i|
69
- comments << Comment.new(body: "Comment #{i}", post_id: post_ids[i % 100], status: %w[approved deleted][i % 2], rating: i % 5 + 1)
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
+ )
70
71
  end
71
72
  Comment.import comments
72
73
 
@@ -23,9 +23,10 @@ Gem::Specification.new do |spec|
23
23
 
24
24
  spec.add_development_dependency 'activerecord'
25
25
  spec.add_development_dependency 'activerecord-import'
26
+ spec.add_development_dependency 'activesupport'
26
27
  spec.add_development_dependency 'benchmark-ips'
27
28
  spec.add_development_dependency 'bundler'
28
29
  spec.add_development_dependency 'rake', '~> 10.0'
29
30
  spec.add_development_dependency 'rspec', '~> 3.3'
30
- spec.add_development_dependency 'sqlite3', '~> 1.3'
31
+ spec.add_development_dependency 'sqlite3'
31
32
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_support/core_ext/module/attribute_accessors'
3
4
  require 'eager_group/version'
4
5
 
5
6
  module EagerGroup
@@ -11,7 +12,7 @@ module EagerGroup
11
12
  end
12
13
 
13
14
  module ClassMethods
14
- attr_reader :eager_group_definitions
15
+ mattr_accessor :eager_group_definitions, default: {}
15
16
 
16
17
  # class Post
17
18
  # define_eager_group :comments_avergage_rating, :comments, :average, :rating
@@ -19,16 +20,16 @@ module EagerGroup
19
20
  # end
20
21
  def define_eager_group(attr, association, aggregate_function, column_name, scope = nil)
21
22
  send :attr_accessor, attr
22
- @eager_group_definitions ||= {}
23
- @eager_group_definitions[attr] = Definition.new association, aggregate_function, column_name, scope
23
+ eager_group_definitions[attr] = Definition.new(association, aggregate_function, column_name, scope)
24
24
 
25
- define_method attr, lambda { |*args|
26
- query_result_cache = instance_variable_get("@#{attr}")
27
- return query_result_cache if args.blank? && query_result_cache.present?
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?
28
29
 
29
- preload_eager_group(attr, *args)
30
- instance_variable_get("@#{attr}")
31
- }
30
+ preload_eager_group(attr, *args)
31
+ instance_variable_get("@#{attr}")
32
+ }
32
33
 
33
34
  define_method "#{attr}=" do |val|
34
35
  instance_variable_set("@#{attr}", val)
@@ -51,4 +52,4 @@ ActiveRecord::Base.class_eval do
51
52
  end
52
53
  end
53
54
  require 'active_record/with_eager_group'
54
- ActiveRecord::Relation.send :prepend, ActiveRecord::WithEagerGroup
55
+ ActiveRecord::Relation.prepend ActiveRecord::WithEagerGroup
@@ -2,7 +2,7 @@
2
2
 
3
3
  module EagerGroup
4
4
  class Definition
5
- attr_reader :association, :aggregate_function, :column_name, :scope
5
+ attr_reader :association, :column_name, :scope
6
6
 
7
7
  def initialize(association, aggregate_function, column_name, scope)
8
8
  @association = association
@@ -10,5 +10,20 @@ module EagerGroup
10
10
  @column_name = column_name
11
11
  @scope = scope
12
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
13
28
  end
14
29
  end
@@ -12,36 +12,44 @@ module EagerGroup
12
12
  def run
13
13
  primary_key = @klass.primary_key
14
14
  @eager_group_values.each do |eager_group_value|
15
- definition_key, arguments = eager_group_value.is_a?(Array) ? [eager_group_value.shift, eager_group_value] : [eager_group_value, nil]
15
+ definition_key, arguments =
16
+ eager_group_value.is_a?(Array) ? [eager_group_value.shift, eager_group_value] : [eager_group_value, nil]
17
+
16
18
  if definition_key.is_a?(Hash)
17
19
  association_name, definition_key = *definition_key.first
18
20
  @records = @records.flat_map { |record| record.send(association_name) }
19
21
  next if @records.empty?
22
+
20
23
  @klass = @records.first.class
21
24
  end
22
25
  record_ids = @records.map { |record| record.send(primary_key) }
23
- next unless definition = @klass.eager_group_definitions[definition_key]
26
+ unless definition = @klass.eager_group_definitions[definition_key]
27
+ next
28
+ end
24
29
 
25
30
  reflection = @klass.reflect_on_association(definition.association)
26
31
  association_class = reflection.klass
27
32
  association_class = association_class.instance_exec(*arguments, &definition.scope) if definition.scope
28
33
 
29
- if reflection.through_reflection
30
- foreign_key = "#{reflection.through_reflection.name}.#{reflection.through_reflection.foreign_key}"
31
- aggregate_hash = association_class.joins(reflection.through_reflection.name)
32
- .where(foreign_key => record_ids)
33
- .where(polymophic_as_condition(reflection.through_reflection))
34
- .group(foreign_key)
35
- .send(definition.aggregate_function, definition.column_name)
36
- else
37
- aggregate_hash = association_class.where(reflection.foreign_key => record_ids)
38
- .where(polymophic_as_condition(reflection))
39
- .group(reflection.foreign_key)
40
- .send(definition.aggregate_function, definition.column_name)
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]] }
41
49
  end
42
50
  @records.each do |record|
43
51
  id = record.send(primary_key)
44
- record.send("#{definition_key}=", aggregate_hash[id] || 0)
52
+ record.send("#{definition_key}=", aggregate_hash[id] || definition.default_value)
45
53
  end
46
54
  end
47
55
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EagerGroup
4
- VERSION = '0.6.1'
4
+ VERSION = '0.8.1'
5
5
  end
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.6.1
4
+ version: 0.8.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: 2019-03-05 00:00:00.000000000 Z
11
+ date: 2019-10-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activesupport
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: benchmark-ips
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -98,16 +112,16 @@ dependencies:
98
112
  name: sqlite3
99
113
  requirement: !ruby/object:Gem::Requirement
100
114
  requirements:
101
- - - "~>"
115
+ - - ">="
102
116
  - !ruby/object:Gem::Version
103
- version: '1.3'
117
+ version: '0'
104
118
  type: :development
105
119
  prerelease: false
106
120
  version_requirements: !ruby/object:Gem::Requirement
107
121
  requirements:
108
- - - "~>"
122
+ - - ">="
109
123
  - !ruby/object:Gem::Version
110
- version: '1.3'
124
+ version: '0'
111
125
  description: Fix n+1 aggregate sql functions for rails
112
126
  email:
113
127
  - flyerhzm@gmail.com
@@ -128,8 +142,6 @@ files:
128
142
  - eager_group.gemspec
129
143
  - lib/active_record/with_eager_group.rb
130
144
  - lib/eager_group.rb
131
- - lib/eager_group/active_record_base.rb
132
- - lib/eager_group/active_record_relation.rb
133
145
  - lib/eager_group/definition.rb
134
146
  - lib/eager_group/preloader.rb
135
147
  - lib/eager_group/version.rb
@@ -152,7 +164,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
152
164
  - !ruby/object:Gem::Version
153
165
  version: '0'
154
166
  requirements: []
155
- rubygems_version: 3.0.1
167
+ rubygems_version: 3.0.3
156
168
  signing_key:
157
169
  specification_version: 4
158
170
  summary: Fix n+1 aggregate sql functions
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class ActiveRecord::Base
4
- class <<self
5
- # Post.eager_group(:approved_comments_count, :comments_average_rating)
6
- delegate :eager_group, to: :all
7
- end
8
- end
@@ -1,32 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class ActiveRecord::Relation
4
- # Post.all.eager_group(:approved_comments_count, :comments_average_rating)
5
-
6
- def exec_queries_with_eager_group
7
- records = exec_queries_without_eager_group
8
- EagerGroup::Preloader.new(klass, records, eager_group_values).run if eager_group_values.present?
9
- records
10
- end
11
- alias_method_chain :exec_queries, :eager_group
12
-
13
- def eager_group(*args)
14
- check_if_method_has_arguments!('eager_group', args)
15
- spawn.eager_group!(*args)
16
- end
17
-
18
- def eager_group!(*args)
19
- self.eager_group_values += args
20
- self
21
- end
22
-
23
- def eager_group_values
24
- @values[:eager_group] || []
25
- end
26
-
27
- def eager_group_values=(values)
28
- raise ImmutableRelation if @loaded
29
-
30
- @values[:eager_group] = values
31
- end
32
- end