├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── conditional_counter_cache.gemspec └── lib ├── conditional_counter_cache.rb └── conditional_counter_cache ├── active_record.rb ├── belongs_to.rb ├── reflection.rb └── version.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.3 2 | - Support Rails 4.2.0 3 | 4 | ## 0.0.2 5 | - Fix non counter-cached belongs_to association 6 | 7 | ## 0.0.1 8 | - 1st release 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in conditional_counter_cache.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Ryo Nakamura 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ConditionalCounterCache 2 | Give condition when to increment/decrement counter cache. 3 | 4 | ## Usage 5 | Customize condition via `:counter_cache` option: 6 | 7 | ```ruby 8 | class Tagging < ActiveRecord::Base 9 | belongs_to :item 10 | belongs_to :tag, counter_cache: { condition: -> { !item.private? } } 11 | end 12 | ``` 13 | 14 | Other examples: 15 | 16 | ```ruby 17 | belongs_to :tag, counter_cache: true 18 | belongs_to :tag, counter_cache: "items_count" 19 | belongs_to :tag, counter_cache: { condition: -> { !item.private? } } 20 | belongs_to :tag, counter_cache: { condition: -> :your_favorite_method_name } 21 | belongs_to :tag, counter_cache: { column_name: "items_count" } 22 | belongs_to :tag, counter_cache: { column_name: "items_count", condition: -> { !item.private? } } 23 | ``` 24 | 25 | ## See also 26 | * [Active Record Associations — Ruby on Rails Guides](http://guides.rubyonrails.org/association_basics.html) 27 | * [#23 Counter Cache Column - RailsCasts](http://railscasts.com/episodes/23-counter-cache-column) 28 | * [magnusvk/counter_culture](https://github.com/magnusvk/counter_culture) 29 | * [Counterculture - Wikipedia, the free encyclopedia](http://en.wikipedia.org/wiki/Counterculture) 30 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | -------------------------------------------------------------------------------- /conditional_counter_cache.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'conditional_counter_cache/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "conditional_counter_cache" 8 | spec.version = ConditionalCounterCache::VERSION 9 | spec.authors = ["Ryo Nakamura"] 10 | spec.email = ["r7kamura@gmail.com"] 11 | spec.summary = "Give condition when to increment/decrement counter cache." 12 | spec.homepage = "https://github.com/r7kamura/conditional_counter_cache" 13 | spec.license = "MIT" 14 | 15 | spec.files = `git ls-files -z`.split("\x0") 16 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 17 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 18 | spec.require_paths = ["lib"] 19 | 20 | spec.add_dependency "activerecord", ">= 4.0.0" 21 | spec.add_dependency "activesupport" 22 | spec.add_development_dependency "bundler", "~> 1.7" 23 | spec.add_development_dependency "rails" 24 | spec.add_development_dependency "rake", "~> 10.0" 25 | spec.add_development_dependency "rspec", "3.0.2" 26 | spec.add_development_dependency "rspec-rails" 27 | end 28 | -------------------------------------------------------------------------------- /lib/conditional_counter_cache.rb: -------------------------------------------------------------------------------- 1 | require "active_record" 2 | 3 | require "conditional_counter_cache/belongs_to" 4 | require "conditional_counter_cache/reflection" 5 | require "conditional_counter_cache/version" 6 | require "conditional_counter_cache/active_record" 7 | -------------------------------------------------------------------------------- /lib/conditional_counter_cache/active_record.rb: -------------------------------------------------------------------------------- 1 | ActiveSupport.on_load(:active_record) do 2 | ActiveRecord::Reflection::AssociationReflection.prepend(ConditionalCounterCache::Reflection) 3 | ActiveRecord::Associations::Builder::BelongsTo.singleton_class.prepend(ConditionalCounterCache::BelongsTo) 4 | end 5 | -------------------------------------------------------------------------------- /lib/conditional_counter_cache/belongs_to.rb: -------------------------------------------------------------------------------- 1 | module ConditionalCounterCache 2 | module BelongsTo 3 | def add_counter_cache_methods(mixin) 4 | return if mixin.method_defined? :belongs_to_counter_cache_after_update 5 | 6 | mixin.class_eval do 7 | unless ActiveRecord::VERSION::MAJOR >= 4 && ActiveRecord::VERSION::MINOR >= 2 8 | def belongs_to_counter_cache_after_create(reflection) 9 | if record = send(reflection.name) 10 | return unless reflection.has_countable?(self) 11 | cache_column = reflection.counter_cache_column 12 | record.class.increment_counter(cache_column, record.id) 13 | @_after_create_counter_called = true 14 | end 15 | end 16 | 17 | def belongs_to_counter_cache_before_destroy(reflection) 18 | foreign_key = reflection.foreign_key.to_sym 19 | unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key 20 | record = send reflection.name 21 | if record && !self.destroyed? 22 | return unless reflection.has_countable?(self) 23 | cache_column = reflection.counter_cache_column 24 | record.class.decrement_counter(cache_column, record.id) 25 | end 26 | end 27 | end 28 | end 29 | 30 | def belongs_to_counter_cache_after_update(reflection) 31 | foreign_key = reflection.foreign_key 32 | cache_column = reflection.counter_cache_column 33 | 34 | if (@_after_create_counter_called ||= false) 35 | @_after_create_counter_called = false 36 | elsif attribute_changed?(foreign_key) && !new_record? && reflection.constructable? 37 | return unless reflection.has_countable?(self) 38 | model = reflection.klass 39 | foreign_key_was = attribute_was foreign_key 40 | foreign_key = attribute foreign_key 41 | 42 | if foreign_key && model.respond_to?(:increment_counter) 43 | model.increment_counter(cache_column, foreign_key) 44 | end 45 | if foreign_key_was && model.respond_to?(:decrement_counter) 46 | model.decrement_counter(cache_column, foreign_key_was) 47 | end 48 | end 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/conditional_counter_cache/reflection.rb: -------------------------------------------------------------------------------- 1 | module ConditionalCounterCache 2 | module Reflection 3 | # @note Overridden 4 | def counter_cache_column 5 | Option.new(self).column_name 6 | end 7 | 8 | # @return [true, false] 9 | def has_countable?(record) 10 | Option.new(self).condition.call(record) 11 | end 12 | 13 | # Utility wrapper of `option[:counter_cache][:condition]`. 14 | class Condition 15 | # @param [Proc, String, Symbol, nil] 16 | def initialize(value) 17 | @value = value 18 | end 19 | 20 | def call(record) 21 | case @value 22 | when Proc 23 | record.instance_exec(&@value) 24 | when nil 25 | true 26 | else 27 | record.send(@value) 28 | end 29 | end 30 | end 31 | 32 | # Utility wrapper of reflection to process `option[:counter_cache]`. 33 | class Option 34 | # @param [ActiveRecord::Reflection] 35 | def initialize(reflection) 36 | @reflection = reflection 37 | end 38 | 39 | # @return [String, nil] Specified column name, or nil meaning "use default column name". 40 | def column_name 41 | case cache 42 | when Hash 43 | cache[:column_name] 44 | when String, Symbol 45 | cache.to_s 46 | when true 47 | "#{@reflection.active_record.name.demodulize.underscore.pluralize}_count" 48 | else 49 | nil 50 | end 51 | end 52 | 53 | # @return [Condition] 54 | def condition 55 | Condition.new(raw_condition) 56 | end 57 | 58 | private 59 | 60 | def cache 61 | @reflection.options[:counter_cache] 62 | end 63 | 64 | # @return [Proc, String, Symbol, nil] 65 | def raw_condition 66 | cache[:condition] if cache.is_a?(Hash) 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/conditional_counter_cache/version.rb: -------------------------------------------------------------------------------- 1 | module ConditionalCounterCache 2 | VERSION = "0.0.3" 3 | end 4 | --------------------------------------------------------------------------------