preload_counts 0.0.3 → 0.0.4
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.
- checksums.yaml +4 -4
- data/.rspec +2 -0
- data/.rvmrc +1 -1
- data/.travis.yml +14 -0
- data/Gemfile +17 -0
- data/README.md +85 -0
- data/lib/preload_counts/ar.rb +15 -19
- data/lib/preload_counts/version.rb +1 -1
- data/preload_counts.gemspec +2 -2
- data/spec/active_record_spec.rb +17 -23
- data/spec/spec_helper.rb +2 -2
- metadata +23 -21
- data/README.rdoc +0 -82
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 08d789c6a318d6934f7d40b3af28308bcc72f99a
|
4
|
+
data.tar.gz: 49116bb125a4e84ed0b0fd50e1d7d509839f913f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b7768313608e32515cd120f442056455c59f43d5df190313b30ba69100ba2e32ad3780434f9c873c0a28b211c79d77b3149d2bb3552af498f2653f6be2283e05
|
7
|
+
data.tar.gz: c50fbead90d424477e048cf354e74fe4a7326e335bc893e168518e2382802ae8e454af890db934cbc1a6daa43d97b0fbff31622a59bb6b40fe4a67648286a7bd
|
data/.rspec
ADDED
data/.rvmrc
CHANGED
@@ -1 +1 @@
|
|
1
|
-
rvm use
|
1
|
+
rvm use 2.2.0@preload_counts --create
|
data/.travis.yml
ADDED
data/Gemfile
CHANGED
@@ -2,3 +2,20 @@ source "http://rubygems.org"
|
|
2
2
|
|
3
3
|
# Specify your gem's dependencies in preload_counts.gemspec
|
4
4
|
gemspec
|
5
|
+
|
6
|
+
activerecord_version = ENV["ACTIVERECORD_VERSION"] || "default"
|
7
|
+
|
8
|
+
activerecord_version = case activerecord_version
|
9
|
+
when "master"
|
10
|
+
{github: "rails/rails"}
|
11
|
+
when "default"
|
12
|
+
">= 3.2.0"
|
13
|
+
else
|
14
|
+
"~> #{activerecord_version}"
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
gem "activerecord", activerecord_version
|
19
|
+
gem "pry"
|
20
|
+
gem "awesome_print"
|
21
|
+
gem "rake"
|
data/README.md
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
[](https://travis-ci.org/smathieu/preload_counts)
|
2
|
+
|
3
|
+
# Preload Counts
|
4
|
+
|
5
|
+
Did you ever write an index page that only needs the count of an association? Consider the following example:
|
6
|
+
|
7
|
+
|
8
|
+
```ruby
|
9
|
+
# Models
|
10
|
+
class Post < ActiveRecord::Base
|
11
|
+
has_many :comments
|
12
|
+
end
|
13
|
+
|
14
|
+
class Comment < ActiveRecord::Base
|
15
|
+
belongs_to :post
|
16
|
+
|
17
|
+
named_scope :by_moderators, lambda { {:conditions => {:by_moderator => true} }
|
18
|
+
end
|
19
|
+
|
20
|
+
# Controller
|
21
|
+
def index
|
22
|
+
@posts = Post.all
|
23
|
+
end
|
24
|
+
|
25
|
+
# View
|
26
|
+
<% @posts.each do |post| %>
|
27
|
+
<%= post.name %>
|
28
|
+
(<%= post.comments.count %>)
|
29
|
+
(<%= post.comments.by_moderators.count %>)
|
30
|
+
<% end %>
|
31
|
+
```
|
32
|
+
|
33
|
+
This will create two count request to the database for each post each time the view
|
34
|
+
is rendered. This is really slow. Preload counts helps you minimize the number of
|
35
|
+
calls to the database with minimal code change.
|
36
|
+
|
37
|
+
## Usage
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
# Model
|
41
|
+
class Post < ActiveRecord::Base
|
42
|
+
# create a named_scope to preload the comments count as well as the comments
|
43
|
+
# by_moderators.
|
44
|
+
preload_counts :comments => [:by_moderators]
|
45
|
+
|
46
|
+
# preload_counts :comments would have only, preloaded the comments which
|
47
|
+
# would have been slightly faster. You can preload any number of scopes, but
|
48
|
+
# the more to preload, the more complex and slow the request will be.
|
49
|
+
has_many :comments
|
50
|
+
end
|
51
|
+
|
52
|
+
# Controller
|
53
|
+
def index
|
54
|
+
# Tell AR to preload the counts in the select request. If you don't specify
|
55
|
+
# this, the behaviour is to fallback to the slow count.
|
56
|
+
@posts = Post.preload_comment_counts
|
57
|
+
end
|
58
|
+
|
59
|
+
# View
|
60
|
+
# You have different accessor to get to the count. Beware of methods like
|
61
|
+
# .empty? that has an internal call to count.
|
62
|
+
<% @posts.each do |post| %>
|
63
|
+
<%= post.name %>
|
64
|
+
(<%= post.comments_count %>)
|
65
|
+
(<%= post.by_moderators_comments_count %>)
|
66
|
+
<% end %>
|
67
|
+
```
|
68
|
+
|
69
|
+
## Benchmark
|
70
|
+
More in depth benchmarking is needed, but here's sone annecdotal evidence.
|
71
|
+
|
72
|
+
Without: 650ms per request
|
73
|
+
With: 450ms per request
|
74
|
+
|
75
|
+
That's right, this saved me 200ms per request with very minimal code change.
|
76
|
+
|
77
|
+
## Support
|
78
|
+
This has been tested on Rails 2.3.12, 3.1.0 and 4.0.2.
|
79
|
+
|
80
|
+
## Help
|
81
|
+
### My requests are still slow?
|
82
|
+
|
83
|
+
It's possible that this isn't the fix you need. Sometimes multiple small requests might be faster than one large request. Always benchmark your page before using this gem.
|
84
|
+
|
85
|
+
Another thing you might want to look into is adding an index to speed up the query that is being generated by preload_counts.
|
data/lib/preload_counts/ar.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
-
# This adds a scope to preload the counts of an association in one SQL query.
|
1
|
+
# This adds a scope to preload the counts of an association in one SQL query.
|
2
2
|
#
|
3
3
|
# Consider the following code:
|
4
4
|
# Service.all.each{|s| puts s.incidents.acknowledged.count}
|
5
5
|
#
|
6
|
-
# Each time count is called, a db query is made to fetch the count.
|
6
|
+
# Each time count is called, a db query is made to fetch the count.
|
7
7
|
#
|
8
8
|
# Adding this to the Service class:
|
9
9
|
#
|
@@ -28,22 +28,22 @@ module PreloadCounts
|
|
28
28
|
singleton.send :define_method, name do
|
29
29
|
sql = ["#{table_name}.*"] + scopes_to_select(association, scopes)
|
30
30
|
sql = sql.join(', ')
|
31
|
-
|
31
|
+
all.select(sql)
|
32
32
|
end
|
33
33
|
|
34
34
|
scopes.each do |scope|
|
35
35
|
# Define accessor for each count
|
36
36
|
accessor_name = find_accessor_name(association, scope)
|
37
37
|
define_method accessor_name do
|
38
|
-
result =
|
39
|
-
result = result.
|
38
|
+
result = public_send(association)
|
39
|
+
result = result.public_send(scope) if scope
|
40
40
|
(self[accessor_name] || result.size).to_i
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
44
44
|
end
|
45
45
|
end
|
46
|
-
|
46
|
+
|
47
47
|
private
|
48
48
|
def scopes_to_select(association, scopes)
|
49
49
|
scopes.map do |scope|
|
@@ -56,25 +56,21 @@ module PreloadCounts
|
|
56
56
|
conditions = []
|
57
57
|
|
58
58
|
if scope
|
59
|
-
|
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
|
59
|
+
scope_sql = resolved_association.send(scope).to_sql
|
64
60
|
condition = scope_sql.gsub(/^.*WHERE/, '')
|
65
61
|
conditions << condition
|
66
62
|
end
|
67
63
|
|
68
|
-
|
69
|
-
|
64
|
+
r_scope = self.reflections.with_indifferent_access[association].scope
|
65
|
+
if r_scope
|
66
|
+
conditions += self.instance_eval(&r_scope).where_values
|
67
|
+
end
|
70
68
|
|
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
69
|
sql = <<-SQL
|
74
|
-
(SELECT count(*)
|
75
|
-
FROM #{association}
|
76
|
-
WHERE #{association}.#{table_name.singularize}_id = #{table_name}.id AND
|
77
|
-
#{conditions_to_sql conditions})
|
70
|
+
(SELECT count(*)
|
71
|
+
FROM #{association}
|
72
|
+
WHERE #{association}.#{table_name.singularize}_id = #{table_name}.id AND
|
73
|
+
#{conditions_to_sql conditions}) AS #{find_accessor_name(association, scope)}
|
78
74
|
SQL
|
79
75
|
end
|
80
76
|
|
data/preload_counts.gemspec
CHANGED
@@ -18,7 +18,7 @@ Gem::Specification.new do |s|
|
|
18
18
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
19
|
s.require_paths = ["lib"]
|
20
20
|
|
21
|
-
s.
|
22
|
-
s.add_development_dependency "
|
21
|
+
s.add_dependency "activerecord", "> 3.2.0"
|
22
|
+
s.add_development_dependency "rspec", "~> 2.0"
|
23
23
|
s.add_development_dependency "sqlite3"
|
24
24
|
end
|
data/spec/active_record_spec.rb
CHANGED
@@ -1,12 +1,10 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require 'logger'
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
ActiveRecord::Base.logger = Logger.new(nil)
|
9
|
-
end
|
4
|
+
puts "Using ActiveRecord #{ActiveRecord::VERSION::STRING}"
|
5
|
+
|
6
|
+
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
|
7
|
+
ActiveRecord::Base.logger = Logger.new(nil)
|
10
8
|
|
11
9
|
def setup_db
|
12
10
|
ActiveRecord::Schema.define(:version => 1) do
|
@@ -14,40 +12,36 @@ def setup_db
|
|
14
12
|
end
|
15
13
|
|
16
14
|
create_table :comments do |t|
|
17
|
-
t.integer :post_id, :null => false
|
15
|
+
t.integer :post_id, :null => false
|
18
16
|
t.datetime :deleted_at
|
19
17
|
end
|
20
18
|
end
|
21
19
|
end
|
22
20
|
|
23
|
-
setup_db
|
21
|
+
setup_db
|
24
22
|
|
25
23
|
class Post < ActiveRecord::Base
|
26
24
|
has_many :comments
|
27
|
-
has_many :active_comments,
|
25
|
+
has_many :active_comments, -> { where( "deleted_at IS NULL") }, :class_name => 'Comment'
|
28
26
|
preload_counts :comments => [:with_even_id]
|
29
27
|
preload_counts :active_comments
|
30
28
|
end
|
31
29
|
|
32
30
|
class PostWithActiveComments < ActiveRecord::Base
|
33
|
-
|
31
|
+
self.table_name = :posts
|
34
32
|
|
35
|
-
has_many :comments,
|
33
|
+
has_many :comments, -> { where "deleted_at IS NULL" }
|
36
34
|
preload_counts :comments
|
37
35
|
end
|
38
36
|
|
39
37
|
class Comment < ActiveRecord::Base
|
40
38
|
belongs_to :post
|
41
39
|
|
42
|
-
|
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
|
40
|
+
scope :with_even_id, -> { where('id % 2 = 0') }
|
47
41
|
end
|
48
42
|
|
49
43
|
def create_data
|
50
|
-
post = Post.create
|
44
|
+
post = Post.create
|
51
45
|
5.times { post.comments.create }
|
52
46
|
5.times { post.comments.create :deleted_at => Time.now }
|
53
47
|
end
|
@@ -56,14 +50,14 @@ create_data
|
|
56
50
|
|
57
51
|
describe Post do
|
58
52
|
it "should have a preload_comment_counts scope" do
|
59
|
-
Post.should respond_to(:preload_comment_counts)
|
53
|
+
Post.should respond_to(:preload_comment_counts)
|
60
54
|
end
|
61
55
|
|
62
56
|
describe 'instance' do
|
63
|
-
let(:post) { Post.first }
|
57
|
+
let(:post) { Post.first }
|
64
58
|
|
65
59
|
it "should have a comment_count accessor" do
|
66
|
-
post.should respond_to(:comments_count)
|
60
|
+
post.should respond_to(:comments_count)
|
67
61
|
end
|
68
62
|
|
69
63
|
it "should be able to get count without preloading them" do
|
@@ -71,12 +65,12 @@ describe Post do
|
|
71
65
|
end
|
72
66
|
|
73
67
|
it "should have an active_comments_count accessor" do
|
74
|
-
post.should respond_to(:comments_count)
|
68
|
+
post.should respond_to(:comments_count)
|
75
69
|
end
|
76
70
|
end
|
77
71
|
|
78
72
|
describe 'instance with preloaded count' do
|
79
|
-
let(:post) { Post.preload_comment_counts.first }
|
73
|
+
let(:post) { Post.preload_comment_counts.first }
|
80
74
|
|
81
75
|
it "should be able to get the association count" do
|
82
76
|
post.comments_count.should equal(10)
|
@@ -90,7 +84,7 @@ end
|
|
90
84
|
|
91
85
|
describe PostWithActiveComments do
|
92
86
|
describe 'instance with preloaded count' do
|
93
|
-
let(:post) { PostWithActiveComments.preload_comment_counts.first }
|
87
|
+
let(:post) { PostWithActiveComments.preload_comment_counts.first }
|
94
88
|
|
95
89
|
it "should be able to get the association count" do
|
96
90
|
post.comments_count.should equal(5)
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,55 +1,55 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: preload_counts
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Simon Mathieu
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-01-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: activerecord
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - ">"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
20
|
-
type: :
|
19
|
+
version: 3.2.0
|
20
|
+
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - ">"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: 3.2.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: rspec
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - ~>
|
31
|
+
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: '2.0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - ~>
|
38
|
+
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: '2.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: sqlite3
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- -
|
45
|
+
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
47
|
version: '0'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- -
|
52
|
+
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
55
|
description: Preload association or scope counts. This can greatly reduce the number
|
@@ -60,10 +60,12 @@ executables: []
|
|
60
60
|
extensions: []
|
61
61
|
extra_rdoc_files: []
|
62
62
|
files:
|
63
|
-
- .gitignore
|
64
|
-
- .
|
63
|
+
- ".gitignore"
|
64
|
+
- ".rspec"
|
65
|
+
- ".rvmrc"
|
66
|
+
- ".travis.yml"
|
65
67
|
- Gemfile
|
66
|
-
- README.
|
68
|
+
- README.md
|
67
69
|
- Rakefile
|
68
70
|
- lib/preload_counts.rb
|
69
71
|
- lib/preload_counts/ar.rb
|
@@ -80,17 +82,17 @@ require_paths:
|
|
80
82
|
- lib
|
81
83
|
required_ruby_version: !ruby/object:Gem::Requirement
|
82
84
|
requirements:
|
83
|
-
- -
|
85
|
+
- - ">="
|
84
86
|
- !ruby/object:Gem::Version
|
85
87
|
version: '0'
|
86
88
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
87
89
|
requirements:
|
88
|
-
- -
|
90
|
+
- - ">="
|
89
91
|
- !ruby/object:Gem::Version
|
90
92
|
version: '0'
|
91
93
|
requirements: []
|
92
94
|
rubyforge_project: preload_counts
|
93
|
-
rubygems_version: 2.
|
95
|
+
rubygems_version: 2.4.5
|
94
96
|
signing_key:
|
95
97
|
specification_version: 4
|
96
98
|
summary: Preload association or scope counts.
|
data/README.rdoc
DELETED
@@ -1,82 +0,0 @@
|
|
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.
|