preload_counts 0.0.2
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.
- data/.gitignore +7 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/README.rdoc +82 -0
- data/Rakefile +24 -0
- data/lib/preload_counts/ar.rb +103 -0
- data/lib/preload_counts/version.rb +3 -0
- data/lib/preload_counts.rb +3 -0
- data/preload_counts.gemspec +26 -0
- data/spec/active_record_spec.rb +99 -0
- data/spec/spec_helper.rb +9 -0
- metadata +148 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use ree-1.8.7@preload_counts
|
data/Gemfile
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
= Preload Counts
|
2
|
+
|
3
|
+
Did you ever write an index page that only needs the count of an association? Consider the following example:
|
4
|
+
|
5
|
+
# Models
|
6
|
+
class Post < ActiveRecord::Base
|
7
|
+
has_many :comments
|
8
|
+
end
|
9
|
+
|
10
|
+
class Comment < ActiveRecord::Base
|
11
|
+
belongs_to :post
|
12
|
+
|
13
|
+
named_scope :by_moderators, lambda { {:conditions => {:by_moderator => true} }
|
14
|
+
end
|
15
|
+
|
16
|
+
# Controller
|
17
|
+
def index
|
18
|
+
@posts = Post.all
|
19
|
+
end
|
20
|
+
|
21
|
+
# View
|
22
|
+
<% @posts.each do |post| %>
|
23
|
+
<%= post.name %>
|
24
|
+
(<%= post.comments.count %>)
|
25
|
+
(<%= post.comments.by_moderators.count %>)
|
26
|
+
<% end %>
|
27
|
+
|
28
|
+
|
29
|
+
This will create two count request to the database for each post each time the view
|
30
|
+
is rendered. This is really slow. Preload counts helps you minimize the number of
|
31
|
+
calls to the database with minimal code change.
|
32
|
+
|
33
|
+
== Usage
|
34
|
+
|
35
|
+
# Model
|
36
|
+
class Post < ActiveRecord::Base
|
37
|
+
# create a named_scope to preload the comments count as well as the comments
|
38
|
+
# by_moderators.
|
39
|
+
preload_counts :comments => [:by_moderators]
|
40
|
+
|
41
|
+
# preload_counts :comments would have only, preloaded the comments which
|
42
|
+
# would have been slightly faster. You can preload any number of scopes, but
|
43
|
+
# the more to preload, the more complex and slow the request will be.
|
44
|
+
has_many :comments
|
45
|
+
end
|
46
|
+
|
47
|
+
# Controller
|
48
|
+
def index
|
49
|
+
# Tell AR to preload the counts in the select request. If you don't specify
|
50
|
+
# this, the behaviour is to fallback to the slow count.
|
51
|
+
@posts = Post.preload_comment_counts
|
52
|
+
end
|
53
|
+
|
54
|
+
# View
|
55
|
+
# You have different accessor to get to the count. Beware of methods like
|
56
|
+
# .empty? that has an internal call to count.
|
57
|
+
<% @posts.each do |post| %>
|
58
|
+
<%= post.name %>
|
59
|
+
(<%= post.comments_count %>)
|
60
|
+
(<%= post.by_moderators_comments_count %>)
|
61
|
+
<% end %>
|
62
|
+
|
63
|
+
== Benchmark
|
64
|
+
|
65
|
+
More in depth benchmarking is needed, but here's sone annecdotal evidence.
|
66
|
+
|
67
|
+
Without: 650ms per request
|
68
|
+
With: 450ms per request
|
69
|
+
|
70
|
+
That's right, this saved me 200ms per request with very minimal code change.
|
71
|
+
|
72
|
+
== Support
|
73
|
+
|
74
|
+
This has been tested on Rails 2.3.12 and Rails 3.1.0
|
75
|
+
|
76
|
+
== Help
|
77
|
+
|
78
|
+
- My request is still slow?
|
79
|
+
|
80
|
+
It's possible that this isn't the fix you need. Sometime multiple small requests might be faster than one large request. Always benchmark your page before using this gem.
|
81
|
+
|
82
|
+
Another think you might want to look into is adding index to speedup the query that is being generated by preload_counts.
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "load_multi_rails_rake_tasks"
|
3
|
+
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
|
6
|
+
desc 'Default: run specs.'
|
7
|
+
task :default => :spec
|
8
|
+
|
9
|
+
desc "Run specs"
|
10
|
+
RSpec::Core::RakeTask.new do |t|
|
11
|
+
t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
|
12
|
+
# Put spec opts in a file named .rspec in root
|
13
|
+
end
|
14
|
+
|
15
|
+
desc "Generate code coverage"
|
16
|
+
RSpec::Core::RakeTask.new(:coverage) do |t|
|
17
|
+
t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
|
18
|
+
t.rcov = true
|
19
|
+
t.rcov_opts = ['--exclude', 'spec']
|
20
|
+
end
|
21
|
+
|
22
|
+
desc 'All tests'
|
23
|
+
task :test => :spec do
|
24
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# This adds a scope to preload the counts of an association in one SQL query.
|
2
|
+
#
|
3
|
+
# Consider the following code:
|
4
|
+
# Service.all.each{|s| puts s.incidents.acknowledged.count}
|
5
|
+
#
|
6
|
+
# Each time count is called, a db query is made to fetch the count.
|
7
|
+
#
|
8
|
+
# Adding this to the Service class:
|
9
|
+
#
|
10
|
+
# preload_counts :incidents => [:acknowledged]
|
11
|
+
#
|
12
|
+
# will add a preload_incident_counts scope to preload the counts and add
|
13
|
+
# accessors to the class. So our codes becaumes
|
14
|
+
#
|
15
|
+
# Service.preload_incident_counts.all.each{|s| puts s.acknowledged_incidents_count}
|
16
|
+
#
|
17
|
+
# And only requires one DB query.
|
18
|
+
module PreloadCounts
|
19
|
+
module ClassMethods
|
20
|
+
def preload_counts(options)
|
21
|
+
options = Array(options).inject({}) {|h, v| h[v] = []; h} unless options.is_a?(Hash)
|
22
|
+
options.each do |association, scopes|
|
23
|
+
scopes = scopes + [nil]
|
24
|
+
|
25
|
+
# Define singleton metho to load all counts
|
26
|
+
name = "preload_#{association.to_s.singularize}_counts"
|
27
|
+
singleton = class << self; self end
|
28
|
+
singleton.send :define_method, name do
|
29
|
+
sql = ['*'] + scopes_to_select(association, scopes)
|
30
|
+
sql = sql.join(', ')
|
31
|
+
scoped(:select => sql)
|
32
|
+
end
|
33
|
+
|
34
|
+
scopes.each do |scope|
|
35
|
+
# Define accessor for each count
|
36
|
+
accessor_name = find_accessor_name(association, scope)
|
37
|
+
define_method accessor_name do
|
38
|
+
result = send(association)
|
39
|
+
result = result.send(scope) if scope
|
40
|
+
(self[accessor_name] || result.size).to_i
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
def scopes_to_select(association, scopes)
|
49
|
+
scopes.map do |scope|
|
50
|
+
scope_to_select(association, scope)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def scope_to_select(association, scope)
|
55
|
+
resolved_association = association.to_s.singularize.camelize.constantize
|
56
|
+
conditions = []
|
57
|
+
|
58
|
+
if scope
|
59
|
+
if ActiveRecord::VERSION::MAJOR < 3
|
60
|
+
scope_sql = resolved_association.scopes[scope].call(resolved_association).send(:construct_finder_sql, {})
|
61
|
+
else
|
62
|
+
scope_sql = resolved_association.send(scope).to_sql
|
63
|
+
end
|
64
|
+
condition = scope_sql.gsub(/^.*WHERE/, '')
|
65
|
+
conditions << condition
|
66
|
+
end
|
67
|
+
|
68
|
+
association_condition = self.reflections[association].options[:conditions]
|
69
|
+
conditions << association_condition if association_condition
|
70
|
+
|
71
|
+
# FIXME This is a really hacking way of getting the named_scope condition.
|
72
|
+
# In Rails 3 we would have AREL to get to it.
|
73
|
+
sql = <<-SQL
|
74
|
+
(SELECT count(*)
|
75
|
+
FROM #{association}
|
76
|
+
WHERE #{association}.#{table_name.singularize}_id = #{table_name}.id AND
|
77
|
+
#{conditions_to_sql conditions}) as #{find_accessor_name(association, scope)}
|
78
|
+
SQL
|
79
|
+
end
|
80
|
+
|
81
|
+
def find_accessor_name(association, scope)
|
82
|
+
accessor_name = "#{association}_count"
|
83
|
+
accessor_name = "#{scope}_" + accessor_name if scope
|
84
|
+
accessor_name
|
85
|
+
end
|
86
|
+
|
87
|
+
def conditions_to_sql(conditions)
|
88
|
+
conditions = ["1 = 1"] if conditions.empty?
|
89
|
+
conditions.join(" AND ")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
module InstanceMethods
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.included(receiver)
|
97
|
+
receiver.extend ClassMethods
|
98
|
+
receiver.send :include, InstanceMethods
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
ActiveRecord::Base.class_eval { include PreloadCounts }
|
103
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "preload_counts/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "preload_counts"
|
7
|
+
s.version = PreloadCounts::VERSION
|
8
|
+
s.authors = ["Simon Mathieu"]
|
9
|
+
s.email = ["simon.math@gmail.com"]
|
10
|
+
s.homepage = "https://github.com/smathieu/preload_counts"
|
11
|
+
s.summary = %q{Preload association or scope counts.}
|
12
|
+
s.description = %q{Preload association or scope counts. This can greatly reduce the number of queries you have to perform and thus yield great performance gains.}
|
13
|
+
|
14
|
+
s.rubyforge_project = "preload_counts"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_development_dependency "rspec"
|
22
|
+
s.add_development_dependency "activerecord", "~> 3.2.0"
|
23
|
+
s.add_development_dependency "sqlite3"
|
24
|
+
s.add_development_dependency "ruby-debug"
|
25
|
+
s.add_development_dependency "multi_rails"
|
26
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
if ActiveRecord::VERSION::MAJOR < 3
|
5
|
+
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
|
6
|
+
else
|
7
|
+
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
|
8
|
+
ActiveRecord::Base.logger = Logger.new(nil)
|
9
|
+
end
|
10
|
+
|
11
|
+
def setup_db
|
12
|
+
ActiveRecord::Schema.define(:version => 1) do
|
13
|
+
create_table :posts do |t|
|
14
|
+
end
|
15
|
+
|
16
|
+
create_table :comments do |t|
|
17
|
+
t.integer :post_id, :null => false
|
18
|
+
t.datetime :deleted_at
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
setup_db
|
24
|
+
|
25
|
+
class Post < ActiveRecord::Base
|
26
|
+
has_many :comments
|
27
|
+
has_many :active_comments, :conditions => "deleted_at IS NULL", :class_name => 'Comment'
|
28
|
+
preload_counts :comments => [:with_even_id]
|
29
|
+
preload_counts :active_comments
|
30
|
+
end
|
31
|
+
|
32
|
+
class PostWithActiveComments < ActiveRecord::Base
|
33
|
+
set_table_name :posts
|
34
|
+
|
35
|
+
has_many :comments, :conditions => "deleted_at IS NULL"
|
36
|
+
preload_counts :comments
|
37
|
+
end
|
38
|
+
|
39
|
+
class Comment < ActiveRecord::Base
|
40
|
+
belongs_to :post
|
41
|
+
|
42
|
+
if ActiveRecord::VERSION::MAJOR < 3
|
43
|
+
named_scope :with_even_id, lambda { {:conditions => "comments.id % 2 == 0"} }
|
44
|
+
else
|
45
|
+
scope :with_even_id, where('id % 2 = 0')
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def create_data
|
50
|
+
post = Post.create
|
51
|
+
5.times { post.comments.create }
|
52
|
+
5.times { post.comments.create :deleted_at => Time.now }
|
53
|
+
end
|
54
|
+
|
55
|
+
create_data
|
56
|
+
|
57
|
+
describe Post do
|
58
|
+
it "should have a preload_comment_counts scope" do
|
59
|
+
Post.should respond_to(:preload_comment_counts)
|
60
|
+
end
|
61
|
+
|
62
|
+
describe 'instance' do
|
63
|
+
let(:post) { Post.first }
|
64
|
+
|
65
|
+
it "should have a comment_count accessor" do
|
66
|
+
post.should respond_to(:comments_count)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should be able to get count without preloading them" do
|
70
|
+
post.comments_count.should equal(10)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should have an active_comments_count accessor" do
|
74
|
+
post.should respond_to(:comments_count)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe 'instance with preloaded count' do
|
79
|
+
let(:post) { Post.preload_comment_counts.first }
|
80
|
+
|
81
|
+
it "should be able to get the association count" do
|
82
|
+
post.comments_count.should equal(10)
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should be able to get the association count with a scope" do
|
86
|
+
post.with_even_id_comments_count.should equal(5)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe PostWithActiveComments do
|
92
|
+
describe 'instance with preloaded count' do
|
93
|
+
let(:post) { PostWithActiveComments.preload_comment_counts.first }
|
94
|
+
|
95
|
+
it "should be able to get the association count" do
|
96
|
+
post.comments_count.should equal(5)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: preload_counts
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 2
|
10
|
+
version: 0.0.2
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Simon Mathieu
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-08-01 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rspec
|
22
|
+
version_requirements: &id001 !ruby/object:Gem::Requirement
|
23
|
+
none: false
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
hash: 3
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
version: "0"
|
31
|
+
type: :development
|
32
|
+
requirement: *id001
|
33
|
+
prerelease: false
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: activerecord
|
36
|
+
version_requirements: &id002 !ruby/object:Gem::Requirement
|
37
|
+
none: false
|
38
|
+
requirements:
|
39
|
+
- - ~>
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
hash: 15
|
42
|
+
segments:
|
43
|
+
- 3
|
44
|
+
- 2
|
45
|
+
- 0
|
46
|
+
version: 3.2.0
|
47
|
+
type: :development
|
48
|
+
requirement: *id002
|
49
|
+
prerelease: false
|
50
|
+
- !ruby/object:Gem::Dependency
|
51
|
+
name: sqlite3
|
52
|
+
version_requirements: &id003 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
hash: 3
|
58
|
+
segments:
|
59
|
+
- 0
|
60
|
+
version: "0"
|
61
|
+
type: :development
|
62
|
+
requirement: *id003
|
63
|
+
prerelease: false
|
64
|
+
- !ruby/object:Gem::Dependency
|
65
|
+
name: ruby-debug
|
66
|
+
version_requirements: &id004 !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
hash: 3
|
72
|
+
segments:
|
73
|
+
- 0
|
74
|
+
version: "0"
|
75
|
+
type: :development
|
76
|
+
requirement: *id004
|
77
|
+
prerelease: false
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: multi_rails
|
80
|
+
version_requirements: &id005 !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
hash: 3
|
86
|
+
segments:
|
87
|
+
- 0
|
88
|
+
version: "0"
|
89
|
+
type: :development
|
90
|
+
requirement: *id005
|
91
|
+
prerelease: false
|
92
|
+
description: Preload association or scope counts. This can greatly reduce the number of queries you have to perform and thus yield great performance gains.
|
93
|
+
email:
|
94
|
+
- simon.math@gmail.com
|
95
|
+
executables: []
|
96
|
+
|
97
|
+
extensions: []
|
98
|
+
|
99
|
+
extra_rdoc_files: []
|
100
|
+
|
101
|
+
files:
|
102
|
+
- .gitignore
|
103
|
+
- .rvmrc
|
104
|
+
- Gemfile
|
105
|
+
- README.rdoc
|
106
|
+
- Rakefile
|
107
|
+
- lib/preload_counts.rb
|
108
|
+
- lib/preload_counts/ar.rb
|
109
|
+
- lib/preload_counts/version.rb
|
110
|
+
- preload_counts.gemspec
|
111
|
+
- spec/active_record_spec.rb
|
112
|
+
- spec/spec_helper.rb
|
113
|
+
homepage: https://github.com/smathieu/preload_counts
|
114
|
+
licenses: []
|
115
|
+
|
116
|
+
post_install_message:
|
117
|
+
rdoc_options: []
|
118
|
+
|
119
|
+
require_paths:
|
120
|
+
- lib
|
121
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
122
|
+
none: false
|
123
|
+
requirements:
|
124
|
+
- - ">="
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
hash: 3
|
127
|
+
segments:
|
128
|
+
- 0
|
129
|
+
version: "0"
|
130
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
131
|
+
none: false
|
132
|
+
requirements:
|
133
|
+
- - ">="
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
hash: 3
|
136
|
+
segments:
|
137
|
+
- 0
|
138
|
+
version: "0"
|
139
|
+
requirements: []
|
140
|
+
|
141
|
+
rubyforge_project: preload_counts
|
142
|
+
rubygems_version: 1.8.24
|
143
|
+
signing_key:
|
144
|
+
specification_version: 3
|
145
|
+
summary: Preload association or scope counts.
|
146
|
+
test_files:
|
147
|
+
- spec/active_record_spec.rb
|
148
|
+
- spec/spec_helper.rb
|