eager_group 0.6.1 → 0.8.1

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 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