eager_group 0.5.0 → 0.6.0

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
  SHA1:
3
- metadata.gz: 3ddfe7747a2b77e9f26cbbbb0029b41acb03b88b
4
- data.tar.gz: 5357c3d44af2542594fc5ea593029cd3e3c5cc7e
3
+ metadata.gz: 22abe02d6bafe11c49460a72d251e3686f007796
4
+ data.tar.gz: c4df75c16f76a672fc4b6dcfea0f5fe80399c064
5
5
  SHA512:
6
- metadata.gz: 80419fb36e918d83f69487633330ffd607d806abaed2ff0a10ecd28134402968105c336aa31e90d361511dc717f3d3bfa3deb7a8a2892663e2a7efc7348ad862
7
- data.tar.gz: ca82b49a3007370aebc5b3f2db9d85d51b3aeda6ee800a16dc20a3bda5a8b136c96bfbcfc7ffeb6523a110ef4083c20859ed0d31abb3cf6df4fe2b450d3d5c04
6
+ metadata.gz: 33cd73103c6cb2597a13e640b028b7c2bfc75982ed3746355b07b758cb3810f5923fdb7967a4e102591d0aa07b51453d0473be94c2d5b2036f123e9286395ddd
7
+ data.tar.gz: 61e0c3121f8504b094e11807a0f377f16c406b26973feea5a631152d0437465131720c0586870cee4e54e5f748bc01f976d3c4da78562ff741e6575a0b663002
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Next Release
2
2
 
3
+ ## 0.6.0 (12/15/2018)
4
+
5
+ * Support hash as `eager_group` argument
6
+ * Support rails 5.x
7
+
3
8
  ## 0.5.0 (09/22/2016)
4
9
 
5
10
  * Add magic method for one record
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # EagerGroup
2
2
 
