model_cached 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Ary Djmal
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.rdoc ADDED
@@ -0,0 +1,81 @@
1
+ = model_cached plugin
2
+
3
+ This Rails 3 plugin gives you the ability to transparently cache single active record objects using memcached.
4
+
5
+ The purpose is to cache by fields that are being validated for unique values.
6
+
7
+ Cache gets refresh after save, and expire after destroy or after a logical delete.
8
+
9
+ It is less than 100 lines of code, so don't be shy to look at the source for full understanding.
10
+
11
+
12
+ == Usage
13
+
14
+ In your model:
15
+
16
+ # defines find_by_id, and handles the cache refresh or expiration
17
+ # also modifies find, so when pass a single integer it uses find_by_id or raises and exception
18
+ cache_by :id
19
+
20
+ # same plus defines find_by_email
21
+ cache_by :id, :email
22
+
23
+ # same but only gives you the cache version if call it with the right scope
24
+ # eg: User.find_by_id(1) => will not use cache
25
+ # eg: current_account.users.find_by_id(1) => will use cache
26
+ cache_by :id, :email, :scope => :account_id
27
+
28
+ # you should use the :logical_delete option specifying the method that checks if a record is deleted
29
+ # disregard if not using logical deletes
30
+ cache_by :id, :email, :logical_delete => :is_deleted?
31
+
32
+ == Real life example
33
+
34
+ class Account < ActiveRecord::Base
35
+ has_many :users
36
+ cache_by :subdomain
37
+ end
38
+
39
+ class User < ActiveRecord::Base
40
+ belongs_to :account
41
+ cache_by :id, :scope => :account_id
42
+ end
43
+
44
+ class ApplicationController < ActionController::Base
45
+ before_filter :require_account, :require_user
46
+
47
+ private
48
+ def current_account
49
+ @_current_account ||= Account.find_by_subdomain(request.subdomain)
50
+ end
51
+
52
+ def current_user
53
+ @_current_user ||= session[:user_id] ? current_account.users.find_by_id(session[:user_id]) : nil
54
+ end
55
+
56
+ def require_account
57
+ unless current_account
58
+ redirect_to sign_up_url
59
+ end
60
+ end
61
+
62
+ def require_user
63
+ unless current_account
64
+ redirect_to sign_in_url
65
+ end
66
+ end
67
+ end
68
+
69
+
70
+ == Dependencies
71
+
72
+ memcached
73
+
74
+
75
+ == Install
76
+
77
+ ./script/plugin install git://github.com/arydjmal/model_cached.git
78
+
79
+
80
+
81
+ Copyright (c) 2012 Ary Djmal, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+
4
+ desc 'Default: run unit tests.'
5
+ task :default => :test
6
+
7
+ desc 'Test the model_cached gem.'
8
+ Rake::TestTask.new(:test) do |t|
9
+ t.libs << 'lib'
10
+ t.libs << 'test'
11
+ t.pattern = 'test/**/*_test.rb'
12
+ t.verbose = false
13
+ end
@@ -0,0 +1,81 @@
1
+ module ModelCached
2
+ def self.included(base)
3
+ base.extend(ClassMethods)
4
+ end
5
+
6
+ module ClassMethods
7
+ def cache_by(*column_names)
8
+ options = {logical_delete: nil, scope: nil}.merge(column_names.extract_options!)
9
+ options[:columns] = column_names
10
+ after_save :refresh_mc_keys
11
+ after_destroy :expire_mc_keys
12
+ class_eval %{ def self.mc_options; #{options}; end }
13
+ column_names.each do |column|
14
+ class_eval %{ def self.find_by_#{column}(value); find_cached_by(#{column.to_s.inspect}, value); end }
15
+ end
16
+ if column_names.include?(:id)
17
+ class_eval do
18
+ def self.find(*args)
19
+ if args.size == 1 && (id = args.first).is_a?(Integer)
20
+ find_by_id(id) || raise(ActiveRecord::RecordNotFound, "Couldn't find #{name} with ID=#{id}")
21
+ else
22
+ super(*args)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ include ModelCached::InstanceMethods
28
+ end
29
+
30
+ def find_cached_by(column, value)
31
+ if mc_options[:scope] and mc_scope_key.nil?
32
+ where(column => value).first
33
+ else
34
+ Rails.cache.fetch(mc_key(column, value)) { where(column => value).first }
35
+ end
36
+ end
37
+
38
+ def mc_scope_key
39
+ self.new().mc_scope_key
40
+ end
41
+
42
+ def mc_key(column, value, scope_key=nil)
43
+ [column, value, scope_key || mc_scope_key].unshift(self.name.tableize).compact.join('/').gsub(/\s+/, '+')
44
+ end
45
+ end
46
+
47
+ module InstanceMethods
48
+ def mc_columns; self.class.mc_options[:columns]; end
49
+ def mc_scope; self.class.mc_options[:scope]; end
50
+ def mc_logical_delete; self.class.mc_options[:logical_delete]; end
51
+
52
+ def mc_key(column, value=nil)
53
+ self.class.mc_key(column, value || self.send(column), mc_scope_key)
54
+ end
55
+
56
+ def mc_scope_key
57
+ "#{mc_scope}:#{self.send(mc_scope)}" if mc_scope
58
+ end
59
+
60
+ def expire_mc_keys
61
+ mc_columns.each do |column|
62
+ Rails.cache.delete(mc_key(column))
63
+ end
64
+ end
65
+
66
+ def refresh_mc_keys
67
+ if mc_logical_delete && send(mc_logical_delete)
68
+ expire_mc_keys
69
+ else
70
+ mc_columns.each do |column|
71
+ Rails.cache.write(mc_key(column), self)
72
+ if send("#{column}_changed?")
73
+ Rails.cache.delete(mc_key(column, send("#{column}_was")))
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ ActiveRecord::Base.send(:include, ModelCached)
@@ -0,0 +1,14 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'model_cached'
3
+ s.version = '0.1.0'
4
+ s.authors = ['Ary Djmal']
5
+ s.email = ['arydjmal@gmail.com']
6
+ s.summary = 'Rails gem that gives you the ability to transparently cache single active record objects using memcached.'
7
+ s.homepage = 'http://github.com/arydjmal/model_cached'
8
+
9
+ s.add_dependency 'rails', '>= 3.0.0'
10
+
11
+ s.add_development_dependency 'rake'
12
+
13
+ s.files = Dir["#{File.dirname(__FILE__)}/**/*"]
14
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,31 @@
1
+ require 'test/unit'
2
+ require 'rails'
3
+ require 'active_support'
4
+ require 'active_record'
5
+ require 'memcache'
6
+ require 'model_cached'
7
+
8
+ ActiveRecord::Base.establish_connection({
9
+ :adapter => 'sqlite3',
10
+ :database => ':memory:'
11
+ })
12
+
13
+ ActiveRecord::Schema.define do
14
+ create_table :accounts do |t|
15
+ t.string :name
16
+ end
17
+
18
+ create_table :users do |t|
19
+ t.string :email
20
+ t.integer :account_id
21
+ t.boolean :deleted
22
+ end
23
+ end
24
+
25
+ class Account < ActiveRecord::Base
26
+ has_many :users
27
+ end
28
+
29
+ class User < ActiveRecord::Base; end
30
+
31
+ RAILS_CACHE = ActiveSupport::Cache.lookup_store(:mem_cache_store)
@@ -0,0 +1,155 @@
1
+ require 'helper'
2
+
3
+ class ModelCachedTest < Test::Unit::TestCase
4
+ def setup
5
+ User.delete_all
6
+ User.send(:cache_by, :id, :email)
7
+ @ary = User.create!(:email => 'Ary')
8
+ @nati = User.create!(:email => 'Nati')
9
+ Rails.cache.clear
10
+ end
11
+
12
+ def test_make_cache_key
13
+ assert_equal 'users/email/Ary', @ary.mc_key('email')
14
+ end
15
+
16
+ def test_find_with_correct_data_when_not_in_cache
17
+ assert_equal nil, Rails.cache.read("users/id/#{@ary.id}")
18
+ assert_equal @ary, User.find(@ary.id)
19
+ assert_equal @ary, Rails.cache.read("users/id/#{@ary.id}")
20
+ end
21
+
22
+ def test_find_with_incorrect_data_when_not_in_cache
23
+ assert_raise(ActiveRecord::RecordNotFound) do
24
+ User.find(1234567890)
25
+ end
26
+ end
27
+
28
+ def test_find_by_id_with_correct_data_when_not_in_cache
29
+ assert_equal @ary, User.find_by_id(@ary.id)
30
+ end
31
+
32
+ def test_find_by_id_with_correct_data_when_in_cache
33
+ assert_equal nil, Rails.cache.read("users/id/#{@ary.id}")
34
+ assert_equal @ary, User.find_by_id(@ary.id)
35
+ assert_equal @ary, Rails.cache.read("users/id/#{@ary.id}")
36
+ end
37
+
38
+ def test_find_by_email_with_correct_data
39
+ assert_equal @ary, User.find_by_email('Ary')
40
+ end
41
+
42
+ def test_find_by_email_with_correct_data_when_in_cache
43
+ Rails.cache.write('users/email/Naty', @nati)
44
+ assert_equal @nati, User.find_by_email('Nati')
45
+ end
46
+
47
+ def test_find_by_email_with_incorrect_data
48
+ assert_equal nil, User.find_by_email('Djmal')
49
+ end
50
+
51
+ def test_should_refresh_cache_after_save
52
+ @ary.email = 'new_email'
53
+ @ary.save
54
+ assert_equal 'new_email', Rails.cache.read("users/id/#{@ary.id}").try(:email)
55
+ end
56
+
57
+ def test_should_expire_cache_and_write_new_cache_when_keys_are_modified
58
+ assert_equal nil, Rails.cache.read('users/email/Ary')
59
+ @ary = User.find_by_email('Ary')
60
+ assert_equal @ary, Rails.cache.read('users/email/Ary')
61
+ @ary.email = 'new_email'
62
+ @ary.save
63
+ assert_equal nil, Rails.cache.read('users/email/Ary')
64
+ assert_equal @ary, Rails.cache.read('users/email/new_email')
65
+ end
66
+
67
+ def test_should_expire_cache_on_destroy
68
+ assert_equal nil, Rails.cache.read("users/id/#{@ary.id}")
69
+ @ary = User.find_by_id(@ary.id)
70
+ assert_equal @ary, Rails.cache.read("users/id/#{@ary.id}")
71
+ @ary.destroy
72
+ assert_equal nil, Rails.cache.read("users/id/#{@ary.id}")
73
+ end
74
+ end
75
+
76
+ class ModelCachedWithScopeTest < Test::Unit::TestCase
77
+ def setup
78
+ User.delete_all
79
+ User.send(:cache_by, :id, :email, :scope => :account_id)
80
+ @account = Account.create!(:name => 'Natural Bits')
81
+ @ary = @account.users.create!(:email => 'Ary')
82
+ @nati = @account.users.create!(:email => 'Nati')
83
+ Rails.cache.clear
84
+ end
85
+
86
+ def test_make_cache_key
87
+ assert_equal "users/email/Ary/account_id:#{@account.id}", @ary.mc_key('email')
88
+ end
89
+
90
+ def test_find_by_id_with_correct_data_when_not_in_cache
91
+ assert_equal @ary, @account.users.find_by_id(@ary.id)
92
+ end
93
+
94
+ def test_find_by_id_with_correct_data_when_in_cache
95
+ key = "users/id/#{@ary.id}/account_id:#{@account.id}"
96
+ assert_equal nil, Rails.cache.read(key)
97
+ @account.users.find_by_id(@ary.id)
98
+ assert_equal @ary, Rails.cache.read(key)
99
+ end
100
+
101
+ def test_find_by_id_without_scoping
102
+ key = "users/id/#{@ary.id}/account_id:#{@account.id}"
103
+ assert_equal nil, Rails.cache.read(key)
104
+ User.find_by_id(@ary.id)
105
+ assert_equal nil, Rails.cache.read(key)
106
+ end
107
+
108
+ def test_find_by_email_with_correct_data
109
+ assert_equal @ary, @account.users.find_by_email('Ary')
110
+ end
111
+
112
+ def test_find_by_email_with_correct_data_when_in_cache
113
+ Rails.cache.write('users/email/Naty', @nati)
114
+ assert_equal @nati, @account.users.find_by_email('Nati')
115
+ end
116
+
117
+ def test_find_by_email_with_incorrect_data
118
+ assert_equal nil, @account.users.find_by_email('Djmal')
119
+ end
120
+
121
+ def test_should_refresh_cache_after_save
122
+ @ary.email = 'new_email'
123
+ @ary.save
124
+ assert_equal 'new_email', Rails.cache.read("users/id/#{@ary.id}/account_id:#{@account.id}").try(:email)
125
+ end
126
+
127
+ def test_should_expire_cache_and_write_new_cache_when_keys_are_modified
128
+ assert_equal nil, Rails.cache.read("users/email/Ary/account_id:#{@account.id}")
129
+ @ary = @account.users.find_by_email('Ary')
130
+ @ary.email = 'new_email'
131
+ @ary.save
132
+ assert_equal nil, Rails.cache.read("users/email/Ary/account_id:#{@account.id}")
133
+ assert_equal @ary, Rails.cache.read("users/email/new_email/account_id:#{@account.id}")
134
+ end
135
+ end
136
+
137
+ class ModelCachedWithLogicalDeleteTest < Test::Unit::TestCase
138
+ def setup
139
+ User.delete_all
140
+ User.send(:cache_by, :id, :logical_delete => :is_deleted?)
141
+ User.class_eval { def is_deleted?() deleted == true; end }
142
+ @ary = User.create!(:email => 'Ary', :deleted => false)
143
+ @nati = User.create!(:email => 'Nati', :deleted => false)
144
+ Rails.cache.clear
145
+ end
146
+
147
+ def test_default_logical_delete
148
+ assert_equal nil, Rails.cache.read("users/id/#{@ary.id}")
149
+ @ary = User.find_by_id(@ary.id)
150
+ assert_equal @ary, Rails.cache.read("users/id/#{@ary.id}")
151
+ @ary.deleted = true
152
+ @ary.save
153
+ assert_equal nil, Rails.cache.read("users/id/#{@ary.id}")
154
+ end
155
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: model_cached
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ary Djmal
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-08-28 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rails
16
+ requirement: &70255150855040 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 3.0.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70255150855040
25
+ - !ruby/object:Gem::Dependency
26
+ name: rake
27
+ requirement: &70255150852720 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70255150852720
36
+ description:
37
+ email:
38
+ - arydjmal@gmail.com
39
+ executables: []
40
+ extensions: []
41
+ extra_rdoc_files: []
42
+ files:
43
+ - ./lib/model_cached.rb
44
+ - ./MIT-LICENSE
45
+ - ./model_cached.gemspec
46
+ - ./Rakefile
47
+ - ./README.rdoc
48
+ - ./test/helper.rb
49
+ - ./test/model_cached_test.rb
50
+ homepage: http://github.com/arydjmal/model_cached
51
+ licenses: []
52
+ post_install_message:
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ! '>='
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ requirements: []
69
+ rubyforge_project:
70
+ rubygems_version: 1.8.10
71
+ signing_key:
72
+ specification_version: 3
73
+ summary: Rails gem that gives you the ability to transparently cache single active
74
+ record objects using memcached.
75
+ test_files: []
76
+ has_rdoc: