Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit aa9b1ea
Showing
10 changed files
with
206 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
*.gem | ||
.bundle | ||
Gemfile.lock | ||
pkg/* | ||
tags | ||
*.swp | ||
*.swo |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
rvm use 1.8.7@preload_counts |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
source "http://rubygems.org" | ||
|
||
# Specify your gem's dependencies in preload_counts.gemspec | ||
gemspec |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
require "bundler/gem_tasks" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
require "preload_counts/version" | ||
require "preload_counts/ar" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
# This adds a scope to preload the counts of an association in one SQL query. | ||
# | ||
# Consider the following code: | ||
# Service.all.each{|s| puts s.incidents.acknowledged.count} | ||
# | ||
# Each time count is called, a db query is made to fetch the count. | ||
# | ||
# Adding this to the Service class: | ||
# | ||
# preload_counts :incidents => [:acknowledged] | ||
# | ||
# will add a preload_incident_counts scope to preload the counts and add | ||
# accessors to the class. So our codes becaumes | ||
# | ||
# Service.preload_incident_counts.all.each{|s| puts s.acknowledged_incidents_count} | ||
# | ||
# And only requires one DB query. | ||
module PreloadCounts | ||
module ClassMethods | ||
def preload_counts(options) | ||
options = Array(options).inject({}) {|h, v| h[v] = []; h} unless options.is_a?(Hash) | ||
options.each do |association, scopes| | ||
scopes = scopes + [nil] | ||
|
||
# Define singleton metho to load all counts | ||
name = "preload_#{association.to_s.singularize}_counts" | ||
singleton = class << self; self end | ||
singleton.send :define_method, name do | ||
sql = ['*'] + scopes_to_select(association, scopes) | ||
sql = sql.join(', ') | ||
scoped(:select => sql) | ||
end | ||
|
||
scopes.each do |scope| | ||
# Define accessor for each count | ||
accessor_name = find_accessor_name(association, scope) | ||
define_method accessor_name do | ||
result = send(association) | ||
result = result.send(scope) if scope | ||
(self[accessor_name] || result.size).to_i | ||
end | ||
end | ||
|
||
end | ||
end | ||
|
||
private | ||
def scopes_to_select(association, scopes) | ||
scopes.map do |scope| | ||
scope_to_select(association, scope) | ||
end | ||
end | ||
|
||
def scope_to_select(association, scope) | ||
resolved_association = association.to_s.singularize.camelize.constantize | ||
scope_sql = if scope | ||
resolved_association.scopes[scope].call(resolved_association).send(:construct_finder_sql, {}) | ||
else | ||
"1 = 1" | ||
end | ||
# FIXME This is a really hacking way of getting the named_scope condition. | ||
# In Rails 3 we would have AREL to get to it. | ||
condition = scope_sql.gsub(/^.*WHERE/, '') | ||
sql = <<-SQL | ||
(SELECT count(*) | ||
FROM #{association} | ||
WHERE #{association}.#{to_s.underscore}_id = #{table_name}.id AND | ||
#{condition}) as #{find_accessor_name(association, scope)} | ||
SQL | ||
end | ||
|
||
def find_accessor_name(association, scope) | ||
accessor_name = "#{association}_count" | ||
accessor_name = "#{scope}_" + accessor_name if scope | ||
accessor_name | ||
end | ||
end | ||
|
||
module InstanceMethods | ||
end | ||
|
||
def self.included(receiver) | ||
receiver.extend ClassMethods | ||
receiver.send :include, InstanceMethods | ||
end | ||
end | ||
|
||
ActiveRecord::Base.class_eval { include PreloadCounts } | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
module PreloadCounts | ||
VERSION = "0.0.1" | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# -*- encoding: utf-8 -*- | ||
$:.push File.expand_path("../lib", __FILE__) | ||
require "preload_counts/version" | ||
|
||
Gem::Specification.new do |s| | ||
s.name = "preload_counts" | ||
s.version = PreloadCounts::VERSION | ||
s.authors = ["Simon Mathieu"] | ||
s.email = ["simon.math@gmail.com"] | ||
s.homepage = "" | ||
s.summary = %q{Preload association or scope counts.} | ||
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.} | ||
|
||
s.rubyforge_project = "preload_counts" | ||
|
||
s.files = `git ls-files`.split("\n") | ||
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") | ||
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } | ||
s.require_paths = ["lib"] | ||
|
||
s.add_development_dependency "rspec" | ||
s.add_development_dependency "rails", "~> 2.3.12" | ||
s.add_development_dependency "sqlite3" | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
require 'spec_helper' | ||
|
||
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:") | ||
|
||
def setup_db | ||
ActiveRecord::Schema.define(:version => 1) do | ||
create_table :posts do |t| | ||
end | ||
|
||
create_table :comments do |t| | ||
t.integer :post_id, :nulll => false | ||
end | ||
end | ||
end | ||
|
||
setup_db | ||
|
||
class Post < ActiveRecord::Base | ||
has_many :comments | ||
preload_counts :comments => [:with_even_id] | ||
end | ||
|
||
class Comment < ActiveRecord::Base | ||
belongs_to :post | ||
|
||
named_scope :with_even_id, lambda { {:conditions => "comments.id % 2 == 0"} } | ||
end | ||
|
||
def create_data | ||
post = Post.create | ||
10.times { post.comments.create } | ||
end | ||
|
||
create_data | ||
|
||
describe Post do | ||
it "should have a preload_comment_counts scope" do | ||
Post.should respond_to(:preload_comment_counts) | ||
end | ||
|
||
describe 'instance' do | ||
let(:post) { Post.first } | ||
|
||
it "should have a comment_count accessor" do | ||
post.should respond_to(:comments_count) | ||
end | ||
|
||
it "should be able to get count without preloading them" do | ||
post.comments_count.should equal(10) | ||
end | ||
end | ||
|
||
describe 'instance with preloaded count' do | ||
let(:post) { Post.preload_comment_counts.first } | ||
|
||
it "should be able to get the association count" do | ||
post.comments_count.should equal(10) | ||
end | ||
|
||
it "should be able to get the association count with a named scope" do | ||
post.with_even_id_comments_count.should equal(5) | ||
end | ||
end | ||
end | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
require 'rspec' | ||
require 'active_record' | ||
require 'preload_counts' | ||
|
||
RSpec.configure do |config| | ||
config.color_enabled = true | ||
config.formatter = 'documentation' | ||
end | ||
|