counter_culture 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. data/.document +5 -0
  2. data/.rspec +1 -0
  3. data/.rvmrc +1 -0
  4. data/Gemfile +22 -0
  5. data/Gemfile.lock +109 -0
  6. data/LICENSE.txt +20 -0
  7. data/README.md +22 -0
  8. data/Rakefile +50 -0
  9. data/VERSION +1 -0
  10. data/counter_culture.gemspec +124 -0
  11. data/lib/counter_culture.rb +260 -0
  12. data/spec/counter_culture_spec.rb +232 -0
  13. data/spec/models/company.rb +3 -0
  14. data/spec/models/industry.rb +2 -0
  15. data/spec/models/product.rb +2 -0
  16. data/spec/models/review.rb +14 -0
  17. data/spec/models/user.rb +3 -0
  18. data/spec/rails_app/.gitignore +15 -0
  19. data/spec/rails_app/Gemfile +38 -0
  20. data/spec/rails_app/README.rdoc +261 -0
  21. data/spec/rails_app/Rakefile +7 -0
  22. data/spec/rails_app/app/assets/images/rails.png +0 -0
  23. data/spec/rails_app/app/assets/javascripts/application.js +15 -0
  24. data/spec/rails_app/app/assets/stylesheets/application.css +13 -0
  25. data/spec/rails_app/app/controllers/application_controller.rb +3 -0
  26. data/spec/rails_app/app/helpers/application_helper.rb +2 -0
  27. data/spec/rails_app/app/mailers/.gitkeep +0 -0
  28. data/spec/rails_app/app/models/.gitkeep +0 -0
  29. data/spec/rails_app/app/views/layouts/application.html.erb +14 -0
  30. data/spec/rails_app/config/application.rb +59 -0
  31. data/spec/rails_app/config/boot.rb +6 -0
  32. data/spec/rails_app/config/database.yml +25 -0
  33. data/spec/rails_app/config/environment.rb +5 -0
  34. data/spec/rails_app/config/environments/development.rb +37 -0
  35. data/spec/rails_app/config/environments/production.rb +67 -0
  36. data/spec/rails_app/config/environments/test.rb +37 -0
  37. data/spec/rails_app/config/initializers/backtrace_silencers.rb +7 -0
  38. data/spec/rails_app/config/initializers/inflections.rb +15 -0
  39. data/spec/rails_app/config/initializers/mime_types.rb +5 -0
  40. data/spec/rails_app/config/initializers/secret_token.rb +7 -0
  41. data/spec/rails_app/config/initializers/session_store.rb +8 -0
  42. data/spec/rails_app/config/initializers/wrap_parameters.rb +14 -0
  43. data/spec/rails_app/config/locales/en.yml +5 -0
  44. data/spec/rails_app/config/routes.rb +58 -0
  45. data/spec/rails_app/config.ru +4 -0
  46. data/spec/rails_app/db/seeds.rb +7 -0
  47. data/spec/rails_app/lib/assets/.gitkeep +0 -0
  48. data/spec/rails_app/lib/tasks/.gitkeep +0 -0
  49. data/spec/rails_app/log/.gitkeep +0 -0
  50. data/spec/rails_app/public/404.html +26 -0
  51. data/spec/rails_app/public/422.html +26 -0
  52. data/spec/rails_app/public/500.html +25 -0
  53. data/spec/rails_app/public/favicon.ico +0 -0
  54. data/spec/rails_app/public/index.html +241 -0
  55. data/spec/rails_app/public/robots.txt +5 -0
  56. data/spec/rails_app/script/rails +6 -0
  57. data/spec/rails_app/test/fixtures/.gitkeep +0 -0
  58. data/spec/rails_app/test/functional/.gitkeep +0 -0
  59. data/spec/rails_app/test/integration/.gitkeep +0 -0
  60. data/spec/rails_app/test/performance/browsing_test.rb +12 -0
  61. data/spec/rails_app/test/test_helper.rb +13 -0
  62. data/spec/rails_app/test/unit/.gitkeep +0 -0
  63. data/spec/rails_app/vendor/assets/javascripts/.gitkeep +0 -0
  64. data/spec/rails_app/vendor/assets/stylesheets/.gitkeep +0 -0
  65. data/spec/rails_app/vendor/plugins/.gitkeep +0 -0
  66. data/spec/schema.rb +52 -0
  67. data/spec/spec_helper.rb +19 -0
  68. metadata +232 -0
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm 1.9.3@counter_culture
data/Gemfile ADDED
@@ -0,0 +1,22 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ # Add dependencies to develop your gem here.
7
+ # Include everything needed to run rake, tests, features, etc.
8
+ group :development, :test do
9
+ gem "rails"
10
+ gem "rspec", "~> 2.10.0"
11
+ gem "after_commit_action"
12
+ end
13
+
14
+ group :development do
15
+ gem "rdoc", "~> 3.12"
16
+ gem "bundler", "~> 1.2.0.pre"
17
+ gem "jeweler", "~> 1.8.3"
18
+ end
19
+
20
+ group :test do
21
+ gem "sqlite3"
22
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,109 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ actionmailer (3.2.3)
5
+ actionpack (= 3.2.3)
6
+ mail (~> 2.4.4)
7
+ actionpack (3.2.3)
8
+ activemodel (= 3.2.3)
9
+ activesupport (= 3.2.3)
10
+ builder (~> 3.0.0)
11
+ erubis (~> 2.7.0)
12
+ journey (~> 1.0.1)
13
+ rack (~> 1.4.0)
14
+ rack-cache (~> 1.2)
15
+ rack-test (~> 0.6.1)
16
+ sprockets (~> 2.1.2)
17
+ activemodel (3.2.3)
18
+ activesupport (= 3.2.3)
19
+ builder (~> 3.0.0)
20
+ activerecord (3.2.3)
21
+ activemodel (= 3.2.3)
22
+ activesupport (= 3.2.3)
23
+ arel (~> 3.0.2)
24
+ tzinfo (~> 0.3.29)
25
+ activeresource (3.2.3)
26
+ activemodel (= 3.2.3)
27
+ activesupport (= 3.2.3)
28
+ activesupport (3.2.3)
29
+ i18n (~> 0.6)
30
+ multi_json (~> 1.0)
31
+ after_commit_action (0.1.3)
32
+ activerecord (>= 3.0.0)
33
+ arel (3.0.2)
34
+ builder (3.0.0)
35
+ diff-lcs (1.1.3)
36
+ erubis (2.7.0)
37
+ git (1.2.5)
38
+ hike (1.2.1)
39
+ i18n (0.6.0)
40
+ jeweler (1.8.3)
41
+ bundler (~> 1.0)
42
+ git (>= 1.2.5)
43
+ rake
44
+ rdoc
45
+ journey (1.0.3)
46
+ json (1.7.3)
47
+ mail (2.4.4)
48
+ i18n (>= 0.4.0)
49
+ mime-types (~> 1.16)
50
+ treetop (~> 1.4.8)
51
+ mime-types (1.18)
52
+ multi_json (1.3.5)
53
+ polyglot (0.3.3)
54
+ rack (1.4.1)
55
+ rack-cache (1.2)
56
+ rack (>= 0.4)
57
+ rack-ssl (1.3.2)
58
+ rack
59
+ rack-test (0.6.1)
60
+ rack (>= 1.0)
61
+ rails (3.2.3)
62
+ actionmailer (= 3.2.3)
63
+ actionpack (= 3.2.3)
64
+ activerecord (= 3.2.3)
65
+ activeresource (= 3.2.3)
66
+ activesupport (= 3.2.3)
67
+ bundler (~> 1.0)
68
+ railties (= 3.2.3)
69
+ railties (3.2.3)
70
+ actionpack (= 3.2.3)
71
+ activesupport (= 3.2.3)
72
+ rack-ssl (~> 1.3.2)
73
+ rake (>= 0.8.7)
74
+ rdoc (~> 3.4)
75
+ thor (~> 0.14.6)
76
+ rake (0.9.2.2)
77
+ rdoc (3.12)
78
+ json (~> 1.4)
79
+ rspec (2.10.0)
80
+ rspec-core (~> 2.10.0)
81
+ rspec-expectations (~> 2.10.0)
82
+ rspec-mocks (~> 2.10.0)
83
+ rspec-core (2.10.1)
84
+ rspec-expectations (2.10.0)
85
+ diff-lcs (~> 1.1.3)
86
+ rspec-mocks (2.10.1)
87
+ sprockets (2.1.3)
88
+ hike (~> 1.2)
89
+ rack (~> 1.0)
90
+ tilt (~> 1.1, != 1.3.0)
91
+ sqlite3 (1.3.6)
92
+ thor (0.14.6)
93
+ tilt (1.3.3)
94
+ treetop (1.4.10)
95
+ polyglot
96
+ polyglot (>= 0.3.1)
97
+ tzinfo (0.3.33)
98
+
99
+ PLATFORMS
100
+ ruby
101
+
102
+ DEPENDENCIES
103
+ after_commit_action
104
+ bundler (~> 1.2.0.pre)
105
+ jeweler (~> 1.8.3)
106
+ rails
107
+ rdoc (~> 3.12)
108
+ rspec (~> 2.10.0)
109
+ sqlite3
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Magnus von Koeller
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,22 @@
1
+ # counter_culture
2
+
3
+ Turbo-charged counter caches for your Rails app. Huge improvements over the Rails standard counter caches:
4
+
5
+ * Updates counter cache when values change, not just when creating and destroying
6
+ * Supports counter caches through multiple levels of relations
7
+ * Supports dynamic column names, making it possible to split up the counter cache for different types of objects
8
+ * Executes counter updates after the commit, avoiding [deadlocks](http://mina.naguib.ca/blog/2010/11/22/postgresql-foreign-key-deadlocks.html)
9
+
10
+ ## Contributing to counter_culture
11
+
12
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
13
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
14
+ * Fork the project.
15
+ * Start a feature/bugfix branch.
16
+ * Commit and push until you are happy with your contribution.
17
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
18
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
19
+
20
+ ## Copyright
21
+
22
+ Copyright (c) 2012 BestVendor. See LICENSE.txt for further details.
data/Rakefile ADDED
@@ -0,0 +1,50 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "counter_culture"
18
+ gem.homepage = "http://github.com/bestvendor/counter_culture"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{Turbo-charged counter caches for your Rails app.}
21
+ gem.description = %Q{counter_culture provides turbo-charged counter caches that are kept up-to-date not just on create and destroy, that support multiple levels of indirection through relationships, allow dynamic column names and that avoid deadlocks by updating in the after_commit callback.}
22
+ gem.email = "magnus@vonkoeller.de"
23
+ gem.authors = ["Magnus von Koeller"]
24
+
25
+ gem.add_dependency 'after_commit_action', '~> 0.1.3'
26
+ end
27
+ Jeweler::RubygemsDotOrgTasks.new
28
+
29
+ require 'rspec/core'
30
+ require 'rspec/core/rake_task'
31
+ RSpec::Core::RakeTask.new(:spec) do |spec|
32
+ spec.pattern = FileList['spec/**/*_spec.rb']
33
+ end
34
+
35
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
36
+ spec.pattern = 'spec/**/*_spec.rb'
37
+ spec.rcov = true
38
+ end
39
+
40
+ task :default => :spec
41
+
42
+ require 'rdoc/task'
43
+ Rake::RDocTask.new do |rdoc|
44
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
45
+
46
+ rdoc.rdoc_dir = 'rdoc'
47
+ rdoc.title = "counter_culture #{version}"
48
+ rdoc.rdoc_files.include('README*')
49
+ rdoc.rdoc_files.include('lib/**/*.rb')
50
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.3
@@ -0,0 +1,124 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "counter_culture"
8
+ s.version = "0.1.3"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Magnus von Koeller"]
12
+ s.date = "2012-05-30"
13
+ s.description = "counter_culture provides turbo-charged counter caches that are kept up-to-date not just on create and destroy, that support multiple levels of indirection through relationships, allow dynamic column names and that avoid deadlocks by updating in the after_commit callback."
14
+ s.email = "magnus@vonkoeller.de"
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.md"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".rspec",
22
+ ".rvmrc",
23
+ "Gemfile",
24
+ "Gemfile.lock",
25
+ "LICENSE.txt",
26
+ "README.md",
27
+ "Rakefile",
28
+ "VERSION",
29
+ "counter_culture.gemspec",
30
+ "lib/counter_culture.rb",
31
+ "spec/counter_culture_spec.rb",
32
+ "spec/models/company.rb",
33
+ "spec/models/industry.rb",
34
+ "spec/models/product.rb",
35
+ "spec/models/review.rb",
36
+ "spec/models/user.rb",
37
+ "spec/rails_app/.gitignore",
38
+ "spec/rails_app/Gemfile",
39
+ "spec/rails_app/README.rdoc",
40
+ "spec/rails_app/Rakefile",
41
+ "spec/rails_app/app/assets/images/rails.png",
42
+ "spec/rails_app/app/assets/javascripts/application.js",
43
+ "spec/rails_app/app/assets/stylesheets/application.css",
44
+ "spec/rails_app/app/controllers/application_controller.rb",
45
+ "spec/rails_app/app/helpers/application_helper.rb",
46
+ "spec/rails_app/app/mailers/.gitkeep",
47
+ "spec/rails_app/app/models/.gitkeep",
48
+ "spec/rails_app/app/views/layouts/application.html.erb",
49
+ "spec/rails_app/config.ru",
50
+ "spec/rails_app/config/application.rb",
51
+ "spec/rails_app/config/boot.rb",
52
+ "spec/rails_app/config/database.yml",
53
+ "spec/rails_app/config/environment.rb",
54
+ "spec/rails_app/config/environments/development.rb",
55
+ "spec/rails_app/config/environments/production.rb",
56
+ "spec/rails_app/config/environments/test.rb",
57
+ "spec/rails_app/config/initializers/backtrace_silencers.rb",
58
+ "spec/rails_app/config/initializers/inflections.rb",
59
+ "spec/rails_app/config/initializers/mime_types.rb",
60
+ "spec/rails_app/config/initializers/secret_token.rb",
61
+ "spec/rails_app/config/initializers/session_store.rb",
62
+ "spec/rails_app/config/initializers/wrap_parameters.rb",
63
+ "spec/rails_app/config/locales/en.yml",
64
+ "spec/rails_app/config/routes.rb",
65
+ "spec/rails_app/db/seeds.rb",
66
+ "spec/rails_app/lib/assets/.gitkeep",
67
+ "spec/rails_app/lib/tasks/.gitkeep",
68
+ "spec/rails_app/log/.gitkeep",
69
+ "spec/rails_app/public/404.html",
70
+ "spec/rails_app/public/422.html",
71
+ "spec/rails_app/public/500.html",
72
+ "spec/rails_app/public/favicon.ico",
73
+ "spec/rails_app/public/index.html",
74
+ "spec/rails_app/public/robots.txt",
75
+ "spec/rails_app/script/rails",
76
+ "spec/rails_app/test/fixtures/.gitkeep",
77
+ "spec/rails_app/test/functional/.gitkeep",
78
+ "spec/rails_app/test/integration/.gitkeep",
79
+ "spec/rails_app/test/performance/browsing_test.rb",
80
+ "spec/rails_app/test/test_helper.rb",
81
+ "spec/rails_app/test/unit/.gitkeep",
82
+ "spec/rails_app/vendor/assets/javascripts/.gitkeep",
83
+ "spec/rails_app/vendor/assets/stylesheets/.gitkeep",
84
+ "spec/rails_app/vendor/plugins/.gitkeep",
85
+ "spec/schema.rb",
86
+ "spec/spec_helper.rb"
87
+ ]
88
+ s.homepage = "http://github.com/bestvendor/counter_culture"
89
+ s.licenses = ["MIT"]
90
+ s.require_paths = ["lib"]
91
+ s.rubygems_version = "1.8.21"
92
+ s.summary = "Turbo-charged counter caches for your Rails app."
93
+
94
+ if s.respond_to? :specification_version then
95
+ s.specification_version = 3
96
+
97
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
98
+ s.add_development_dependency(%q<rails>, [">= 0"])
99
+ s.add_development_dependency(%q<rspec>, ["~> 2.10.0"])
100
+ s.add_development_dependency(%q<after_commit_action>, [">= 0"])
101
+ s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
102
+ s.add_development_dependency(%q<bundler>, ["~> 1.2.0.pre"])
103
+ s.add_development_dependency(%q<jeweler>, ["~> 1.8.3"])
104
+ s.add_runtime_dependency(%q<after_commit_action>, ["~> 0.1.3"])
105
+ else
106
+ s.add_dependency(%q<rails>, [">= 0"])
107
+ s.add_dependency(%q<rspec>, ["~> 2.10.0"])
108
+ s.add_dependency(%q<after_commit_action>, [">= 0"])
109
+ s.add_dependency(%q<rdoc>, ["~> 3.12"])
110
+ s.add_dependency(%q<bundler>, ["~> 1.2.0.pre"])
111
+ s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
112
+ s.add_dependency(%q<after_commit_action>, ["~> 0.1.3"])
113
+ end
114
+ else
115
+ s.add_dependency(%q<rails>, [">= 0"])
116
+ s.add_dependency(%q<rspec>, ["~> 2.10.0"])
117
+ s.add_dependency(%q<after_commit_action>, [">= 0"])
118
+ s.add_dependency(%q<rdoc>, ["~> 3.12"])
119
+ s.add_dependency(%q<bundler>, ["~> 1.2.0.pre"])
120
+ s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
121
+ s.add_dependency(%q<after_commit_action>, ["~> 0.1.3"])
122
+ end
123
+ end
124
+
@@ -0,0 +1,260 @@
1
+ require 'after_commit_action'
2
+
3
+ module CounterCulture
4
+
5
+ module ActiveRecord
6
+
7
+ def self.included(base)
8
+ # also add class methods to ActiveRecord::Base
9
+ base.extend ClassMethods
10
+ end
11
+
12
+ module ClassMethods
13
+ # this holds all configuration data
14
+ attr_reader :after_commit_counter_cache
15
+
16
+ # called to configure counter caches
17
+ def counter_culture(relation, options = {})
18
+ unless @after_commit_counter_cache
19
+ # initialize callbacks only once
20
+ after_create :_update_counts_after_create
21
+ after_destroy :_update_counts_after_destroy
22
+ after_update :_update_counts_after_update
23
+
24
+ # we keep a list of all counter caches we must maintain
25
+ @after_commit_counter_cache = []
26
+ end
27
+
28
+ # add the current information to our list
29
+ @after_commit_counter_cache<< {
30
+ :relation => relation.is_a?(Enumerable) ? relation : [relation],
31
+ :counter_cache_name => (options[:column_name] || "#{name.tableize}_count"),
32
+ :column_names => options[:column_names],
33
+ :foreign_key_values => options[:foreign_key_values]
34
+ }
35
+ end
36
+
37
+ # checks all of the declared counter caches on this class for correctnes based
38
+ # on original data; if the counter cache is incorrect, sets it to the correct
39
+ # count
40
+ #
41
+ # returns: a list of fixed record as an array of hashes of the form:
42
+ # { :entity => which model the count was fixed on,
43
+ # :id => the id of the model that had the incorrect count,
44
+ # :what => which column contained the incorrect count,
45
+ # :wrong => the previously saved, incorrect count,
46
+ # :right => the newly fixed, correct count }
47
+ #
48
+ def counter_culture_fix_counts
49
+ fixed = []
50
+ @after_commit_counter_cache.map do |hash|
51
+ # which class does this relation ultimately point to? that's where we have to start
52
+ klass = relation_klass(hash[:relation])
53
+
54
+ # we are only interested in the id and the count of related objects (that's this class itself)
55
+ query = klass.select("#{klass.table_name}.id, COUNT(#{self.table_name}.id) AS count")
56
+ query = query.group("#{klass.table_name}.id")
57
+
58
+ # if we're provided a custom set of column names with conditions, use them; just use the
59
+ # column name otherwise
60
+ raise "Must provide :column_names option for relation #{hash[:relation].inspect} when :counter_cache_name is a Proc" if hash[:counter_cache_name].is_a?(Proc) && !hash[:column_names]
61
+ column_names = hash[:column_names] || {nil => hash[:counter_cache_name]}
62
+ raise ":column_names must be a Hash of conditions and column names" unless column_names.is_a?(Hash)
63
+
64
+ # iterate over all the possible counter cache column names
65
+ column_names.each do |where, column_name|
66
+ # if there are additional conditions, add them here
67
+ counts = query.where(where)
68
+
69
+ # we need to work our way back from the end-point of the relation to this class itself;
70
+ # make a list of arrays pointing to the second-to-last, third-to-last, etc.
71
+ reverse_relation = []
72
+ (1..hash[:relation].length).to_a.reverse.each {|i| reverse_relation<< hash[:relation][0,i] }
73
+
74
+ # we need to join together tables until we get back to the table this class itself
75
+ # lives in
76
+ reverse_relation.each do |cur_relation|
77
+ reflect = relation_reflect(cur_relation)
78
+ counts = counts.joins("JOIN #{reflect.active_record.table_name} ON #{reflect.table_name}.id = #{reflect.active_record.table_name}.#{reflect.foreign_key}")
79
+ end
80
+ # and then we collect the counts in an id => count hash
81
+ counts = counts.inject({}){|memo, model| memo[model.id] = model.count.to_i; memo}
82
+
83
+ # now that we know what the correct counts are, we need to iterate over all instances
84
+ # and check whether the count is correct; if not, we correct it
85
+ klass.find_each do |model|
86
+ if model.send(column_name) != counts[model.id].to_i
87
+ # keep track of what we fixed, e.g. for a notification email
88
+ fixed<< {
89
+ :entity => klass.name,
90
+ :id => model.id,
91
+ :what => column_name,
92
+ :wrong => model.send(column_name),
93
+ :right => counts[model.id]
94
+ }
95
+ # use update_all because it's faster and because a fixed counter-cache shouldn't
96
+ # update the timestamp
97
+ klass.update_all "#{column_name} = #{counts[model.id].to_i}", "id = #{model.id}"
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ return fixed
104
+ end
105
+
106
+ private
107
+ # gets the reflect object on the given relation
108
+ #
109
+ # relation: a symbol or array of symbols; specifies the relation
110
+ # that has the counter cache column
111
+ def relation_reflect(relation)
112
+ relation = relation.is_a?(Enumerable) ? relation.dup : [relation]
113
+
114
+ # go from one relation to the next until we hit the last reflect object
115
+ klass = self
116
+ while relation.size > 0
117
+ cur_relation = relation.shift
118
+ reflect = klass.reflect_on_association(cur_relation)
119
+ raise "No relation #{cur_relation} on #{klass.name}" if reflect.nil?
120
+ klass = reflect.klass
121
+ end
122
+
123
+ return reflect
124
+ end
125
+
126
+ # gets the class of the given relation
127
+ #
128
+ # relation: a symbol or array of symbols; specifies the relation
129
+ # that has the counter cache column
130
+ def relation_klass(relation)
131
+ relation_reflect(relation).klass
132
+ end
133
+
134
+ # gets the foreign key name of the given relation
135
+ #
136
+ # relation: a symbol or array of symbols; specifies the relation
137
+ # that has the counter cache column
138
+ def relation_foreign_key(relation)
139
+ relation_reflect(relation).foreign_key
140
+ end
141
+
142
+ # gets the foreign key name of the relation. will look at the first
143
+ # level only -- i.e., if passed an array will consider only its
144
+ # first element
145
+ #
146
+ # relation: a symbol or array of symbols; specifies the relation
147
+ # that has the counter cache column
148
+ def first_level_relation_foreign_key(relation)
149
+ relation = relation.first if relation.is_a?(Enumerable)
150
+ relation_reflect(relation).foreign_key
151
+ end
152
+
153
+ end
154
+
155
+ private
156
+ # called by after_create callback
157
+ def _update_counts_after_create
158
+ self.class.after_commit_counter_cache.each do |hash|
159
+ # increment counter cache
160
+ change_counter_cache(true, hash)
161
+ end
162
+ end
163
+
164
+ # called by after_destroy callback
165
+ def _update_counts_after_destroy
166
+ self.class.after_commit_counter_cache.each do |hash|
167
+ # decrement counter cache
168
+ change_counter_cache(false, hash)
169
+ end
170
+ end
171
+
172
+ # called by after_update callback
173
+ def _update_counts_after_update
174
+ self.class.after_commit_counter_cache.each do |hash|
175
+ # only update counter caches if the foreign key changed
176
+ if send("#{first_level_relation_foreign_key(hash[:relation])}_changed?")
177
+ # increment the counter cache of the new value
178
+ change_counter_cache(true, hash)
179
+ # decrement the counter cache of the old value
180
+ change_counter_cache(false, hash, true)
181
+ end
182
+ end
183
+ end
184
+
185
+ # increments or decrements a counter cache
186
+ #
187
+ # increment: true to increment, false to decrement
188
+ # hash:
189
+ # :relation => which relation to increment the count on,
190
+ # :counter_cache_name => the column name of the counter cache
191
+ # was: whether to get the current value or the old value of the
192
+ # first part of the relation
193
+ def change_counter_cache(increment, hash, was = false)
194
+ # default to the current foreign key value
195
+ id_to_change = foreign_key_value(hash[:relation], was)
196
+ # allow overwriting of foreign key value by the caller
197
+ id_to_change = hash[:foreign_key_values].call(id_to_change) if hash[:foreign_key_values]
198
+ if id_to_change
199
+ execute_after_commit do
200
+ # increment or decrement?
201
+ method = increment ? :increment_counter : :decrement_counter
202
+
203
+ # figure out what the column name is
204
+ if hash[:counter_cache_name].is_a? Proc
205
+ # dynamic column name -- call the Proc
206
+ counter_cache_name = hash[:counter_cache_name].call(self)
207
+ else
208
+ # static column name
209
+ counter_cache_name = hash[:counter_cache_name]
210
+ end
211
+
212
+ # do it!
213
+ relation_klass(hash[:relation]).send(method, counter_cache_name, id_to_change)
214
+ end
215
+ end
216
+ end
217
+
218
+ # gets the value of the foreign key on the given relation
219
+ #
220
+ # relation: a symbol or array of symbols; specifies the relation
221
+ # that has the counter cache column
222
+ # was: whether to get the current or past value from ActiveRecord;
223
+ # pass true to get the past value, false or nothing to get the
224
+ # current value
225
+ def foreign_key_value(relation, was = false)
226
+ relation = relation.is_a?(Enumerable) ? relation.dup : [relation]
227
+ if was
228
+ first = relation.shift
229
+ foreign_key_value = send("#{relation_foreign_key(first)}_was")
230
+ value = relation_klass(first).find(foreign_key_value) if foreign_key_value
231
+ else
232
+ value = self
233
+ end
234
+ while !value.nil? && relation.size > 0
235
+ value = value.send(relation.shift)
236
+ end
237
+ return value.try(:id)
238
+ end
239
+
240
+ def relation_klass(relation)
241
+ self.class.send :relation_klass, relation
242
+ end
243
+
244
+ def relation_reflect(relation)
245
+ self.class.send :relation_reflect, relation
246
+ end
247
+
248
+ def relation_foreign_key(relation)
249
+ self.class.send :relation_foreign_key, relation
250
+ end
251
+
252
+ def first_level_relation_foreign_key(relation)
253
+ self.class.send :first_level_relation_foreign_key, relation
254
+ end
255
+
256
+ end
257
+
258
+ # extend ActiveRecord with our own code here
259
+ ::ActiveRecord::Base.send :include, ActiveRecord
260
+ end