├── Gemfile ├── .gitignore ├── lib ├── custom_counter_cache.rb └── custom_counter_cache │ ├── version.rb │ └── model.rb ├── test ├── gemfiles │ ├── rails-4.0 │ ├── rails-4.1 │ ├── rails-4.2 │ ├── rails-5.0 │ └── rails-5.1 ├── test_helper.rb └── counter_test.rb ├── .travis.yml ├── Rakefile ├── custom_counter_cache.gemspec ├── LICENSE ├── README.rdoc └── Gemfile.lock /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .rbenv-version 2 | .ruby-version 3 | .rvmrc 4 | *.gem 5 | -------------------------------------------------------------------------------- /lib/custom_counter_cache.rb: -------------------------------------------------------------------------------- 1 | module CustomCounterCache 2 | require 'custom_counter_cache/model' 3 | end 4 | -------------------------------------------------------------------------------- /lib/custom_counter_cache/version.rb: -------------------------------------------------------------------------------- 1 | module CustomCounterCache 2 | 3 | VERSION = '0.2.3' 4 | 5 | end 6 | -------------------------------------------------------------------------------- /test/gemfiles/rails-4.0: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec path: "./../.." 4 | 5 | gem 'rails', '~> 4.0.0' 6 | -------------------------------------------------------------------------------- /test/gemfiles/rails-4.1: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec path: "./../.." 4 | 5 | gem 'rails', '~> 4.1.0' 6 | -------------------------------------------------------------------------------- /test/gemfiles/rails-4.2: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec path: "./../.." 4 | 5 | gem 'rails', '~> 4.2.0' 6 | -------------------------------------------------------------------------------- /test/gemfiles/rails-5.0: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec path: "./../.." 4 | 5 | gem 'rails', '~> 5.0.0' 6 | -------------------------------------------------------------------------------- /test/gemfiles/rails-5.1: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec path: "./../.." 4 | 5 | gem 'rails', '~> 5.1.0' 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2.5 4 | - 2.3.3 5 | - 2.4.0 6 | - 2.4.1 7 | gemfile: 8 | - test/gemfiles/rails-4.0 9 | - test/gemfiles/rails-4.1 10 | - test/gemfiles/rails-4.2 11 | - test/gemfiles/rails-5.0 12 | - test/gemfiles/rails-5.1 13 | matrix: 14 | exclude: 15 | - rvm: 2.4.0 16 | gemfile: test/gemfiles/rails-4.0 17 | - rvm: 2.4.0 18 | gemfile: test/gemfiles/rails-4.1 19 | - rvm: 2.4.1 20 | gemfile: test/gemfiles/rails-4.0 21 | - rvm: 2.4.1 22 | gemfile: test/gemfiles/rails-4.1 23 | 24 | notifications: 25 | recipients: 26 | email: 27 | - cedric@howe.net 28 | on_success: change 29 | on_failure: always 30 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $LOAD_PATH << File.dirname(__FILE__) 3 | require 'rake/testtask' 4 | require 'lib/custom_counter_cache/version' 5 | 6 | namespace :gem do 7 | 8 | desc 'Run tests.' 9 | Rake::TestTask.new(:test) do |test| 10 | test.libs << 'lib' << 'test' 11 | test.pattern = 'test/**/*_test.rb' 12 | test.verbose = true 13 | end 14 | 15 | desc 'Build gem.' 16 | task build: :test do 17 | system "gem build custom_counter_cache.gemspec" 18 | end 19 | 20 | desc 'Build, tag and push gem.' 21 | task release: :build do 22 | # tag and push 23 | system "git tag v#{CustomCounterCache::VERSION}" 24 | system "git push origin --tags" 25 | # push gem 26 | system "gem push custom_counter_cache-#{CustomCounterCache::VERSION}.gem" 27 | end 28 | 29 | end 30 | 31 | task default: 'gem:test' 32 | -------------------------------------------------------------------------------- /custom_counter_cache.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $LOAD_PATH << File.dirname(__FILE__) + '/lib' 3 | require 'custom_counter_cache/version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'custom_counter_cache' 7 | s.version = CustomCounterCache::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.license = 'MIT' 10 | s.authors = 'Cedric Howe' 11 | s.email = 'cedric@howe.net' 12 | s.homepage = 'http://github.com/cedric/custom_counter_cache/' 13 | s.summary = 'Custom counter_cache functionality that supports conditions and multiple models.' 14 | s.description = '' 15 | s.require_paths = ['lib'] 16 | s.files = Dir['lib/**/*.rb'] 17 | s.required_rubygems_version = '>= 1.3.6' 18 | s.add_dependency('rails', '>= 4.0') 19 | s.add_development_dependency('sqlite3', '>= 1.3.3') 20 | s.test_files = Dir['test/**/*.rb'] 21 | s.rubyforge_project = 'custom_counter_cache' 22 | end 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2011-2017 Cedric Howe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | {Gem Version}[http://badge.fury.io/rb/custom_counter_cache] 2 | {Build Status}[https://travis-ci.org/cedric/custom_counter_cache] 3 | 4 | == Custom Counter Cache 5 | 6 | This is a simple approach to creating a custom counter cache in Rails that can be used across multiple models. 7 | 8 | === Installation 9 | 10 | Add the following to your Gemfile: 11 | 12 | gem 'custom_counter_cache' 13 | 14 | === Example 15 | 16 | == Class with counter cache 17 | 18 | This is the block that will be used to calculate the value for the counter cache. It will be called by other models through their association via an after_save or after_destroy callback. 19 | 20 | include CustomCounterCache::Model 21 | define_counter_cache :articles_count do |user| 22 | user.articles.where(state: 'published').count 23 | end 24 | 25 | == Class with callbacks 26 | 27 | This will define the after_create, after_update and after_destroy callbacks. An :if option can be provided to limit when these callbacks get triggered. 28 | 29 | include CustomCounterCache::Model 30 | update_counter_cache :user, :articles_count, if: -> (article) { article.state_changed? } 31 | 32 | These callbacks can be added to any number of models that might need to change the counter cache. 33 | 34 | == Counter Cache 35 | 36 | To store the counter cache you need to create a column for the model with the counter cache (example: articles_count). 37 | 38 | If you would like to store all of your counter caches in a single table, you can use this migration: 39 | 40 | create_table :counters do |t| 41 | t.references :countable, polymorphic: true 42 | t.string :key, null: false 43 | t.integer :value, null: false, default: 0 44 | t.timestamps 45 | end 46 | add_index :counters, [ :countable_id, :countable_type, :key ], unique: true 47 | 48 | Here is the example model to go with: 49 | 50 | class Counter < ActiveRecord::Base 51 | belongs_to :countable, polymorphic: true 52 | validates :countable, presence: true 53 | end 54 | 55 | If you would like to store your counter cache in an existing table, you can use this migration: 56 | 57 | def change 58 | add_column :users, :articles_count, :integer, default: 0, null: false 59 | end 60 | 61 | To backfill your counters, run something like this either in a migration or in the console: 62 | 63 | User.select(:id).find_each(batch_size: 100) { |u| u.update_articles_count } 64 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'minitest/autorun' 3 | require 'sqlite3' 4 | require 'action_view' 5 | require 'active_record' 6 | require 'custom_counter_cache' 7 | 8 | ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') 9 | 10 | ActiveRecord::Schema.define(version: 1) do 11 | create_table :users do |t| 12 | end 13 | 14 | create_table :articles do |t| 15 | t.belongs_to :user 16 | t.string :state, default: 'unpublished' 17 | end 18 | 19 | create_table :comments do |t| 20 | t.belongs_to :user 21 | t.references :commentable, polymorphic: true 22 | t.string :state, default: "unpublished" 23 | end 24 | 25 | create_table :counters do |t| 26 | t.references :countable, polymorphic: true 27 | t.string :key, null: false 28 | t.integer :value, null: false, default: 0 29 | end 30 | add_index :counters, [ :countable_id, :countable_type, :key ], unique: true 31 | 32 | create_table :boxes do |t| 33 | t.integer :green_balls_count, default: 0 34 | t.integer :lifetime_balls_count, default: 0 35 | t.integer :destroyed_balls_count, default: 0 36 | end 37 | 38 | create_table :balls do |t| 39 | t.belongs_to :box 40 | t.string :color, default: 'red' 41 | end 42 | end 43 | 44 | class ApplicationRecord < ActiveRecord::Base 45 | self.abstract_class = true 46 | include CustomCounterCache::Model 47 | end 48 | 49 | class User < ApplicationRecord 50 | has_many :articles, dependent: :destroy 51 | define_counter_cache :published_count do |user| 52 | user.articles.where(articles: { state: 'published' }).count 53 | end 54 | end 55 | 56 | class Article < ApplicationRecord 57 | belongs_to :user 58 | update_counter_cache :user, :published_count, if: Proc.new { |article| article.state_changed? } 59 | has_many :comments, as: :commentable, dependent: :destroy 60 | define_counter_cache :comments_count do |article| 61 | article.comments.where(state: "published").count 62 | end 63 | end 64 | 65 | class Comment < ApplicationRecord 66 | belongs_to :commentable, polymorphic: true 67 | update_counter_cache :commentable, :comments_count, if: Proc.new { |comment| comment.state_changed? } 68 | end 69 | 70 | class Counter < ApplicationRecord 71 | belongs_to :countable, polymorphic: true 72 | end 73 | 74 | class Box < ApplicationRecord 75 | has_many :balls 76 | define_counter_cache :green_balls_count do |box| 77 | box.balls.green.count 78 | end 79 | define_counter_cache :lifetime_balls_count do |box| 80 | box.lifetime_balls_count + 1 81 | end 82 | define_counter_cache :destroyed_balls_count do |box| 83 | box.destroyed_balls_count + 1 84 | end 85 | end 86 | 87 | class Ball < ApplicationRecord 88 | belongs_to :box 89 | scope :green, lambda { where(color: 'green') } 90 | update_counter_cache :box, :green_balls_count, if: Proc.new { |ball| ball.color_changed? } 91 | update_counter_cache :box, :lifetime_balls_count, except: [:update, :destroy] 92 | update_counter_cache :box, :destroyed_balls_count, only: [:destroy] 93 | end 94 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | custom_counter_cache (0.2.3) 5 | rails (>= 4.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | actioncable (5.2.0) 11 | actionpack (= 5.2.0) 12 | nio4r (~> 2.0) 13 | websocket-driver (>= 0.6.1) 14 | actionmailer (5.2.0) 15 | actionpack (= 5.2.0) 16 | actionview (= 5.2.0) 17 | activejob (= 5.2.0) 18 | mail (~> 2.5, >= 2.5.4) 19 | rails-dom-testing (~> 2.0) 20 | actionpack (5.2.0) 21 | actionview (= 5.2.0) 22 | activesupport (= 5.2.0) 23 | rack (~> 2.0) 24 | rack-test (>= 0.6.3) 25 | rails-dom-testing (~> 2.0) 26 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 27 | actionview (5.2.0) 28 | activesupport (= 5.2.0) 29 | builder (~> 3.1) 30 | erubi (~> 1.4) 31 | rails-dom-testing (~> 2.0) 32 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 33 | activejob (5.2.0) 34 | activesupport (= 5.2.0) 35 | globalid (>= 0.3.6) 36 | activemodel (5.2.0) 37 | activesupport (= 5.2.0) 38 | activerecord (5.2.0) 39 | activemodel (= 5.2.0) 40 | activesupport (= 5.2.0) 41 | arel (>= 9.0) 42 | activestorage (5.2.0) 43 | actionpack (= 5.2.0) 44 | activerecord (= 5.2.0) 45 | marcel (~> 0.3.1) 46 | activesupport (5.2.0) 47 | concurrent-ruby (~> 1.0, >= 1.0.2) 48 | i18n (>= 0.7, < 2) 49 | minitest (~> 5.1) 50 | tzinfo (~> 1.1) 51 | arel (9.0.0) 52 | builder (3.2.3) 53 | concurrent-ruby (1.0.5) 54 | crass (1.0.4) 55 | erubi (1.7.1) 56 | globalid (0.4.1) 57 | activesupport (>= 4.2.0) 58 | i18n (1.0.1) 59 | concurrent-ruby (~> 1.0) 60 | loofah (2.2.2) 61 | crass (~> 1.0.2) 62 | nokogiri (>= 1.5.9) 63 | mail (2.7.0) 64 | mini_mime (>= 0.1.1) 65 | marcel (0.3.2) 66 | mimemagic (~> 0.3.2) 67 | method_source (0.9.0) 68 | mimemagic (0.3.2) 69 | mini_mime (1.0.0) 70 | mini_portile2 (2.3.0) 71 | minitest (5.11.3) 72 | nio4r (2.3.1) 73 | nokogiri (1.8.2) 74 | mini_portile2 (~> 2.3.0) 75 | rack (2.0.5) 76 | rack-test (1.0.0) 77 | rack (>= 1.0, < 3) 78 | rails (5.2.0) 79 | actioncable (= 5.2.0) 80 | actionmailer (= 5.2.0) 81 | actionpack (= 5.2.0) 82 | actionview (= 5.2.0) 83 | activejob (= 5.2.0) 84 | activemodel (= 5.2.0) 85 | activerecord (= 5.2.0) 86 | activestorage (= 5.2.0) 87 | activesupport (= 5.2.0) 88 | bundler (>= 1.3.0) 89 | railties (= 5.2.0) 90 | sprockets-rails (>= 2.0.0) 91 | rails-dom-testing (2.0.3) 92 | activesupport (>= 4.2.0) 93 | nokogiri (>= 1.6) 94 | rails-html-sanitizer (1.0.4) 95 | loofah (~> 2.2, >= 2.2.2) 96 | railties (5.2.0) 97 | actionpack (= 5.2.0) 98 | activesupport (= 5.2.0) 99 | method_source 100 | rake (>= 0.8.7) 101 | thor (>= 0.18.1, < 2.0) 102 | rake (12.3.1) 103 | sprockets (3.7.1) 104 | concurrent-ruby (~> 1.0) 105 | rack (> 1, < 3) 106 | sprockets-rails (3.2.1) 107 | actionpack (>= 4.0) 108 | activesupport (>= 4.0) 109 | sprockets (>= 3.0.0) 110 | sqlite3 (1.3.13) 111 | thor (0.20.0) 112 | thread_safe (0.3.6) 113 | tzinfo (1.2.5) 114 | thread_safe (~> 0.1) 115 | websocket-driver (0.7.0) 116 | websocket-extensions (>= 0.1.0) 117 | websocket-extensions (0.1.3) 118 | 119 | PLATFORMS 120 | ruby 121 | 122 | DEPENDENCIES 123 | custom_counter_cache! 124 | sqlite3 (>= 1.3.3) 125 | 126 | BUNDLED WITH 127 | 1.16.0 128 | -------------------------------------------------------------------------------- /test/counter_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper') 2 | 3 | class CounterTest < MiniTest::Unit::TestCase 4 | 5 | def setup 6 | @user = User.create 7 | @box = Box.create 8 | Counter.destroy_all 9 | end 10 | 11 | def test_default_counter_value 12 | assert_equal 0, @user.published_count 13 | assert_equal 0, @box.green_balls_count 14 | end 15 | 16 | def test_create_and_destroy_counter 17 | @user.articles.create(state: 'published') 18 | assert_equal 1, Counter.count 19 | @user.destroy 20 | assert_equal 0, Counter.count 21 | end 22 | 23 | def test_create_and_destroy_polymorphic_association_counter 24 | @article = @user.articles.create(state: "published") 25 | assert_equal 0, @article.comments.size 26 | @comment = @article.comments.create(state: "published") 27 | assert_equal 1, @article.comments.size 28 | @article.destroy 29 | assert_equal 0, @article.comments.size 30 | end 31 | 32 | def test_increment_and_decrement_counter_with_conditions 33 | @article = @user.articles.create(state: 'unpublished') 34 | assert_equal 0, @user.published_count 35 | @article.update_attribute :state, 'published' 36 | assert_equal 1, @user.published_count 37 | 3.times { |i| @user.articles.create(state: 'published') } 38 | assert_equal 4, @user.published_count 39 | @user.articles.each {|a| a.update_attributes(state: 'unpublished') } 40 | assert_equal 0, @user.published_count 41 | end 42 | 43 | def test_increment_and_decrement_polymorphic_counter_with_conditions 44 | @article = @user.articles.create(state: "published") 45 | @comment = @article.comments.create(state: "unpublished") 46 | assert_equal 0, @article.comments_count 47 | @comment.update_attribute :state, "published" 48 | assert_equal 1, @article.comments_count 49 | 3.times { |i| @article.comments.create(state: "published") } 50 | assert_equal 4, @article.comments_count 51 | @article.comments.each { |c| c.update_attributes(state: "unpublished") } 52 | assert_equal 0, @article.comments_count 53 | end 54 | 55 | def test_increment_and_decrement_counter_with_conditions_on_model_with_counter_column 56 | @ball = @box.balls.create(color: 'red') 57 | assert_equal 0, @box.reload.green_balls_count 58 | @ball.update_attribute :color, 'green' 59 | assert_equal 1, @box.reload.green_balls_count 60 | 3.times { |i| @box.balls.create(color: 'green') } 61 | assert_equal 4, @box.reload.green_balls_count 62 | @box.balls.each {|b| b.update_attributes(color: 'red') } 63 | assert_equal 0, @box.reload.green_balls_count 64 | end 65 | 66 | # Test that an eager loaded 67 | def test_eager_loading_with_no_counter 68 | @article = @user.articles.create(state: 'unpublished') 69 | user = User.includes(:counters).first 70 | assert_equal 0, user.published_count 71 | 72 | end 73 | 74 | def test_eager_loading_with_counter 75 | @article = @user.articles.create(state: 'published') 76 | @user = User.includes(:counters).find(@user.id) 77 | assert_equal 1, @user.published_count 78 | end 79 | 80 | def test_except_option 81 | @ball = @box.balls.create 82 | assert_equal 1, @box.reload.lifetime_balls_count 83 | @ball.update(color: 'green') 84 | assert_equal 1, @box.reload.lifetime_balls_count 85 | @ball.destroy 86 | assert_equal 1, @box.reload.lifetime_balls_count 87 | end 88 | 89 | def test_only_option 90 | @ball = @box.balls.create 91 | assert_equal 0, @box.reload.destroyed_balls_count 92 | @ball.update(color: 'green') 93 | assert_equal 0, @box.reload.destroyed_balls_count 94 | @ball.destroy 95 | assert_equal 1, @box.reload.destroyed_balls_count 96 | end 97 | 98 | end 99 | -------------------------------------------------------------------------------- /lib/custom_counter_cache/model.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/concern' 2 | 3 | module CustomCounterCache::Model 4 | extend ActiveSupport::Concern 5 | 6 | module ClassMethods 7 | def define_counter_cache(cache_column, &block) 8 | return unless table_exists? 9 | 10 | # counter accessors 11 | unless column_names.include?(cache_column.to_s) 12 | has_many :counters, as: :countable, dependent: :destroy 13 | define_method "#{cache_column}" do 14 | # check if the counter is loaded 15 | if counters.loaded? && counter = counters.detect{|c| c.key == cache_column.to_s } 16 | counter.value 17 | else 18 | counters.find_by_key(cache_column.to_s).try(:value).to_i 19 | end 20 | end 21 | define_method "#{cache_column}=" do |count| 22 | if ( counter = counters.find_by_key(cache_column.to_s) ) 23 | counter.update_attribute :value, count.to_i 24 | else 25 | counters.create key: cache_column.to_s, value: count.to_i 26 | end 27 | end 28 | end 29 | 30 | # counter update method 31 | define_method "update_#{cache_column}" do 32 | if self.class.column_names.include?(cache_column.to_s) 33 | update_attribute cache_column, block.call(self) 34 | else 35 | send "#{cache_column}=", block.call(self) 36 | end 37 | end 38 | 39 | rescue StandardError => e 40 | # Support Heroku's database-less assets:precompile pre-deploy step: 41 | raise e unless ENV['DATABASE_URL'].to_s.include?('//user:pass@127.0.0.1/') 42 | end 43 | 44 | def update_counter_cache(association, cache_column, options = {}) 45 | return unless table_exists? 46 | 47 | association = association.to_sym 48 | cache_column = cache_column.to_sym 49 | method_name = "callback_#{association}_#{cache_column}".to_sym 50 | reflection = reflect_on_association(association) 51 | foreign_key = reflection.try(:foreign_key) || reflection.association_foreign_key 52 | 53 | # define callback 54 | define_method method_name do 55 | # update old association 56 | rails_5_1_or_newer = ActiveModel.version >= Gem::Version.new('5.1.0') 57 | target_key = reflection.options[:polymorphic] ? "#{association}_type" : foreign_key 58 | target_changed = rails_5_1_or_newer ? send("saved_change_to_#{target_key}?") : send("#{target_key}_changed?") 59 | if target_changed 60 | old_id = rails_5_1_or_newer ? send("#{target_key}_before_last_save") : send("#{target_key}_was") 61 | klass = if reflection.options[:polymorphic] 62 | ( old_id || send("#{association}_type") ).constantize 63 | else 64 | reflection.klass 65 | end 66 | if ( old_id && record = klass.find_by(id: old_id) ) 67 | record.send("update_#{cache_column}") 68 | end 69 | end 70 | # update new association 71 | if ( record = send(association) ) 72 | record.send("update_#{cache_column}") 73 | end 74 | end 75 | 76 | skip_callback = Proc.new { |callback, opts| 77 | (opts[:except].present? && opts[:except].include?(callback)) || 78 | (opts[:only].present? && !opts[:only].include?(callback)) 79 | } 80 | 81 | # set callbacks 82 | after_create method_name, options unless skip_callback.call(:create, options) 83 | after_update method_name, options unless skip_callback.call(:update, options) 84 | after_destroy method_name, options unless skip_callback.call(:destroy, options) 85 | 86 | rescue StandardError => e 87 | # Support Heroku's database-less assets:precompile pre-deploy step: 88 | raise e unless ENV['DATABASE_URL'].to_s.include?('//user:pass@127.0.0.1/') 89 | end 90 | end 91 | end 92 | --------------------------------------------------------------------------------