eager_group 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e3e98c847d6d1283e9687c8b1b6a770dddd9c645
4
+ data.tar.gz: 7525252e8291a4b9ee3c270b6e9f318d5bb1b5da
5
+ SHA512:
6
+ metadata.gz: 9ee51ef879b7359070dc7f45e9138dabc776e29a209ba2499bb7f799c373ea0c4f6d9c92ea0a04a313dc8b46dcd366bb478bcb44c817bf44a9cfe214231c4389
7
+ data.tar.gz: 3f201ec3c596ec40990e6b9d5a669b76388df8566a8b31cc15f4ae5a02039cab5276f1f89221e9cd0827806842c68d1f4dc46a4be8eddab449190d44a6a4a102
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2
4
+ env:
5
+ - DB=sqlite
6
+ script: bundle exec rspec spec
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Next Release
2
+
3
+ ## 0.1.0 (06/29/2015)
4
+
5
+ * first release
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in eager_group.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # EagerGroup
2
+
3
+ [![Build Status](https://secure.travis-ci.org/xinminlabs/eager_group.png)](http://travis-ci.org/xinminlabs/eager_group)
4
+
5
+ Fix n+1 aggregate sql functions for rails, like
6
+
7
+ SELECT "posts".* FROM "posts";
8
+ SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = 1 AND "comments"."status" = 'approved'
9
+ SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = 2 AND "comments"."status" = 'approved'
10
+ SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = 3 AND "comments"."status" = 'approved'
11
+
12
+ =>
13
+
14
+ SELECT "posts".* FROM "posts";
15
+ 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;
16
+
17
+ or
18
+
19
+ SELECT "posts".* FROM "posts";
20
+ SELECT AVG("comments"."rating") AS avg_id FROM "comments" WHERE "comments"."post_id" = 1;
21
+ SELECT AVG("comments"."rating") AS avg_id FROM "comments" WHERE "comments"."post_id" = 2;
22
+ SELECT AVG("comments"."rating") AS avg_id FROM "comments" WHERE "comments"."post_id" = 3;
23
+
24
+ =>
25
+
26
+ SELECT "posts".* FROM "posts";
27
+ 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;
28
+
29
+ ## Installation
30
+
31
+ Add this line to your application's Gemfile:
32
+
33
+ ```ruby
34
+ gem 'eager_group'
35
+ ```
36
+
37
+ And then execute:
38
+
39
+ $ bundle
40
+
41
+ Or install it yourself as:
42
+
43
+ $ gem install eager_group
44
+
45
+ ## Usage
46
+
47
+ First you need to define what aggregate function you want to eager
48
+ load.
49
+
50
+ class Post
51
+ has_many :comments
52
+
53
+ define_eager_group :comments_average_rating, :comments, :average, :rating
54
+ define_eager_group :approved_comments_count, :comments, :count, :*, -> { approved }
55
+ end
56
+
57
+ class Comment
58
+ belongs_to :post
59
+
60
+ scope :approved, -> { where(status: 'approved') }
61
+ end
62
+
63
+ The parameters for `define_eager_group` are as follows
64
+
65
+ * `definition_name`, it's used to be a reference in `eager_group` query
66
+ method, it also generates a method with the same name to fetch the
67
+ result.
68
+ * `association`, association name you want to aggregate.
69
+ * `aggregate_function`, aggregate sql function, can be one of `average`,
70
+ `count`, `maximum`, `minimum`, `sum`.
71
+ * `column_name`, aggregate column name, it can be `:*` for `count`
72
+ * `scope`, scope is optional, it's used to filter data for aggregation.
73
+
74
+ Then you can use `eager_group` to fix n+1 aggregate sql functions
75
+ when querying
76
+
77
+ posts = Post.all.eager_group(:comments_average_rating, :approved_comments_count)
78
+ posts.each do |post|
79
+ post.comments_average_rating
80
+ post.approved_comments_count
81
+ end
82
+
83
+ EagerGroup will execute `GROUP BY` sqls for you then set the value of
84
+ attributes.
85
+
86
+ ## Benchmark
87
+
88
+ I wrote a benchmark script [here][1], it queries approved comments count
89
+ and comments average rating for 20 posts, with eager group, it gets 10
90
+ times faster, WOW!
91
+
92
+ ## Contributing
93
+
94
+ Bug reports and pull requests are welcome on GitHub at https://github.com/xinminlabs/eager_group.
95
+
96
+ [1]: https://github.com/xinminlabs/eager_group/blob/master/benchmark.rb
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/benchmark.rb ADDED
@@ -0,0 +1,87 @@
1
+ # Calculating -------------------------------------
2
+ # Without EagerGroup 2.000 i/100ms
3
+ # With EagerGroup 28.000 i/100ms
4
+ # -------------------------------------------------
5
+ # Without EagerGroup 28.883 (± 6.9%) i/s - 144.000
6
+ # With EagerGroup 281.755 (± 5.0%) i/s - 1.428k
7
+ #
8
+ # Comparison:
9
+ # With EagerGroup: 281.8 i/s
10
+ # Without EagerGroup: 28.9 i/s - 9.76x slower
11
+ $: << 'lib'
12
+ require 'benchmark/ips'
13
+ require 'active_record'
14
+ require 'activerecord-import'
15
+ require 'eager_group'
16
+
17
+ class Post < ActiveRecord::Base
18
+ has_many :comments
19
+
20
+ define_eager_group :comments_average_rating, :comments, :average, :rating
21
+ define_eager_group :approved_comments_count, :comments, :count, :*, -> { approved }
22
+ end
23
+
24
+ class Comment < ActiveRecord::Base
25
+ belongs_to :post
26
+
27
+ scope :approved, -> { where(status: 'approved') }
28
+ end
29
+
30
+ # create database eager_group_benchmark;
31
+ ActiveRecord::Base.establish_connection(:adapter => 'mysql2', :database => 'eager_group_benchmark', :server => '/tmp/mysql.socket', :username => 'root')
32
+
33
+ ActiveRecord::Base.connection.tables.each do |table|
34
+ ActiveRecord::Base.connection.drop_table(table)
35
+ end
36
+
37
+ ActiveRecord::Schema.define do
38
+ self.verbose = false
39
+
40
+ create_table :posts, :force => true do |t|
41
+ t.string :title
42
+ t.string :body
43
+ t.timestamps null: false
44
+ end
45
+
46
+ create_table :comments, :force => true do |t|
47
+ t.string :body
48
+ t.string :status
49
+ t.integer :rating
50
+ t.integer :post_id
51
+ t.timestamps null: false
52
+ end
53
+ end
54
+
55
+ posts_size = 100
56
+ comments_size = 1000
57
+
58
+ posts = []
59
+ posts_size.times do |i|
60
+ posts << Post.new(:title => "Title #{i}", :body => "Body #{i}")
61
+ end
62
+ Post.import posts
63
+ post_ids = Post.all.pluck(:id)
64
+
65
+ comments = []
66
+ 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)
68
+ end
69
+ Comment.import comments
70
+
71
+ Benchmark.ips do |x|
72
+ x.report("Without EagerGroup") do
73
+ Post.limit(20).each do |post|
74
+ post.comments.approved.count
75
+ post.comments.approved.average('rating')
76
+ end
77
+ end
78
+
79
+ x.report("With EagerGroup") do
80
+ Post.eager_group(:approved_comments_count, :comments_average_rating).limit(20).each do |post|
81
+ post.approved_comments_count
82
+ post.comments_average_rating
83
+ end
84
+ end
85
+
86
+ x.compare!
87
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "eager_group"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'eager_group/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "eager_group"
8
+ spec.version = EagerGroup::VERSION
9
+ spec.authors = ["Richard Huang"]
10
+ spec.email = ["flyerhzm@gmail.com"]
11
+
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"
15
+
16
+ spec.license = 'MIT'
17
+
18
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_dependency "activerecord"
24
+ spec.add_development_dependency "bundler"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "rspec", "~> 3.3"
27
+ spec.add_development_dependency "sqlite3", "~> 1.3"
28
+ spec.add_development_dependency "activerecord-import"
29
+ spec.add_development_dependency "benchmark-ips"
30
+ end
@@ -0,0 +1,29 @@
1
+ require "eager_group/version"
2
+ require 'active_record'
3
+ require 'eager_group/active_record_base'
4
+ require 'eager_group/active_record_relation'
5
+
6
+ module EagerGroup
7
+ autoload :Preloader, 'eager_group/preloader'
8
+ autoload :Definition, 'eager_group/definition'
9
+
10
+ def self.included(base)
11
+ base.extend ClassMethods
12
+ end
13
+
14
+ module ClassMethods
15
+ attr_reader :eager_group_definations
16
+
17
+ # class Post
18
+ # define_eager_group :comments_avergage_rating, :comments, :average, :rating
19
+ # define_eager_group :approved_comments_count, :comments, :count, :*, -> { approved }
20
+ # end
21
+ 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
+ end
26
+ end
27
+ end
28
+
29
+ ActiveRecord::Base.send :include, EagerGroup
@@ -0,0 +1,6 @@
1
+ class ActiveRecord::Base
2
+ class <<self
3
+ # Post.eager_group(:approved_comments_count, :comments_average_rating)
4
+ delegate :eager_group, :to => :all
5
+ end
6
+ end
@@ -0,0 +1,31 @@
1
+ class ActiveRecord::Relation
2
+ # Post.all.eager_group(:approved_comments_count, :comments_average_rating)
3
+
4
+ def exec_queries_with_eager_group
5
+ 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
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
+ @values[:eager_group] = values
30
+ end
31
+ end
@@ -0,0 +1,12 @@
1
+ module EagerGroup
2
+ class Definition
3
+ attr_reader :association, :aggregate_function, :column_name, :scope
4
+
5
+ def initialize(association, aggregate_function, column_name, scope)
6
+ @association = association
7
+ @aggregate_function = aggregate_function
8
+ @column_name = column_name
9
+ @scope = scope
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,30 @@
1
+ module EagerGroup
2
+ class Preloader
3
+ def initialize(klass, records, eager_group_values)
4
+ @klass = klass
5
+ @records = Array.wrap(records).compact.uniq
6
+ @eager_group_values = eager_group_values
7
+ end
8
+
9
+ # Preload aggregate functions
10
+ def run
11
+ primary_key = @klass.primary_key
12
+ record_ids = @records.map { |record| record.send primary_key }
13
+ @eager_group_values.each do |eager_group_value|
14
+ definition = @klass.eager_group_definations[eager_group_value]
15
+ if definition
16
+ reflect = @klass.reflect_on_association(definition.association)
17
+ association_class = reflect.class_name.constantize
18
+ association_class = association_class.instance_exec(&definition.scope) if definition.scope
19
+ aggregate_hash = association_class.where(reflect.foreign_key => record_ids)
20
+ .group(reflect.foreign_key)
21
+ .send(definition.aggregate_function, definition.column_name)
22
+ @records.each do |record|
23
+ id = record.send primary_key
24
+ record.send "#{eager_group_value}=", aggregate_hash[id]
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,3 @@
1
+ module EagerGroup
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,159 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: eager_group
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Richard Huang
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2015-06-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.3'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.3'
69
+ - !ruby/object:Gem::Dependency
70
+ name: sqlite3
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.3'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.3'
83
+ - !ruby/object:Gem::Dependency
84
+ name: activerecord-import
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: benchmark-ips
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: Fix n+1 aggregate sql functions for rails
112
+ email:
113
+ - flyerhzm@gmail.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".gitignore"
119
+ - ".rspec"
120
+ - ".travis.yml"
121
+ - CHANGELOG.md
122
+ - Gemfile
123
+ - README.md
124
+ - Rakefile
125
+ - benchmark.rb
126
+ - bin/console
127
+ - bin/setup
128
+ - eager_group.gemspec
129
+ - lib/eager_group.rb
130
+ - lib/eager_group/active_record_base.rb
131
+ - lib/eager_group/active_record_relation.rb
132
+ - lib/eager_group/definition.rb
133
+ - lib/eager_group/preloader.rb
134
+ - lib/eager_group/version.rb
135
+ homepage: https://github.com/xinminlabs/eager_group
136
+ licenses:
137
+ - MIT
138
+ metadata: {}
139
+ post_install_message:
140
+ rdoc_options: []
141
+ require_paths:
142
+ - lib
143
+ required_ruby_version: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ version: '0'
148
+ required_rubygems_version: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ requirements: []
154
+ rubyforge_project:
155
+ rubygems_version: 2.4.7
156
+ signing_key:
157
+ specification_version: 4
158
+ summary: Fix n+1 aggregate sql functions
159
+ test_files: []