preload_counts 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ tags
6
+ *.swp
7
+ *.swo
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use ree-1.8.7@preload_counts
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in preload_counts.gemspec
4
+ gemspec
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,3 @@
1
+ module PreloadCounts
2
+ VERSION = "0.0.2"
3
+ end
@@ -0,0 +1,3 @@
1
+ require "preload_counts/version"
2
+ require "preload_counts/ar"
3
+
@@ -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
@@ -0,0 +1,9 @@
1
+ require 'rspec'
2
+ require 'active_record'
3
+ require 'preload_counts'
4
+
5
+ RSpec.configure do |config|
6
+ config.color_enabled = true
7
+ config.formatter = 'documentation'
8
+ end
9
+
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