activerecord-has_count 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1638ba180bf14cd976c29912e38505fb21b73a4b
4
- data.tar.gz: dd52048a4744eb4f5b740b0b0df9ca74f8a8eb99
3
+ metadata.gz: 69e774d016a1d4b507eb55f8f0f9504cbf198a3b
4
+ data.tar.gz: abbd2d57baa8ebdf82d65ce8f6080febafaaaf57
5
5
  SHA512:
6
- metadata.gz: c3e3313087b1e78e6ef21b26c938a4ebe17124c74ac9c596970f4614e329f7f89375f1bdf3c2586b6dcd068f0e3ed705ccb38526ddb8b2d59e7635d6e8179930
7
- data.tar.gz: 8128b246278b8c678a7fd8a52588e570444555e8e5362a0636cdb86170b77e92c8e2a3695035d393daa485ef7aad6d812ef62ddd923c64ce8065f1d9500bbcec
6
+ metadata.gz: 7ef3dacda31025b0d12def2faa3b14a75b43ab114a0389489a7962bc0e1c53542402991e0809198192ba8c28cc77c0bfd56c7533f79ca05a5cc6fe0406c65b94
7
+ data.tar.gz: 30460a520bfd15c0f131ec19091415976834fb0b876bc4c362148bc604f34a71827b39e76e33f00d1f07d03f8e4148e1e480191829e007ad4daad26dad74a73c
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ *.sqlite3
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ -c
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ rvm:
2
+ - 2.0.0
3
+ branches:
4
+ only:
5
+ - master
6
+ script:
7
+ - "bundle exec rspec"
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # ActiveRecord::HasCount
1
+ # ActiveRecord::HasCount [![Build Status](https://travis-ci.org/k0kubun/activerecord-has_count.png?branch=master)](https://travis-ci.org/k0kubun/activerecord-has_count)
2
2
 
3
3
  N+1 count query killer for ActiveRecord
4
4
  ActiveRecord::HasCount allows you to cache count of associated records by eager loading
@@ -20,7 +20,7 @@ gem 'activerecord-has_count'
20
20
 
21
21
  ## Usage
22
22
 
23
- ### Add count\_preloadable scope
23
+ ### Add has\_count scope
24
24
  First, call `has_count` with an association whose count you want to preload
25
25
 