3
3
  [![Build Status](https://secure.travis-ci.org/xinminlabs/eager_group.png)](http://travis-ci.org/xinminlabs/eager_group)
4
+ [![AwesomeCode Status for
5
+ xinminlabs/eager_group](https://awesomecode.io/projects/e5386790-9420-4003-831a-c9a8c8a48108/status)](https://awesomecode.io/repos/xinminlabs/eager_group)
4
6
 
5
7
  [More explaination on our blog](http://blog.xinminlabs.com/2015/06/29/eager_group/)
6
8
 
@@ -28,7 +30,7 @@ or
28
30
  SELECT "posts".* FROM "posts";
29
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;
30
32
 
31
- It only supports Rails 4.x so far.
33
+ It supports Rails 4.x and Rails 5.x
32
34
 
33
35
  ## Installation
34
36
 
@@ -87,14 +89,18 @@ when querying
87
89
  EagerGroup will execute `GROUP BY` sqls for you then set the value of
88
90
  attributes.
89
91
 
90
- `define_eager_group` will define a method in model.
91
- You can call the `definition_name` directly for convenience,
92
+ `define_eager_group` will define a method in model.
93
+ You can call the `definition_name` directly for convenience,
92
94
  but it would not help you to fix n+1 aggregate sql issue.
93
95
 
94
96
  post = Post.first
95
97
  post.commets_average_rating
96
98
  post.approved_comments_count
97
99
 
100
+ ## Advanced
101
+
102
+ User.limit(10).includes(:posts).eager_group(posts: [:comments_average_rating, :approved_comments_count])
103
+
98
104
  ## Benchmark
99
105
 
100
106
  I wrote a benchmark script [here][1], it queries approved comments count
data/Rakefile CHANGED
@@ -1 +1 @@
1
- require "bundler/gem_tasks"
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,7 +30,7 @@ class Comment < ActiveRecord::Base
28
30
  end
29
31
 
30
32
  # create database eager_group_benchmark;
31
- ActiveRecord::Base.establish_connection(:adapter => 'mysql2', :database => 'eager_group_benchmark', :server => '/tmp/mysql.socket', :username => 'root')
33
+ ActiveRecord::Base.establish_connection(adapter: 'mysql2', database: 'eager_group_benchmark', server: '/tmp/mysql.socket', username: 'root')
32
34
 
33
35
  ActiveRecord::Base.connection.tables.each do |table|
34
36
  ActiveRecord::Base.connection.drop_table(table)
@@ -37,13 +39,13 @@ end
37
39
  ActiveRecord::Schema.define do
38
40
  self.verbose = false
39
41
 
40
- create_table :posts, :force => true do |t|
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, :force => true do |t|
48
+ create_table :comments, force: true do |t|
47
49
  t.string :body
48
50
  t.string :status
49
51
  t.integer :rating
@@ -57,26 +59,26 @@ comments_size = 1000
57
59
 
58
60
  posts = []
59
61
  posts_size.times do |i|
60
- posts << Post.new(:title => "Title #{i}", :body => "Body #{i}")
62
+ posts << Post.new(title: "Title #{i}", body: "Body #{i}")
61
63
  end
62
64
  Post.import posts
63
65
  post_ids = Post.all.pluck(:id)
64
66
 
65
67
  comments = []
66
68
  comments_size.times do |i|
67
- comments << Comment.new(:body => "Comment #{i}", :post_id => post_ids[i%100], :status => ["approved", "deleted"][i%2], rating: i%5+1)
69
+ comments << Comment.new(body: "Comment #{i}", post_id: post_ids[i % 100], status: %w[approved deleted][i % 2], rating: i % 5 + 1)
68
70
  end
69
71
  Comment.import comments
70
72
 
71
73
  Benchmark.ips do |x|
72
- x.report("Without EagerGroup") do
74
+ x.report('Without EagerGroup') do
73
75
  Post.limit(20).each do |post|
74
76
  post.comments.approved.count
75
77
  post.comments.approved.average('rating')
76
78
  end
77
79
  end
78
80
 
79
- x.report("With EagerGroup") do
81
+ x.report('With EagerGroup') do
80
82
  Post.eager_group(:approved_comments_count, :comments_average_rating).limit(20).each do |post|
81
83
  post.approved_comments_count
82
84
  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 "bundler/setup"
4
- require "eager_group"
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 "irb"
14
+ require 'irb'
14
15
  IRB.start
data/eager_group.gemspec CHANGED
@@ -1,30 +1,31 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
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 = "eager_group"
8
+ spec.name = 'eager_group'
8
9
  spec.version = EagerGroup::VERSION
9
- spec.authors = ["Richard Huang"]
10
- spec.email = ["flyerhzm@gmail.com"]
10
+ spec.authors = ['Richard Huang']
11
+ spec.email = ['flyerhzm@gmail.com']
11
12
 
12
- spec.summary = %q{Fix n+1 aggregate sql functions}
13
- spec.description = %q{Fix n+1 aggregate sql functions for rails}
14
- spec.homepage = "https://github.com/xinminlabs/eager_group"
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/xinminlabs/eager_group'
15
16
 
16
- spec.license = 'MIT'
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 = "exe"
20
+ spec.bindir = 'exe'
20
21
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
- spec.require_paths = ["lib"]
22
+ spec.require_paths = ['lib']
22
23
 
23
- spec.add_development_dependency "bundler"
24
- spec.add_development_dependency "rake", "~> 10.0"
25
- spec.add_development_dependency "rspec", "~> 3.3"
26
- spec.add_development_dependency "sqlite3", "~> 1.3"
27
- spec.add_development_dependency "activerecord"
28
- spec.add_development_dependency "activerecord-import"
29
- spec.add_development_dependency "benchmark-ips"
24
+ spec.add_development_dependency 'activerecord'
25
+ spec.add_development_dependency 'activerecord-import'
26
+ spec.add_development_dependency 'benchmark-ips'
27
+ spec.add_development_dependency 'bundler'
28
+ spec.add_development_dependency 'rake', '~> 10.0'
29
+ spec.add_development_dependency 'rspec', '~> 3.3'
30
+ spec.add_development_dependency 'sqlite3', '~> 1.3'
30
31
  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,6 @@
1
- require "eager_group/version"
2
- require 'active_record'
3
- require 'eager_group/active_record_base'
4
- require 'eager_group/active_record_relation'
1
+ # frozen_string_literal: true
2
+
3
+ require 'eager_group/version'
5
4
 
6
5
  module EagerGroup
7
6
  autoload :Preloader, 'eager_group/preloader'
@@ -12,36 +11,44 @@ module EagerGroup
12
11
  end
13
12
 
14
13
  module ClassMethods
15
- attr_reader :eager_group_definations
14
+ attr_reader :eager_group_definitions
16
15
 
17
16
  # class Post
18
17
  # define_eager_group :comments_avergage_rating, :comments, :average, :rating
19
18
  # define_eager_group :approved_comments_count, :comments, :count, :*, -> { approved }
20
19
  # end
21
20
  def define_eager_group(attr, association, aggregate_function, column_name, scope = nil)
22
- self.send :attr_accessor, attr
23
- @eager_group_definations ||= {}
24
- @eager_group_definations[attr] = Definition.new association, aggregate_function, column_name, scope
25
-
26
- define_method attr, -> (*args) do
21
+ send :attr_accessor, attr
22
+ @eager_group_definitions ||= {}
23
+ @eager_group_definitions[attr] = Definition.new association, aggregate_function, column_name, scope
24
+
25
+ define_method attr, lambda { |*args|
27
26
  query_result_cache = instance_variable_get("@#{attr}")
28
- if args.blank? && query_result_cache.present?
29
- return query_result_cache
30
- end
27
+ return query_result_cache if args.blank? && query_result_cache.present?
28
+
31
29
  preload_eager_group(attr, *args)
32
30
  instance_variable_get("@#{attr}")
33
- end
34
-
31
+ }
32
+
35
33
  define_method "#{attr}=" do |val|
36
34
  instance_variable_set("@#{attr}", val)
37
35
  end
38
36
  end
39
37
  end
40
-
38
+
41
39
  private
40
+
42
41
  def preload_eager_group(*eager_group_value)
43
42
  EagerGroup::Preloader.new(self.class, [self], [eager_group_value]).run
44
43
  end
45
44
  end
46
45
 
47
- ActiveRecord::Base.send :include, EagerGroup
46
+ require 'active_record'
47
+ ActiveRecord::Base.class_eval do
48
+ include EagerGroup
49
+ class << self
50
+ delegate :eager_group, to: :all
51
+ end
52
+ end
53
+ require 'active_record/with_eager_group'
54
+ ActiveRecord::Relation.send :prepend, ActiveRecord::WithEagerGroup
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class ActiveRecord::Base
2
4
  class <<self
3
5
  # Post.eager_group(:approved_comments_count, :comments_average_rating)
4
- delegate :eager_group, :to => :all
6
+ delegate :eager_group, to: :all
5
7
  end
6
8
  end
@@ -1,11 +1,11 @@
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
  alias_method_chain :exec_queries, :eager_group
@@ -26,6 +26,7 @@ class ActiveRecord::Relation
26
26
 
27
27
  def eager_group_values=(values)
28
28
  raise ImmutableRelation if @loaded
29
+
29
30
  @values[:eager_group] = values
30
31
  end
31
32
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module EagerGroup
2
4
  class Definition
3
5
  attr_reader :association, :aggregate_function, :column_name, :scope
@@ -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,44 @@ 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
- defination_key, arguments = eager_group_value.is_a?(Array) ? [eager_group_value.shift, eager_group_value] : [eager_group_value, nil]
15
- if definition = @klass.eager_group_definations[defination_key]
16
- reflection = @klass.reflect_on_association(definition.association)
17
- association_class = reflection.klass
18
- association_class = association_class.instance_exec(*arguments, &definition.scope) if definition.scope
19
- polymophic_as_condition = lambda {|reflection|
20
- if reflection.type
21
- ["#{reflection.name}.#{reflection.type} = ?", @klass.base_class.name]
22
- else
23
- []
24
- end
25
- }
15
+ definition_key, arguments = eager_group_value.is_a?(Array) ? [eager_group_value.shift, eager_group_value] : [eager_group_value, nil]
16
+ if definition_key.is_a?(Hash)
17
+ association_name, definition_key = *definition_key.first
18
+ @records = @records.flat_map { |record| record.send(association_name) }
19
+ @klass = @records.first.class
20
+ end
21
+ record_ids = @records.map { |record| record.send(primary_key) }
22
+ next unless definition = @klass.eager_group_definitions[definition_key]
26
23
 
27
- if reflection.through_reflection
28
- foreign_key = "#{reflection.through_reflection.name}.#{reflection.through_reflection.foreign_key}"
29
- aggregate_hash = association_class.joins(reflection.through_reflection.name)
30
- .where("#{foreign_key} IN (?)", record_ids)
31
- .where(polymophic_as_condition.call(reflection.through_reflection))
32
- .group("#{foreign_key}")
33
- .send(definition.aggregate_function, definition.column_name)
34
- else
35
- aggregate_hash = association_class.where(reflection.foreign_key => record_ids)
36
- .where(polymophic_as_condition.call(reflection))
37
- .group(reflection.foreign_key)
38
- .send(definition.aggregate_function, definition.column_name)
39
- end
40
- @records.each do |record|
41
- id = record.send primary_key
42
- record.send "#{defination_key}=", aggregate_hash[id] || 0
43
- end
24
+ reflection = @klass.reflect_on_association(definition.association)
25
+ association_class = reflection.klass
26
+ association_class = association_class.instance_exec(*arguments, &definition.scope) if definition.scope
27
+
28
+ if reflection.through_reflection
29
+ foreign_key = "#{reflection.through_reflection.name}.#{reflection.through_reflection.foreign_key}"
30
+ aggregate_hash = association_class.joins(reflection.through_reflection.name)
31
+ .where(foreign_key => record_ids)
32
+ .where(polymophic_as_condition(reflection.through_reflection))
33
+ .group(foreign_key)
34
+ .send(definition.aggregate_function, definition.column_name)
35
+ else
36
+ aggregate_hash = association_class.where(reflection.foreign_key => record_ids)
37
+ .where(polymophic_as_condition(reflection))
38
+ .group(reflection.foreign_key)
39
+ .send(definition.aggregate_function, definition.column_name)
40
+ end
41
+ @records.each do |record|
42
+ id = record.send(primary_key)
43
+ record.send("#{definition_key}=", aggregate_hash[id] || 0)
44
44
  end
45
45
  end
46
46
  end
47
+
48
+ private
49
+
50
+ def polymophic_as_condition(reflection)
51
+ reflection.type ? { reflection.name => { reflection.type => @klass.base_class.name } } : []
52
+ end
47
53
  end
48
54
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module EagerGroup
2
- VERSION = "0.5.0"
4
+ VERSION = '0.6.0'
3
5
  end
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.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Huang
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-09-22 00:00:00.000000000 Z
11
+ date: 2018-12-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: bundler
14
+ name: activerecord
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
@@ -25,89 +25,89 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: rake
28
+ name: activerecord-import
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '10.0'
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: '10.0'
40
+ version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: rspec
42
+ name: benchmark-ips
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "~>"
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '3.3'
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: '3.3'
54
+ version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: sqlite3
56
+ name: bundler
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - "~>"
59
+ - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: '1.3'
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: '1.3'
68
+ version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: activerecord
70
+ name: rake
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - ">="
73
+ - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '0'
75
+ version: '10.0'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - ">="
80
+ - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '0'
82
+ version: '10.0'
83
83
  - !ruby/object:Gem::Dependency
84
- name: activerecord-import
84
+ name: rspec
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - ">="
87
+ - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '0'
89
+ version: '3.3'
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: '3.3'
97
97
  - !ruby/object:Gem::Dependency
98
- name: benchmark-ips
98
+ name: sqlite3
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - ">="
101
+ - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '0'
103
+ version: '1.3'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - ">="
108
+ - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '0'
110
+ version: '1.3'
111
111
  description: Fix n+1 aggregate sql functions for rails
112
112
  email:
113
113
  - flyerhzm@gmail.com
@@ -126,6 +126,7 @@ files:
126
126
  - bin/console
127
127
  - bin/setup
128
128
  - eager_group.gemspec
129
+ - lib/active_record/with_eager_group.rb
129
130
  - lib/eager_group.rb
130
131
  - lib/eager_group/active_record_base.rb
131
132
  - lib/eager_group/active_record_relation.rb
@@ -152,7 +153,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
152
153
  version: '0'
153
154
  requirements: []
154
155
  rubyforge_project:
155
- rubygems_version: 2.5.1
156
+ rubygems_version: 2.6.14
156
157
  signing_key:
157
158
  specification_version: 4
158
159
  summary: Fix n+1 aggregate sql functions