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