26
26
  ```rb
@@ -63,7 +63,7 @@ end
63
63
 
64
64
  ## Contributing
65
65
 
66
- 1. Fork it ( https://github.com/k0kubun/has_count/fork )
66
+ 1. Fork it ( https://github.com/k0kubun/activerecord-has_count/fork )
67
67
  2. Create your feature branch (`git checkout -b my-new-feature`)
68
68
  3. Commit your changes (`git commit -am 'Add some feature'`)
69
69
  4. Push to the branch (`git push origin my-new-feature`)
@@ -1,6 +1,6 @@
1
1
  lib = File.expand_path('../lib', __FILE__)
2
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
- require 'active_record/has_count/version'
3
+ require 'activerecord-has_count/version'
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "activerecord-has_count"
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
17
17
  spec.require_paths = ["lib"]
18
18
 
19
19
  spec.required_ruby_version = ">= 1.9.2"
20
- spec.add_runtime_dependency "activerecord", ">= 3.0"
20
+ spec.add_runtime_dependency "activerecord", ">= 3.2.0"
21
21
  spec.add_development_dependency "rspec", "~> 3.0.0"
22
22
  spec.add_development_dependency "factory_girl", "~> 4.2.0"
23
23
  spec.add_development_dependency "sqlite3"
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem_dir = File.expand_path("../../", __FILE__)
4
+
5
+ gem "activerecord", "~> 4.0"
6
+ gem "activerecord-import"
7
+ gem "activerecord-has_count", path: gem_dir
8
+ gem "pry"
9
+ gem "mysql2"
@@ -0,0 +1,16 @@
1
+ # N+1 COUNT query benchmarks
2
+
3
+ ## Preparation
4
+
5
+ ```bash
6
+ $ git clone git@github.com:k0kubun/activerecord-has_count
7
+ $ cd activerecord-has_count
8
+ $ mysql -uroot -e"create database bench"
9
+ $ bundle install
10
+ ```
11
+
12
+ ## Run
13
+
14
+ ```bash
15
+ $ bundle exec ruby benchmark.rb
16
+ ```
@@ -0,0 +1,60 @@
1
+ require "benchmark"
2
+ require "active_record"
3
+ require "activerecord-import"
4
+ require "activerecord-has_count"
5
+
6
+ spec_dir = File.expand_path("../../spec", __FILE__)
7
+ Dir[File.join(spec_dir, "models/*.rb")].each { |f| require f }
8
+
9
+ database_yml = File.join(spec_dir, "database.yml")
10
+ ActiveRecord::Base.configurations["bench"] = YAML.load_file(database_yml)["bench"]
11
+ ActiveRecord::Base.establish_connection :bench
12
+
13
+ ActiveRecord::Schema.define do
14
+ create_table :tweets, force: true do |t|
15
+ t.column :created_at, :datetime
16
+ t.column :updated_at, :datetime
17
+ end
18
+
19
+ create_table :replies, force: true do |t|
20
+ t.column :tweet_id, :integer
21
+ t.column :created_at, :datetime
22
+ t.column :updated_at, :datetime
23
+ end
24
+ end
25
+
26
+ [Tweet, Reply].each(&:delete_all)
27
+
28
+ TWEET_COUNT = 5
29
+ REPLY_COUNT = 10000
30
+
31
+ TWEET_COUNT.times do
32
+ tweet = Tweet.create
33
+
34
+ replies = REPLY_COUNT.times.map do
35
+ Reply.new(tweet: tweet)
36
+ end
37
+ Reply.import(replies, validate: false)
38
+ end
39
+
40
+ Benchmark.bmbm do |bench|
41
+ bench.report("COUNT association") do
42
+ tweets = Tweet.first(TWEET_COUNT)
43
+
44
+ tweets.each { |t| t.replies.count }
45
+ end
46
+
47
+ bench.report("LEFT JOIN") do
48
+ tweets = Tweet.joins('LEFT JOIN replies ON tweets.id = replies.tweet_id').
49
+ select('tweets.*, COUNT(replies.id) AS replies_count').
50
+ group('tweets.id').first(TWEET_COUNT)
51
+
52
+ tweets.each { |t| t.replies_count }
53
+ end
54
+
55
+ bench.report("size of preloaded association") do
56
+ tweets = Tweet.preload(:replies).first(TWEET_COUNT)
57
+
58
+ tweets.each { |t| t.replies.size }
59
+ end
60
+ end
@@ -1,20 +1,7 @@
1
1
  module ActiveRecord
2
2
  module Associations
3
- module ClassMethods
4
- module HasCount
5
- private
6
-
7
- def has_count(name, scope = nil, options = {}, &extension)
8
- name_with_count = :"#{name}_count"
9
-
10
- reflection = Builder::HasCount.build(self, name_with_count, scope, options, &extension)
11
- Reflection.add_reflection(self, name_with_count, reflection)
12
- end
13
- end
14
- end
15
-
16
3
  class HasCount < SingularAssociation
17
- # Not preloaded behaviour of count_preloadable association
4
+ # Not preloaded behaviour of has_count association
18
5
  # When this method is called, it will be N+1 query
19
6
  def load_target
20
7
  count_target = name_without_count.to_sym
@@ -0,0 +1,23 @@
1
+ module ActiveRecord
2
+ module HasCount
3
+ module Model
4
+ def self.included(model)
5
+ model.singleton_class.class_eval do
6
+ include ClassMethods
7
+ end
8
+ end
9
+
10
+ module ClassMethods
11
+ private
12
+
13
+ def has_count(name, scope = nil, options = {}, &extension)
14
+ name_with_count = :"#{name}_count"
15
+
16
+ reflection = ActiveRecord::Associations::Builder::HasCount.
17
+ build(self, name_with_count, scope, options, &extension)
18
+ ActiveRecord::Reflection.add_reflection(self, name_with_count, reflection)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,5 +1,5 @@
1
1
  module ActiveRecord
2
2
  module HasCount
3
- VERSION = "0.0.1"
3
+ VERSION = "0.0.2"
4
4
  end
5
5
  end
@@ -1,15 +1,15 @@
1
+ require "active_record"
2
+ require "active_support/lazy_load_hooks"
3
+
1
4
  require "active_record/associations/has_count"
2
5
  require "active_record/associations/builder/has_count"
3
6
  require "active_record/associations/preloader/has_count"
4
7
  require "active_record/associations/join_dependency/has_count"
5
8
  require "active_record/reflection/has_count"
6
9
 
7
- module ActiveRecord
8
- module Associations
9
- module ClassMethods
10
- include HasCount
11
- end
12
- end
10
+ ActiveSupport.on_load(:active_record) do
11
+ require "activerecord-has_count/model"
12
+ ActiveRecord::Base.send(:include, ActiveRecord::HasCount::Model)
13
13
  end
14
14
 
15
15
  module ActiveRecord
data/spec/database.yml ADDED
@@ -0,0 +1,13 @@
1
+ test:
2
+ adapter: sqlite3
3
+ database: test.sqlite3
4
+ pool: 5
5
+ timeout: 5000
6
+ bench:
7
+ adapter: mysql2
8
+ encoding: utf8
9
+ database: bench
10
+ pool: 5
11
+ username: root
12
+ password:
13
+ socket: /tmp/mysql.sock
@@ -0,0 +1,28 @@
1
+ require "spec_helper"
2
+
3
+ describe "#eager_load" do
4
+ describe "has_count association" do
5
+ let(:tweets_count) { 3 }
6
+ let(:tweets) do
7
+ tweets_count.times.map do
8
+ FactoryGirl.create(:tweet)
9
+ end
10
+ end
11
+
12
+ before do
13
+ tweets.each_with_index do |tweet, index|
14
+ index.times do
15
+ FactoryGirl.create(:reply, tweet: tweet)
16
+ end
17
+ end
18
+ end
19
+
20
+ it "raises ActiveRecord::EagerLoadHasCountError" do
21
+ # Currently this is not supported because I can't come up with how to implement.
22
+ # I'm waiting for your contribution.
23
+ expect {
24
+ Tweet.all.eager_load(:replies_count).map(&:replies_count)
25
+ }.to raise_error(ActiveRecord::EagerLoadHasCountError)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,57 @@
1
+ require "spec_helper"
2
+
3
+ describe "#includes" do
4
+ describe "builtin associations" do
5
+ let(:replies_count) { 3 }
6
+ let!(:tweet) { FactoryGirl.create(:tweet) }
7
+ before do
8
+ replies_count.times do
9
+ FactoryGirl.create(:reply, tweet: tweet)
10
+ end
11
+ end
12
+
13
+ context "given has_many association" do
14
+ it "works as usual" do
15
+ tweet = Tweet.includes(:replies).first
16
+ expect(tweet.replies.count).to eq(replies_count)
17
+ end
18
+ end
19
+
20
+ context "given belongs_to association" do
21
+ let!(:reply) { FactoryGirl.create(:reply, tweet: tweet) }
22
+
23
+ it "works as usual" do
24
+ included_reply = Reply.includes(:tweet).find(reply.id)
25
+ expect(included_reply.tweet).to eq(tweet)
26
+ end
27
+ end
28
+ end
29
+
30
+ describe "has_count association" do
31
+ let(:tweets_count) { 3 }
32
+ let(:tweets) do
33
+ tweets_count.times.map do
34
+ FactoryGirl.create(:tweet)
35
+ end
36
+ end
37
+
38
+ before do
39
+ tweets.each_with_index do |tweet, index|
40
+ index.times do
41
+ FactoryGirl.create(:reply, tweet: tweet)
42
+ end
43
+ end
44
+ end
45
+
46
+ it "does not execute N+1 queries by preload" do
47
+ expect_query_counts(1 + tweets_count) { Tweet.all.map(&:replies_count) }
48
+ expect_query_counts(2) { Tweet.all.includes(:replies_count).map(&:replies_count) }
49
+ end
50
+
51
+ it "counts properly" do
52
+ expected = Tweet.all.map { |t| t.replies.count }
53
+ expect(Tweet.all.map(&:replies_count)).to eq(expected)
54
+ expect(Tweet.all.includes(:replies_count).map(&:replies_count)).to eq(expected)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,3 @@
1
+ class Reply < ActiveRecord::Base
2
+ belongs_to :tweet
3
+ end
@@ -0,0 +1,4 @@
1
+ class Tweet < ActiveRecord::Base
2
+ has_many :replies
3
+ has_count :replies
4
+ end
@@ -0,0 +1,57 @@
1
+ require "spec_helper"
2
+
3
+ describe "#preload" do
4
+ describe "builtin associations" do
5
+ let(:replies_count) { 3 }
6
+ let!(:tweet) { FactoryGirl.create(:tweet) }
7
+ before do
8
+ replies_count.times do
9
+ FactoryGirl.create(:reply, tweet: tweet)
10
+ end
11
+ end
12
+
13
+ context "given has_many association" do
14
+ it "works as usual" do
15
+ tweet = Tweet.preload(:replies).first
16
+ expect(tweet.replies.count).to eq(replies_count)
17
+ end
18
+ end
19
+
20
+ context "given belongs_to association" do
21
+ let!(:reply) { FactoryGirl.create(:reply, tweet: tweet) }
22
+
23
+ it "works as usual" do
24
+ preloaded_reply = Reply.preload(:tweet).find(reply.id)
25
+ expect(preloaded_reply.tweet).to eq(tweet)
26
+ end
27
+ end
28
+ end
29
+
30
+ describe "has_count association" do
31
+ let(:tweets_count) { 3 }
32
+ let(:tweets) do
33
+ tweets_count.times.map do
34
+ FactoryGirl.create(:tweet)
35
+ end
36
+ end
37
+
38
+ before do
39
+ tweets.each_with_index do |tweet, index|
40
+ index.times do
41
+ FactoryGirl.create(:reply, tweet: tweet)
42
+ end
43
+ end
44
+ end
45
+
46
+ it "does not execute N+1 queries by preload" do
47
+ expect_query_counts(1 + tweets_count) { Tweet.all.map(&:replies_count) }
48
+ expect_query_counts(2) { Tweet.all.preload(:replies_count).map(&:replies_count) }
49
+ end
50
+
51
+ it "counts properly" do
52
+ expected = Tweet.all.map { |t| t.replies.count }
53
+ expect(Tweet.all.map(&:replies_count)).to eq(expected)
54
+ expect(Tweet.all.preload(:replies_count).map(&:replies_count)).to eq(expected)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,39 @@
1
+ require "pathname"
2
+ require "pry"
3
+ require "active_record"
4
+ require "activerecord-has_count"
5
+
6
+ spec_dir = Pathname.new(File.dirname(__FILE__))
7
+ Dir[File.join(spec_dir, "models/*.rb")].each { |f| require f }
8
+
9
+ require "factory_girl"
10
+ Dir[File.join(spec_dir, "support/*.rb")].each { |f| require f }
11
+
12
+ database_yml = File.join(spec_dir, "database.yml")
13
+ ActiveRecord::Base.configurations["test"] = YAML.load_file(database_yml)["test"]
14
+ ActiveRecord::Base.establish_connection :test
15
+
16
+ ActiveRecord::Schema.define do
17
+ create_table :tweets, force: true do |t|
18
+ t.column :user_id, :integer
19
+ t.column :created_at, :datetime
20
+ t.column :updated_at, :datetime
21
+ end
22
+
23
+ create_table :replies, force: true do |t|
24
+ t.column :tweet_id, :integer
25
+ t.column :created_at, :datetime
26
+ t.column :updated_at, :datetime
27
+ end
28
+ end
29
+
30
+ RSpec.configure do |config|
31
+ ALL_MODELS = [
32
+ Tweet,
33
+ Reply,
34
+ ].freeze
35
+
36
+ config.after do
37
+ ALL_MODELS.each(&:delete_all)
38
+ end
39
+ end
@@ -0,0 +1,54 @@
1
+ # Copied from:
2
+ # https://github.com/rails/rails/blob/9bb76261d39b59e7e229c80d052ca91a65ff17be/activerecord/test/cases/test_case.rb#L40-L52
3
+ def expect_query_counts(num = 1, options = {})
4
+ ignore_none = options.fetch(:ignore_none) { num == :any }
5
+ SQLCounter.clear_log
6
+ x = yield
7
+
8
+ the_log = ignore_none ? SQLCounter.log_all : SQLCounter.log
9
+ expect(the_log.size).to eq(num)
10
+
11
+ x
12
+ end
13
+
14
+ class SQLCounter
15
+ class << self
16
+ attr_accessor :ignored_sql, :log, :log_all
17
+ def clear_log; self.log = []; self.log_all = []; end
18
+ end
19
+
20
+ self.clear_log
21
+
22
+ self.ignored_sql = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/]
23
+
24
+ # FIXME: this needs to be refactored so specific database can add their own
25
+ # ignored SQL, or better yet, use a different notification for the queries
26
+ # instead examining the SQL content.
27
+ oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im]
28
+ mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i]
29
+ postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i]
30
+ sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im]
31
+
32
+ [oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql|
33
+ ignored_sql.concat db_ignored_sql
34
+ end
35
+
36
+ attr_reader :ignore
37
+
38
+ def initialize(ignore = Regexp.union(self.class.ignored_sql))
39
+ @ignore = ignore
40
+ end
41
+
42
+ def call(name, start, finish, message_id, values)
43
+ sql = values[:sql]
44
+
45
+ # FIXME: this seems bad. we should probably have a better way to indicate
46
+ # the query was cached
47
+ return if 'CACHE' == values[:name]
48
+
49
+ self.class.log_all << sql
50
+ self.class.log << sql unless ignore =~ sql
51
+ end
52
+ end
53
+
54
+ ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new)
@@ -0,0 +1,4 @@
1
+ FactoryGirl.define do
2
+ factory :tweet
3
+ factory :reply
4
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-has_count
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Takashi Kokubun
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-09-13 00:00:00.000000000 Z
11
+ date: 2014-09-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '>='
18
18
  - !ruby/object:Gem::Version
19
- version: '3.0'
19
+ version: 3.2.0
20
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: '3.0'
26
+ version: 3.2.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rspec
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -116,18 +116,33 @@ extensions: []
116
116
  extra_rdoc_files: []
117
117
  files:
118
118
  - .gitignore
119
+ - .rspec
120
+ - .travis.yml
119
121
  - Gemfile
120
122
  - LICENSE.txt
121
123
  - README.md
122
124
  - Rakefile
123
125
  - activerecord-has_count.gemspec
126
+ - benchmarks/Gemfile
127
+ - benchmarks/README.md
128
+ - benchmarks/benchmark.rb
124
129
  - lib/active_record/associations/builder/has_count.rb
125
130
  - lib/active_record/associations/has_count.rb
126
131
  - lib/active_record/associations/join_dependency/has_count.rb
127
132
  - lib/active_record/associations/preloader/has_count.rb
128
- - lib/active_record/has_count/version.rb
129
133
  - lib/active_record/reflection/has_count.rb
130
134
  - lib/activerecord-has_count.rb
135
+ - lib/activerecord-has_count/model.rb
136
+ - lib/activerecord-has_count/version.rb
137
+ - spec/database.yml
138
+ - spec/eager_load_spec.rb
139
+ - spec/includes_spec.rb
140
+ - spec/models/reply.rb
141
+ - spec/models/tweet.rb
142
+ - spec/preload_spec.rb
143
+ - spec/spec_helper.rb
144
+ - spec/support/assert_queries.rb
145
+ - spec/support/factories.rb
131
146
  homepage: https://github.com/k0kubun/activerecord-has_count
132
147
  licenses:
133
148
  - MIT
@@ -148,9 +163,18 @@ required_rubygems_version: !ruby/object:Gem::Requirement
148
163
  version: '0'
149
164
  requirements: []
150
165
  rubyforge_project:
151
- rubygems_version: 2.0.3
166
+ rubygems_version: 2.0.14
152
167
  signing_key:
153
168
  specification_version: 4
154
169
  summary: N+1 count query killer for ActiveRecord
155
- test_files: []
170
+ test_files:
171
+ - spec/database.yml
172
+ - spec/eager_load_spec.rb
173
+ - spec/includes_spec.rb
174
+ - spec/models/reply.rb
175
+ - spec/models/tweet.rb
176
+ - spec/preload_spec.rb
177
+ - spec/spec_helper.rb
178
+ - spec/support/assert_queries.rb
179
+ - spec/support/factories.rb
156
180
  has_rdoc: