├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── README.md ├── Rakefile ├── init.rb ├── lib ├── second_level_cache.rb └── second_level_cache │ ├── active_record.rb │ ├── active_record │ ├── base.rb │ ├── belongs_to_association.rb │ ├── fetch_by_uniq_key.rb │ ├── finder_methods.rb │ ├── has_one_association.rb │ └── persistence.rb │ ├── arel │ └── wheres.rb │ ├── config.rb │ ├── record_marshal.rb │ └── version.rb ├── second_level_cache.gemspec └── test ├── active_record ├── base_test.rb ├── belongs_to_association_test.rb ├── finder_methods_test.rb ├── model │ ├── book.rb │ ├── image.rb │ ├── post.rb │ ├── topic.rb │ └── user.rb ├── persistence_test.rb ├── polymorphic_association_test.rb ├── second_level_cache_test.rb └── test_helper.rb ├── record_marshal_test.rb ├── require_test.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle 2 | /nbproject/* 3 | /Gemfile.lock 4 | /.rvmrc 5 | /doc 6 | /pkg 7 | /tags 8 | /spec/log 9 | *.log 10 | *.gem 11 | *.sqlite3 12 | *.swp 13 | *.swo 14 | 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.0.0 4 | - 1.9.3 5 | - 1.9.2 6 | - rbx-19mode 7 | - ruby-head 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 1.2.0 2 | ----- 3 | * [clear cache after update_counters](https://github.com/csdn-dev/second_level_cache/commit/240dde81199124092e0e8ad0500c167ac146e301) 4 | 5 | 1.2.1 6 | ----- 7 | * [fix polymorphic association bug] 8 | 9 | 1.3.0 10 | ----- 11 | * [clean cache after touch] 12 | 13 | 1.3.1 14 | ----- 15 | * [clean cache after update_column/increment!/decrement!] 16 | 17 | 1.3.2 18 | ----- 19 | * [fix has one assciation issue] 20 | 21 | 1.4.0 22 | ----- 23 | * [cache has one assciation] 24 | 25 | 1.4.1 26 | ----- 27 | * [fix errors when belongs_to association return nil] 28 | 29 | 1.5.0 30 | ----- 31 | * [add cache version to quick clear cache for special model] 32 | 33 | 1.5.1 34 | ----- 35 | * [use new marshal machanism to avoid clear assocation cache manually] 36 | 37 | 1.6.0 38 | ----- 39 | * [write through cache] 40 | * [disable SecondLevelCache for spicial model] 41 | * [only cache `SELECT *` query] 42 | 43 | 1.6.1 44 | ----- 45 | * [Fix bug: undefined method `select_all_column?' for []:ActiveRecord::Relation] by sishen 46 | 47 | 1.6.2 48 | ----- 49 | * [can disable/enable fetch_by_uinq_key method] 50 | * [Fix Bug: serialized attribute columns marshal issue #11] 51 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | # Specify your gem's dependencies in second_level_cache.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SecondLevelCache 2 | 3 | [![Gem Version](https://badge.fury.io/rb/second_level_cache.png)](http://badge.fury.io/rb/second_level_cache) 4 | [![Dependency Status](https://gemnasium.com/csdn-dev/second_level_cache.png)](https://gemnasium.com/csdn-dev/second_level_cache) 5 | [![Build Status](https://travis-ci.org/csdn-dev/second_level_cache.png?branch=master)](https://travis-ci.org/csdn-dev/second_level_cache) 6 | [![Code Climate](https://codeclimate.com/github/csdn-dev/second_level_cache.png)](https://codeclimate.com/github/csdn-dev/second_level_cache) 7 | 8 | SecondLevelCache is a write-through and read-through caching library inspired by Cache Money and cache_fu, support only Rails3 and ActiveRecord. 9 | 10 | Read-Through: Queries by ID, like `current_user.articles.find(params[:id])`, will first look in cache store and then look in the database for the results of that query. If there is a cache miss, it will populate the cache. 11 | 12 | Write-Through: As objects are created, updated, and deleted, all of the caches are automatically kept up-to-date and coherent. 13 | 14 | 15 | ## Install 16 | 17 | In your gem file: 18 | 19 | ```ruby 20 | gem "second_level_cache", "~> 1.6" 21 | ``` 22 | 23 | ## Usage 24 | 25 | For example, cache User objects: 26 | 27 | ```ruby 28 | class User < ActiveRecord::Base 29 | acts_as_cached(:version => 1, :expires_in => 1.week) 30 | end 31 | ``` 32 | 33 | Then it will fetch cached object in this situations: 34 | 35 | ```ruby 36 | User.find(1) 37 | User.find_by_id(1) 38 | User.find_by_id!(1) 39 | User.find_by_id_and_name(1, "Hooopo") 40 | User.where(:status => 1).find_by_id(1) 41 | user.articles.find_by_id(1) 42 | user.articles.find(1) 43 | User.where(:status => 1).find(1) 44 | article.user 45 | ``` 46 | 47 | Cache key: 48 | 49 | ```ruby 50 | user = User.find 1 51 | user.second_level_cache_key # We will get the key looks like "slc/user/1/0" 52 | ``` 53 | 54 | Expires cache: 55 | 56 | ```ruby 57 | user = User.find(1) 58 | user.expire_second_level_cache 59 | ``` 60 | or expires cache using class method: 61 | ```ruby 62 | User.expire_second_level_cache(1) 63 | ``` 64 | 65 | Disable SecondLevelCache: 66 | 67 | ```ruby 68 | User.without_second_level_cache do 69 | user = User.find 1 70 | # ... 71 | end 72 | ``` 73 | 74 | Only `SELECT *` query will be cached: 75 | 76 | ```ruby 77 | # this query will NOT be cached 78 | User.select("id, name").find(1) 79 | ``` 80 | 81 | Notice: 82 | 83 | * SecondLevelCache cache by model name and id, so only find_one query will work. 84 | * Only equal conditions query WILL get cache; and SQL string query like `User.where("name = 'Hooopo'").find(1)` WILL NOT work. 85 | * SecondLevelCache sync cache after transaction commit: 86 | 87 | ```ruby 88 | # user and account's write_second_level_cache operation will invoke after the logger. 89 | ActiveRecord::Base.transaction do 90 | user.save 91 | account.save 92 | Rails.logger.info "info" 93 | end # <- Cache write 94 | 95 | # if you want to do something after user and account's write_second_level_cache operation, do this way: 96 | ActiveRecord::Base.transaction do 97 | user.save 98 | account.save 99 | end # <- Cache write 100 | Rails.logger.info "info" 101 | ``` 102 | 103 | ## Configure 104 | 105 | In production env, we recommend to use [Dalli](https://github.com/mperham/dalli) as Rails cache store. 106 | ```ruby 107 | config.cache_store = [:dalli_store, APP_CONFIG["memcached_host"], {:namespace => "ns", :compress => true}] 108 | ``` 109 | 110 | ## Tips: 111 | 112 | * When you want to clear only second level cache apart from other cache for example fragment cache in cache store, 113 | you can only change the `cache_key_prefix`: 114 | 115 | ```ruby 116 | SecondLevelCache.configure.cache_key_prefix = "slc1" 117 | ``` 118 | * When schema of your model changed, just change the `version` of the speical model, avoding clear all the cache. 119 | 120 | ```ruby 121 | class User < ActiveRecord::Base 122 | acts_as_cached(:version => 2, :expires_in => 1.week) 123 | end 124 | ``` 125 | 126 | * It provides a great feature, not hits db when fetching record via unique key(not primary key). 127 | 128 | ```ruby 129 | # this will fetch from cache 130 | user = User.fetch_by_uniq_key("hooopo", :nick_name) 131 | 132 | # this also fetch from cache 133 | user = User.fetch_by_uniq_key!("hooopo", :nick_name) # this will raise `ActiveRecord::RecordNotFound` Exception when nick name not exists. 134 | ``` 135 | 136 | ## Contributors 137 | 138 | * [chloerei](https://github.com/chloerei) 139 | * [reyesyang](https://github.com/reyesyang) 140 | * [hooopo](https://github.com/hooopo) 141 | * [sishen](https://github.com/sishen) 142 | 143 | ## License 144 | 145 | MIT License 146 | 147 | Permission is hereby granted, free of charge, to any person obtaining 148 | a copy of this software and associated documentation files (the 149 | "Software"), to deal in the Software without restriction, including 150 | without limitation the rights to use, copy, modify, merge, publish, 151 | distribute, sublicense, and/or sell copies of the Software, and to 152 | permit persons to whom the Software is furnished to do so, subject to 153 | the following conditions: 154 | 155 | The above copyright notice and this permission notice shall be 156 | included in all copies or substantial portions of the Software. 157 | 158 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 159 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 160 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 161 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 162 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 163 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 164 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 165 | 166 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require 'bundler/gem_tasks' 3 | require 'rake/testtask' 4 | 5 | task :default => :test 6 | 7 | Rake::TestTask.new do |t| 8 | t.libs << "lib" << "test" 9 | t.test_files = FileList['test/**/*_test.rb'] 10 | end 11 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require File.expand_path("../lib/second_level_cache", __FILE__) 3 | -------------------------------------------------------------------------------- /lib/second_level_cache.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'active_support/all' 3 | require 'second_level_cache/config' 4 | require 'second_level_cache/record_marshal' 5 | 6 | module SecondLevelCache 7 | def self.configure 8 | block_given? ? yield(Config) : Config 9 | end 10 | 11 | class << self 12 | delegate :logger, :cache_store, :cache_key_prefix, :to => Config 13 | end 14 | 15 | module Mixin 16 | extend ActiveSupport::Concern 17 | 18 | module ClassMethods 19 | attr_reader :second_level_cache_options 20 | 21 | def acts_as_cached(options = {}) 22 | @second_level_cache_enabled = true 23 | @second_level_cache_options = options 24 | @second_level_cache_options[:expires_in] ||= 1.week 25 | @second_level_cache_options[:version] ||= 0 26 | end 27 | 28 | def second_level_cache_enabled? 29 | !!@second_level_cache_enabled 30 | end 31 | 32 | def without_second_level_cache 33 | old, @second_level_cache_enabled = @second_level_cache_enabled, false 34 | 35 | yield if block_given? 36 | ensure 37 | @second_level_cache_enabled = old 38 | end 39 | 40 | def cache_store 41 | Config.cache_store 42 | end 43 | 44 | def logger 45 | Config.logger 46 | end 47 | 48 | def cache_key_prefix 49 | Config.cache_key_prefix 50 | end 51 | 52 | def cache_version 53 | second_level_cache_options[:version] 54 | end 55 | 56 | def second_level_cache_key(id) 57 | "#{cache_key_prefix}/#{name.downcase}/#{id}/#{cache_version}" 58 | end 59 | 60 | def read_second_level_cache(id) 61 | RecordMarshal.load(SecondLevelCache.cache_store.read(second_level_cache_key(id))) if self.second_level_cache_enabled? 62 | end 63 | 64 | def expire_second_level_cache(id) 65 | SecondLevelCache.cache_store.delete(second_level_cache_key(id)) if self.second_level_cache_enabled? 66 | end 67 | end 68 | 69 | def second_level_cache_key 70 | self.class.second_level_cache_key(id) 71 | end 72 | 73 | def expire_second_level_cache 74 | SecondLevelCache.cache_store.delete(second_level_cache_key) if self.class.second_level_cache_enabled? 75 | end 76 | 77 | def write_second_level_cache 78 | if self.class.second_level_cache_enabled? 79 | SecondLevelCache.cache_store.write(second_level_cache_key, RecordMarshal.dump(self), :expires_in => self.class.second_level_cache_options[:expires_in]) 80 | end 81 | end 82 | 83 | alias update_second_level_cache write_second_level_cache 84 | end 85 | end 86 | 87 | require 'second_level_cache/active_record' if defined?(ActiveRecord) 88 | -------------------------------------------------------------------------------- /lib/second_level_cache/active_record.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'second_level_cache/active_record/base' 3 | require 'second_level_cache/active_record/fetch_by_uniq_key' 4 | require 'second_level_cache/active_record/finder_methods' 5 | require 'second_level_cache/active_record/persistence' 6 | require 'second_level_cache/active_record/belongs_to_association' 7 | require 'second_level_cache/active_record/has_one_association' 8 | 9 | ActiveRecord::Base.send(:include, SecondLevelCache::Mixin) 10 | ActiveRecord::Base.send(:include, SecondLevelCache::ActiveRecord::Base) 11 | ActiveRecord::Base.send(:extend, SecondLevelCache::ActiveRecord::FetchByUniqKey) 12 | ActiveRecord::Relation.send(:include, SecondLevelCache::ActiveRecord::FinderMethods) 13 | ActiveRecord::Base.send(:include, SecondLevelCache::ActiveRecord::Persistence) 14 | ActiveRecord::Associations::BelongsToAssociation.send(:include, SecondLevelCache::ActiveRecord::Associations::BelongsToAssociation) 15 | ActiveRecord::Associations::HasOneAssociation.send(:include, SecondLevelCache::ActiveRecord::Associations::HasOneAssociation) 16 | -------------------------------------------------------------------------------- /lib/second_level_cache/active_record/base.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | module SecondLevelCache 3 | module ActiveRecord 4 | module Base 5 | extend ActiveSupport::Concern 6 | 7 | included do 8 | after_commit :expire_second_level_cache, :on => :destroy 9 | after_commit :update_second_level_cache, :on => :update 10 | after_commit :write_second_level_cache, :on => :create 11 | 12 | class << self 13 | alias_method_chain :update_counters, :cache 14 | end 15 | end 16 | 17 | 18 | module ClassMethods 19 | def update_counters_with_cache(id, counters) 20 | update_counters_without_cache(id, counters).tap do 21 | Array(id).each{|i| expire_second_level_cache(i)} 22 | end 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/second_level_cache/active_record/belongs_to_association.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | module SecondLevelCache 3 | module ActiveRecord 4 | module Associations 5 | module BelongsToAssociation 6 | extend ActiveSupport::Concern 7 | included do 8 | class_eval do 9 | alias_method_chain :find_target, :second_level_cache 10 | end 11 | end 12 | 13 | def find_target_with_second_level_cache 14 | return find_target_without_second_level_cache unless klass.second_level_cache_enabled? 15 | cache_record = klass.read_second_level_cache(second_level_cache_key) 16 | return cache_record.tap{|record| set_inverse_instance(record)} if cache_record 17 | record = find_target_without_second_level_cache 18 | 19 | record.tap do |r| 20 | set_inverse_instance(r) 21 | r.write_second_level_cache 22 | end if record 23 | end 24 | 25 | private 26 | 27 | def second_level_cache_key 28 | owner[reflection.foreign_key] 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/second_level_cache/active_record/fetch_by_uniq_key.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | module SecondLevelCache 3 | module ActiveRecord 4 | module FetchByUniqKey 5 | def fetch_by_uniq_key(value, uniq_key_name) 6 | return self.where(uniq_key_name => value).first unless self.second_level_cache_enabled? 7 | if iid = SecondLevelCache.cache_store.read(cache_uniq_key(value, uniq_key_name)) 8 | self.find_by_id(iid) 9 | else 10 | record = self.where(uniq_key_name => value).first 11 | record.tap{|record| SecondLevelCache.cache_store.write(cache_uniq_key(value, uniq_key_name), record.id)} if record 12 | end 13 | end 14 | 15 | def fetch_by_uniq_key!(value, uniq_key_name) 16 | fetch_by_uniq_key(value, uniq_key_name) || raise(::ActiveRecord::RecordNotFound) 17 | end 18 | 19 | private 20 | 21 | def cache_uniq_key(value, uniq_key_name) 22 | "uniq_key_#{self.name}_#{uniq_key_name}_#{value}" 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/second_level_cache/active_record/finder_methods.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'second_level_cache/arel/wheres' 3 | 4 | module SecondLevelCache 5 | module ActiveRecord 6 | module FinderMethods 7 | extend ActiveSupport::Concern 8 | 9 | included do 10 | class_eval do 11 | alias_method_chain :find_one, :second_level_cache 12 | alias_method_chain :find_by_attributes, :second_level_cache 13 | end 14 | end 15 | 16 | # TODO fetch multi ids 17 | # 18 | # Cacheable: 19 | # 20 | # current_user.articles.where(:status => 1).visiable.find(params[:id]) 21 | # 22 | # Uncacheable: 23 | # 24 | # Article.where("user_id = '1'").find(params[:id]) 25 | # Article.where("user_id > 1").find(params[:id]) 26 | # Article.where("articles.user_id = 1").find(prams[:id]) 27 | # Article.where("user_id = 1 AND ...").find(params[:id]) 28 | def find_one_with_second_level_cache(id) 29 | return find_one_without_second_level_cache(id) unless second_level_cache_enabled? 30 | return find_one_without_second_level_cache(id) unless select_all_column? 31 | 32 | id = id.id if ActiveRecord::Base === id 33 | if ::ActiveRecord::IdentityMap.enabled? && cachable? && record = from_identity_map(id) 34 | return record 35 | end 36 | 37 | if cachable? 38 | return record if record = @klass.read_second_level_cache(id) 39 | end 40 | 41 | if cachable_without_conditions? 42 | if record = @klass.read_second_level_cache(id) 43 | return record if where_match_with_cache?(where_values, record) 44 | end 45 | end 46 | 47 | record = find_one_without_second_level_cache(id) 48 | record.write_second_level_cache 49 | record 50 | end 51 | 52 | # TODO cache find_or_create_by_id 53 | def find_by_attributes_with_second_level_cache(match, attributes, *args) 54 | return find_by_attributes_without_second_level_cache(match, attributes, *args) unless second_level_cache_enabled? 55 | return find_by_attributes_without_second_level_cache(match, attributes, *args) unless select_all_column? 56 | 57 | conditions = Hash[attributes.map {|a| [a, args[attributes.index(a)]]}] 58 | 59 | if conditions.has_key?("id") 60 | result = wrap_bang(match.bang?) do 61 | if conditions.size == 1 62 | find_one_with_second_level_cache(conditions["id"]) 63 | else 64 | where(conditions.except("id")).find_one_with_second_level_cache(conditions["id"]) 65 | end 66 | end 67 | yield(result) if block_given? #edge rails do this bug rails3.1.0 not 68 | 69 | return result 70 | end 71 | 72 | find_by_attributes_without_second_level_cache(match, attributes, *args) 73 | end 74 | 75 | private 76 | 77 | def wrap_bang(bang) 78 | bang ? yield : (yield rescue nil) 79 | end 80 | 81 | def cachable? 82 | where_values.blank? && 83 | limit_one? && order_values.blank? && 84 | includes_values.blank? && preload_values.blank? && 85 | readonly_value.nil? && joins_values.blank? && !@klass.locking_enabled? 86 | end 87 | 88 | def cachable_without_conditions? 89 | limit_one? && order_values.blank? && 90 | includes_values.blank? && preload_values.blank? && 91 | readonly_value.nil? && joins_values.blank? && !@klass.locking_enabled? 92 | end 93 | 94 | def where_match_with_cache?(where_values, cache_record) 95 | condition = SecondLevelCache::Arel::Wheres.new(where_values) 96 | return false unless condition.all_equality? 97 | condition.extract_pairs.all? do |pair| 98 | cache_record.read_attribute(pair[:left]) == pair[:right] 99 | end 100 | end 101 | 102 | def limit_one? 103 | limit_value.blank? || limit_value == 1 104 | end 105 | 106 | def select_all_column? 107 | select_values.blank? 108 | end 109 | 110 | def from_identity_map(id) 111 | ::ActiveRecord::IdentityMap.get(@klass, id) 112 | end 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /lib/second_level_cache/active_record/has_one_association.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | module SecondLevelCache 3 | module ActiveRecord 4 | module Associations 5 | module HasOneAssociation 6 | extend ActiveSupport::Concern 7 | included do 8 | class_eval do 9 | alias_method_chain :find_target, :second_level_cache 10 | end 11 | end 12 | 13 | def find_target_with_second_level_cache 14 | return find_target_without_second_level_cache unless association_class.second_level_cache_enabled? 15 | cache_record = association_class.fetch_by_uniq_key(owner[reflection.active_record_primary_key], reflection.foreign_key) 16 | return cache_record.tap{|record| set_inverse_instance(record)} if cache_record 17 | 18 | record = find_target_without_second_level_cache 19 | 20 | record.tap do |r| 21 | set_inverse_instance(r) 22 | r.write_second_level_cache 23 | end if record 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/second_level_cache/active_record/persistence.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | module SecondLevelCache 3 | module ActiveRecord 4 | module Persistence 5 | extend ActiveSupport::Concern 6 | 7 | included do 8 | class_eval do 9 | alias_method_chain :reload, :second_level_cache 10 | alias_method_chain :touch, :second_level_cache 11 | alias_method_chain :update_column, :second_level_cache 12 | end 13 | end 14 | 15 | def update_column_with_second_level_cache(name, value) 16 | update_column_without_second_level_cache(name, value).tap{update_second_level_cache} 17 | end 18 | 19 | def reload_with_second_level_cache(options = nil) 20 | reload_without_second_level_cache(options).tap{expire_second_level_cache} 21 | end 22 | 23 | def touch_with_second_level_cache(name = nil) 24 | touch_without_second_level_cache(name).tap{update_second_level_cache} 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/second_level_cache/arel/wheres.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | module SecondLevelCache 3 | module Arel 4 | class Wheres 5 | attr_reader :where_values 6 | 7 | def initialize(where_values) 8 | @where_values = where_values 9 | end 10 | 11 | # Determine whether all conditions is equality, for example: 12 | # 13 | # Article.where("user_id = 1").where(:status => 1).find(1) 14 | def all_equality? 15 | where_values.all?{|where_value| where_value.is_a?(::Arel::Nodes::Equality)} 16 | end 17 | 18 | # Extract conditions to pairs, for checking whether cache match the conditions. 19 | def extract_pairs 20 | where_values.map do |where_value| 21 | if where_value.is_a?(String) 22 | left, right = where_value.split(/\s*=\s*/, 2) 23 | right = right.to_i 24 | else 25 | left, right = where_value.left.name, where_value.right 26 | end 27 | { 28 | :left => left, 29 | :right => right 30 | } 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/second_level_cache/config.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | module SecondLevelCache 3 | module Config 4 | extend self 5 | 6 | attr_accessor :cache_store, :logger, :cache_key_prefix 7 | 8 | def cache_store 9 | @cache_store ||= Rails.cache if defined?(Rails) 10 | @cache_store 11 | end 12 | 13 | def logger 14 | @logger ||= Rails.logger if defined?(Rails) 15 | @logger ||= Logger.new(STDOUT) 16 | end 17 | 18 | def cache_key_prefix 19 | @cache_key_prefix ||= 'slc' 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/second_level_cache/record_marshal.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | module RecordMarshal 3 | class << self 4 | # dump ActiveRecord instace with only attributes. 5 | # ["User", 6 | # {"id"=>30, 7 | # "email"=>"dddssddd@gmail.com", 8 | # "created_at"=>2012-07-25 18:25:57 UTC 9 | # } 10 | # ] 11 | 12 | def dump(record) 13 | [ 14 | record.class.name, 15 | record.attributes 16 | ] 17 | end 18 | 19 | # load a cached record 20 | def load(serialized) 21 | return unless serialized 22 | record = serialized[0].constantize.allocate 23 | record.init_with('attributes' => serialized[1]) 24 | record 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/second_level_cache/version.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | module SecondLevelCache 3 | VERSION = "1.6.2" 4 | end 5 | -------------------------------------------------------------------------------- /second_level_cache.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/second_level_cache/version', __FILE__) 3 | lib = File.expand_path('../lib', __FILE__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | 6 | Gem::Specification.new do |gem| 7 | gem.authors = ["wangxz"] 8 | gem.email = ["wangxz@csdn.net"] 9 | gem.description = %q{Write Through and Read Through caching library inspired by CacheMoney and cache_fu, support only Rails3 and ActiveRecord.} 10 | gem.summary = <<-SUMMARY 11 | SecondLevelCache is a write-through and read-through caching library inspired by Cache Money and cache_fu, support only Rails3 and ActiveRecord. 12 | 13 | Read-Through: Queries by ID, like current_user.articles.find(params[:id]), will first look in cache store and then look in the database for the results of that query. If there is a cache miss, it will populate the cache. 14 | 15 | Write-Through: As objects are created, updated, and deleted, all of the caches are automatically kept up-to-date and coherent. 16 | SUMMARY 17 | 18 | gem.homepage = "https://github.com/csdn-dev/second_level_cache" 19 | 20 | gem.files = Dir.glob("lib/**/*.rb") + [ 21 | "README.md", 22 | "Rakefile", 23 | "Gemfile", 24 | "init.rb", 25 | "CHANGELOG.md", 26 | "second_level_cache.gemspec" 27 | ] 28 | gem.test_files = Dir.glob("test/**/*.rb") 29 | gem.executables = gem.files.grep(%r{^bin/}) 30 | gem.name = "second_level_cache" 31 | gem.require_paths = ["lib"] 32 | gem.version = SecondLevelCache::VERSION 33 | 34 | gem.add_runtime_dependency "activesupport", ["~> 3.2.0"] 35 | 36 | gem.add_development_dependency "activerecord", ["~> 3.2.0"] 37 | gem.add_development_dependency "sqlite3" 38 | gem.add_development_dependency "rake" 39 | end 40 | -------------------------------------------------------------------------------- /test/active_record/base_test.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'active_record/test_helper' 3 | 4 | class ActiveRecord::BaseTest < Test::Unit::TestCase 5 | def setup 6 | @user = User.create :name => 'csdn', :email => 'test@csdn.com' 7 | end 8 | 9 | def test_should_update_cache_when_update_attributes 10 | @user.update_attributes :name => 'change' 11 | assert_equal @user.name, User.read_second_level_cache(@user.id).name 12 | end 13 | 14 | def test_should_update_cache_when_update_attribute 15 | @user.update_attribute :name, 'change' 16 | assert_equal @user.name, User.read_second_level_cache(@user.id).name 17 | end 18 | 19 | def test_should_expire_cache_when_destroy 20 | @user.destroy 21 | assert_nil User.read_second_level_cache(@user.id) 22 | end 23 | 24 | def test_should_expire_cache_when_update_counters 25 | assert_equal @user.books_count, 0 26 | @user.books.create 27 | assert_nil User.read_second_level_cache(@user.id) 28 | user = User.find(@user.id) 29 | assert_equal user.books_count, @user.books_count + 1 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/active_record/belongs_to_association_test.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'active_record/test_helper' 3 | 4 | class ActiveRecord::BelongsToAssociationTest < Test::Unit::TestCase 5 | def setup 6 | @user = User.create :name => 'csdn', :email => 'test@csdn.com' 7 | end 8 | 9 | def test_should_get_cache_when_use_belongs_to_association 10 | book = @user.books.create 11 | 12 | @user.write_second_level_cache 13 | book.clear_association_cache 14 | no_connection do 15 | assert_equal @user, book.user 16 | end 17 | end 18 | 19 | def test_should_write_belongs_to_association_cache 20 | book = @user.books.create 21 | @user.expire_second_level_cache 22 | assert_nil User.read_second_level_cache(@user.id) 23 | assert_equal @user, book.user 24 | # assert_not_nil User.read_second_level_cache(@user.id) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/active_record/finder_methods_test.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'active_record/test_helper' 3 | 4 | class ActiveRecord::FinderMethodsTest < Test::Unit::TestCase 5 | def setup 6 | @user = User.create :name => 'csdn', :email => 'test@csdn.com' 7 | end 8 | 9 | def test_should_find_without_cache 10 | SecondLevelCache.cache_store.clear 11 | assert_equal @user, User.find(@user.id) 12 | end 13 | 14 | def test_should_find_with_cache 15 | @user.write_second_level_cache 16 | no_connection do 17 | assert_equal @user, User.find(@user.id) 18 | end 19 | end 20 | 21 | def test_should_find_with_condition 22 | @user.write_second_level_cache 23 | no_connection do 24 | assert_equal @user, User.where(:name => @user.name).find(@user.id) 25 | end 26 | end 27 | 28 | def test_should_NOT_find_from_cache_when_select_speical_columns 29 | @user.write_second_level_cache 30 | only_id_user = User.select("id").find(@user.id) 31 | assert_raise(ActiveModel::MissingAttributeError) do 32 | only_id_user.name 33 | end 34 | end 35 | 36 | def test_without_second_level_cache 37 | @user.name = "NewName" 38 | @user.write_second_level_cache 39 | User.without_second_level_cache do 40 | @from_db = User.find(@user.id) 41 | end 42 | assert_not_equal @user.name, @from_db.name 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/active_record/model/book.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | ActiveRecord::Base.connection.create_table(:books, :force => true) do |t| 3 | t.string :title 4 | t.string :body 5 | t.integer :user_id 6 | t.integer :images_count, :default => 0 7 | end 8 | 9 | class Book < ActiveRecord::Base 10 | acts_as_cached 11 | 12 | belongs_to :user, :counter_cache => true 13 | has_many :images, :as => :imagable 14 | end 15 | -------------------------------------------------------------------------------- /test/active_record/model/image.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | ActiveRecord::Base.connection.create_table(:images, :force => true) do |t| 3 | t.string :url 4 | t.string :imagable_type 5 | t.integer :imagable_id 6 | end 7 | 8 | class Image < ActiveRecord::Base 9 | acts_as_cached 10 | 11 | belongs_to :imagable, :polymorphic => true, :counter_cache => true 12 | end 13 | 14 | -------------------------------------------------------------------------------- /test/active_record/model/post.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | ActiveRecord::Base.connection.create_table(:posts, :force => true) do |t| 3 | t.text :body 4 | t.integer :topic_id 5 | end 6 | 7 | class Post < ActiveRecord::Base 8 | acts_as_cached 9 | belongs_to :topic, :touch => true 10 | end 11 | -------------------------------------------------------------------------------- /test/active_record/model/topic.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | ActiveRecord::Base.connection.create_table(:topics, :force => true) do |t| 3 | t.string :title 4 | t.text :body 5 | 6 | t.timestamps 7 | end 8 | 9 | class Topic < ActiveRecord::Base 10 | acts_as_cached 11 | 12 | has_many :posts 13 | end 14 | -------------------------------------------------------------------------------- /test/active_record/model/user.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | ActiveRecord::Base.connection.create_table(:users, :force => true) do |t| 3 | t.text :options 4 | t.string :name 5 | t.string :email 6 | t.integer :books_count, :default => 0 7 | t.integer :images_count, :default => 0 8 | t.timestamps 9 | end 10 | 11 | class User < ActiveRecord::Base 12 | CacheVersion = 3 13 | serialize :options, Array 14 | acts_as_cached(:version => CacheVersion, :expires_in => 3.day) 15 | 16 | has_many :books 17 | has_many :images, :as => :imagable 18 | end 19 | -------------------------------------------------------------------------------- /test/active_record/persistence_test.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'active_record/test_helper' 3 | 4 | class ActiveRecord::PersistenceTest < Test::Unit::TestCase 5 | def setup 6 | @user = User.create :name => 'csdn', :email => 'test@csdn.com' 7 | @topic = Topic.create :title => "csdn" 8 | end 9 | 10 | def test_should_reload_object 11 | User.increment_counter :books_count, @user.id 12 | assert_equal 0, @user.books_count 13 | assert_equal 1, @user.reload.books_count 14 | end 15 | 16 | def test_should_update_cache_after_touch 17 | old_updated_time = @user.updated_at 18 | @user.touch 19 | assert !(old_updated_time == @user.updated_at) 20 | new_user = User.find @user.id 21 | assert_equal new_user, @user 22 | end 23 | 24 | def test_should_update_cache_after_update_column 25 | @user.update_column :name, "new_name" 26 | new_user = User.find @user.id 27 | assert_equal new_user, @user 28 | end 29 | 30 | def test_should_return_true_if_touch_ok 31 | assert @topic.touch == true 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/active_record/polymorphic_association_test.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'active_record/test_helper' 3 | 4 | class ActiveRecord::PolymorphicAssociationTest < Test::Unit::TestCase 5 | def setup 6 | @user = User.create :name => 'csdn', :email => 'test@csdn.com' 7 | end 8 | 9 | def test_should_get_cache_when_use_polymorphic_association 10 | image = @user.images.create 11 | 12 | @user.write_second_level_cache 13 | no_connection do 14 | assert_equal @user, image.imagable 15 | end 16 | end 17 | 18 | def test_should_write_polymorphic_association_cache 19 | image = @user.images.create 20 | assert_nil User.read_second_level_cache(@user.id) 21 | assert_equal @user, image.imagable 22 | end 23 | end 24 | 25 | -------------------------------------------------------------------------------- /test/active_record/second_level_cache_test.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'active_record/test_helper' 3 | 4 | class ActiveRecord::SecondLevelCacheTest < Test::Unit::TestCase 5 | def setup 6 | @user = User.create :name => 'csdn', :email => 'test@csdn.com' 7 | end 8 | 9 | def test_should_get_cache_key 10 | assert_equal "slc/user/#{@user.id}/#{User::CacheVersion}", @user.second_level_cache_key 11 | end 12 | 13 | def test_should_write_and_read_cache 14 | @user.write_second_level_cache 15 | assert_not_nil User.read_second_level_cache(@user.id) 16 | @user.expire_second_level_cache 17 | assert_nil User.read_second_level_cache(@user.id) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/active_record/test_helper.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'test_helper' 3 | require 'active_record' 4 | require 'second_level_cache/active_record' 5 | 6 | def open_test_db_connect 7 | ActiveRecord::Base.establish_connection( 8 | :adapter => 'sqlite3', 9 | :database => 'test/test.sqlite3' 10 | ) 11 | end 12 | open_test_db_connect 13 | 14 | def close_test_db_connect 15 | ActiveRecord::Base.connection.disconnect! 16 | end 17 | 18 | class Test::Unit::TestCase 19 | def no_connection 20 | close_test_db_connect 21 | assert_nothing_raised { yield } 22 | ensure 23 | open_test_db_connect 24 | end 25 | 26 | def teardown 27 | User.delete_all 28 | end 29 | end 30 | 31 | require 'active_record/model/user' 32 | require 'active_record/model/book' 33 | require 'active_record/model/image' 34 | require 'active_record/model/topic' 35 | require 'active_record/model/post' 36 | -------------------------------------------------------------------------------- /test/record_marshal_test.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'active_record/test_helper' 3 | 4 | class RecordMarshalTest < Test::Unit::TestCase 5 | def setup 6 | @user = User.create :name => 'csdn', :email => 'test@csdn.com' 7 | end 8 | 9 | def test_should_dump_active_record_object 10 | dumped = RecordMarshal.dump(@user) 11 | assert dumped.is_a?(Array) 12 | assert_equal "User", dumped[0] 13 | assert_equal @user.attributes, dumped[1] 14 | end 15 | 16 | 17 | def test_should_load_active_record_object 18 | @user.write_second_level_cache 19 | assert_equal @user, User.read_second_level_cache(@user.id) 20 | end 21 | 22 | 23 | def test_should_load_nil 24 | @user.expire_second_level_cache 25 | assert_nil User.read_second_level_cache(@user.id) 26 | end 27 | 28 | def test_should_load_active_record_object_without_association_cache 29 | @user.books 30 | @user.write_second_level_cache 31 | assert_empty User.read_second_level_cache(@user.id).association_cache 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/require_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'active_record' 3 | 4 | class RequireTest < Test::Unit::TestCase 5 | def setup 6 | ActiveRecord::Relation 7 | require 'active_record/test_helper' 8 | @user = User.create :name => 'Dingding Ye', :email => 'yedingding@gmail.com' 9 | end 10 | 11 | def test_should_find_the_user 12 | assert_equal @user, User.find(@user.id) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'rubygems' 3 | require 'bundler/setup' 4 | require 'second_level_cache' 5 | require 'test/unit' 6 | 7 | SecondLevelCache.configure do |config| 8 | config.cache_store = ActiveSupport::Cache::MemoryStore.new 9 | end 10 | 11 | SecondLevelCache.logger.level = Logger::INFO 12 | --------------------------------------------------------------------------------