eager_group 0.5.0 → 0.6.0

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