cached-models 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,61 @@
1
+ *0.0.3 (October 22nd, 2008)*
2
+
3
+ * Tagged v0.0.3
4
+
5
+ * Test cases cleanup
6
+
7
+ * Sugar syntax for AssociationCollection#size
8
+
9
+ * Use loaded collection, instead of read from cache, when use scoped find form AssociationCollection
10
+
11
+ author.posts.find(1) # no cache read for #find
12
+
13
+ * Use loaded collection, instead of read from cache, when use #size, #empty? and #any? from AssociationCollection. Added support for :uniq option.
14
+
15
+ * Reduced cache overhead using read instead of fetch for access to AssociationCollection. Enhanced Mocha expectactions.
16
+
17
+ # BEFORE
18
+ author.posts # => cache fetch
19
+
20
+ # NOW
21
+ author.posts # => cache read
22
+
23
+ * Fixed typos in CHANGELOG and README
24
+
25
+ * Don't instantiate ivar when read from AssociationCollection if options[:cached] == true
26
+
27
+ * Reduced by half cache lookups when read from AssociationCollection
28
+
29
+ # BEFORE
30
+ author.posts # => cache read + cache fetch
31
+
32
+ # NOW
33
+ author.posts # => cache fetch
34
+
35
+ * Fixed Mocha expectations
36
+
37
+ * Fixed clear, delete and destroy cases for AssociationCollection
38
+
39
+ author.posts.delete(post)
40
+ author.posts.delete_all
41
+ author.posts.destroy
42
+ author.posts.destroy_all
43
+ author.posts.clear
44
+
45
+ * Fixed concurrency issues, using Thread#current to store cached_associations instead of ivar
46
+
47
+ * Make sure tests suite runs in 'test' environment. Introduced SKIP_MOCHA env variable, in order to run tests directly on cache
48
+
49
+ $ rake cached_models SKIP_MOCHA=true
50
+
51
+ * Bypass cache for will_paginate on association collection
52
+
53
+ author.posts.paginate(:all, :page => 1, :per_page => 10)
54
+
55
+ * Make sure habtm and has_one are safely used
56
+
57
+
58
+
1
59
  *0.0.2 (October 10th, 2008)*
2
60
 
3
61
  * Updated README with new installation instructions
data/README CHANGED
@@ -8,7 +8,7 @@ Check for news and tutorials at the {project home page}[http://www.lucaguidi.com
8
8
 
9
9
  = Usage
10
10
 
11
- Using Memcached and Rails 2.2.1
11
+ Using Memcached and Rails 2.1.1
12
12
 
13
13
  Make sure to configure your current environment with:
14
14
 
data/Rakefile CHANGED
@@ -2,7 +2,7 @@ require 'rake'
2
2
  require 'rake/testtask'
3
3
  require 'rake/rdoctask'
4
4
 
5
- version = '0.0.2'
5
+ version = '0.0.3'
6
6
  repositories = %w( origin rubyforge )
7
7
 
8
8
  desc 'Default: run unit tests.'
data/about.yml CHANGED
@@ -1,8 +1,8 @@
1
1
  author: Luca Guidi
2
2
  email: guidi.luca@gmail.com
3
3
  homepage: http://lucaguidi.com/pages/cached_models
4
- summary: CachedModels provides to your models a transparent approach to use Rails internal caching mechanism.
5
- description: CachedModels provides to your models a transparent approach to use Rails internal caching mechanism.
4
+ summary: CachedModels provides to your ActiveRecord models a transparent approach to use ActiveSupport caching mechanism.
5
+ description: CachedModels provides to your ActiveRecord models a transparent approach to use ActiveSupport caching mechanism.
6
6
  license: MIT
7
- rails_version: 2.1.1+
8
- version: 0.0.1
7
+ rails_version: 2.1.0+
8
+ version: 0.0.3
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "cached-models"
3
- s.version = "0.0.2"
4
- s.date = "2008-10-10"
3
+ s.version = "0.0.3"
4
+ s.date = "2008-10-22"
5
5
  s.summary = "Transparent caching policy for your models"
6
6
  s.author = "Luca Guidi"
7
7
  s.email = "guidi.luca@gmail.com"
@@ -9,9 +9,8 @@ Gem::Specification.new do |s|
9
9
  s.description = "CachedModels provides to your ActiveRecord models a transparent approach to use ActiveSupport caching mechanism."
10
10
  s.has_rdoc = true
11
11
  s.rubyforge_project = %q{cached-models}
12
- s.files = ["CHANGELOG", "MIT-LICENSE", "README", "Rakefile", "about.yml", "cached-models.gemspec", "init.rb", "install.rb", "lib/activerecord/lib/active_record.rb", "lib/activerecord/lib/active_record/associations.rb", "lib/activerecord/lib/active_record/associations/association_collection.rb", "lib/activerecord/lib/active_record/associations/association_proxy.rb", "lib/activerecord/lib/active_record/associations/has_many_association.rb", "lib/activerecord/lib/active_record/base.rb", "lib/cached-models.rb", "lib/cached_models.rb", "setup.rb", "tasks/cached_models_tasks.rake", "test/active_record/associations/has_many_association_test.rb", "test/active_record/base_test.rb", "test/fixtures/authors.yml", "test/fixtures/blogs.yml", "test/fixtures/comments.yml", "test/fixtures/posts.yml", "test/fixtures/tags.yml", "test/models/author.rb", "test/models/blog.rb", "test/models/comment.rb", "test/models/post.rb", "test/models/tag.rb", "test/test_helper.rb", "uninstall.rb"]
13
- s.test_files = ["test/active_record/associations/has_many_association_test.rb",
14
- "test/active_record/base_test.rb"]
12
+ s.files = ["CHANGELOG", "MIT-LICENSE", "README", "Rakefile", "about.yml", "cached-models.gemspec", "init.rb", "install.rb", "lib/activerecord/lib/active_record.rb", "lib/activerecord/lib/active_record/associations.rb", "lib/activerecord/lib/active_record/associations/association_collection.rb", "lib/activerecord/lib/active_record/associations/association_proxy.rb", "lib/activerecord/lib/active_record/associations/has_many_association.rb", "lib/activerecord/lib/active_record/base.rb", "lib/cached-models.rb", "lib/cached_models.rb", "setup.rb", "tasks/cached_models_tasks.rake", "test/active_record/associations/has_and_belongs_to_many_association_test.rb", "test/active_record/associations/has_many_association_test.rb", "test/active_record/associations/has_one_association_test.rb", "test/active_record/base_test.rb", "test/fixtures/addresses.yml", "test/fixtures/authors.yml", "test/fixtures/blogs.yml", "test/fixtures/categories.yml", "test/fixtures/categories_posts.yml", "test/fixtures/comments.yml", "test/fixtures/posts.yml", "test/fixtures/tags.yml", "test/models/address.rb", "test/models/author.rb", "test/models/blog.rb", "test/models/category.rb", "test/models/comment.rb", "test/models/post.rb", "test/models/tag.rb", "test/test_helper.rb", "uninstall.rb"]
13
+ s.test_files = ["test/active_record/associations/has_and_belongs_to_many_association_test.rb", "test/active_record/associations/has_many_association_test.rb", "test/active_record/associations/has_one_association_test.rb", "test/active_record/base_test.rb"]
15
14
  s.extra_rdoc_files = ['README', 'CHANGELOG']
16
15
 
17
16
  s.add_dependency("activesupport", ["> 2.1.0"])
@@ -251,11 +251,15 @@ module ActiveRecord
251
251
  end
252
252
 
253
253
  if options[:cached]
254
- method_name = "belongs_to_after_save_for_#{reflection.name}".to_sym
255
- define_method(method_name) do
254
+ after_save_method_name = "belongs_to_after_save_for_#{reflection.name}".to_sym
255
+ after_destroy_method_name = "belongs_to_after_destroy_for_#{reflection.name}".to_sym
256
+ define_method(after_save_method_name) do
256
257
  send(reflection.name).expire_cache_for(self.class.name)
257
258
  end
258
- after_save method_name
259
+
260
+ alias_method after_destroy_method_name, after_save_method_name
261
+ after_save after_save_method_name
262
+ after_destroy after_destroy_method_name
259
263
  end
260
264
 
261
265
  add_single_associated_validation_callbacks(reflection.name) if options[:validate] == true
@@ -277,19 +281,19 @@ module ActiveRecord
277
281
 
278
282
  unless association.respond_to?(:loaded?)
279
283
  association = association_proxy_class.new(self, reflection)
280
- instance_variable_set(ivar, association)
284
+ if options[:cached]
285
+ cache_write(reflection, association)
286
+ else
287
+ instance_variable_set(ivar, association)
288
+ end
281
289
  end
282
290
 
283
291
  if force_reload
284
292
  association.reload
285
- cache_delete(reflection) if options[:cached]
293
+ cache_write(reflection, association) if options[:cached]
286
294
  end
287
295
 
288
- if options[:cached]
289
- cache_fetch(reflection, association)
290
- else
291
- association
292
- end
296
+ association
293
297
  end
294
298
 
295
299
  method_name = "#{reflection.name.to_s.singularize}_ids"
@@ -306,6 +310,27 @@ module ActiveRecord
306
310
  end
307
311
  end
308
312
 
313
+ def has_and_belongs_to_many(association_id, options = {}, &extension)
314
+ reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension)
315
+
316
+ add_multiple_associated_validation_callbacks(reflection.name) unless options[:validate] == false
317
+ add_multiple_associated_save_callbacks(reflection.name)
318
+ collection_accessor_methods(reflection, HasAndBelongsToManyAssociation, options)
319
+
320
+ # Don't use a before_destroy callback since users' before_destroy
321
+ # callbacks will be executed after the association is wiped out.
322
+ old_method = "destroy_without_habtm_shim_for_#{reflection.name}"
323
+ class_eval <<-end_eval unless method_defined?(old_method)
324
+ alias_method :#{old_method}, :destroy_without_callbacks
325
+ def destroy_without_callbacks
326
+ #{reflection.name}.clear
327
+ #{old_method}
328
+ end
329
+ end_eval
330
+
331
+ add_association_callbacks(reflection.name, options)
332
+ end
333
+
309
334
  def collection_accessor_methods(reflection, association_proxy_class, options, writer = true)
310
335
  collection_reader_method(reflection, association_proxy_class, options)
311
336
 
@@ -4,22 +4,18 @@ module ActiveRecord
4
4
  module Associations
5
5
  class AssociationCollection < AssociationProxy #:nodoc:
6
6
  def find(*args)
7
+ options = args.extract_options!
7
8
  expects_array = args.first.kind_of?(Array)
8
- ids = args.flatten.compact.uniq.map(&:to_i)
9
+ args = args.flatten.compact.uniq
9
10
 
10
- if @reflection.options[:cached]
11
- result = @owner.send(:cache_read, @reflection)
12
- if result
13
- result = result.select { |record| ids.include? record.id }
14
- result = expects_array ? result : result.first
15
- return result
16
- end
11
+ if @reflection.options[:cached] && !args.first.is_a?(Symbol)
12
+ result = self.select { |record| args.map(&:to_i).include? record.id }
13
+ return expects_array ? result : result.first
17
14
  end
18
15
 
19
- options = args.extract_options!
20
-
21
16
  # If using a custom finder_sql, scan the entire collection.
22
17
  if @reflection.options[:finder_sql]
18
+ ids = args.flatten.compact.uniq.map(&:to_i)
23
19
  if ids.size == 1
24
20
  id = ids.first
25
21
  record = load_target.detect { |r| id == r.id }
@@ -72,13 +68,49 @@ module ActiveRecord
72
68
  result && self
73
69
  end
74
70
 
71
+ # Remove +records+ from this association. Does not destroy +records+.
72
+ def delete(*records)
73
+ records = flatten_deeper(records)
74
+ records.each { |record| raise_on_type_mismatch(record) }
75
+
76
+ @owner.transaction do
77
+ records.each { |record| callback(:before_remove, record) }
78
+
79
+ old_records = records.reject {|r| r.new_record? }
80
+ delete_records(old_records) if old_records.any?
81
+
82
+ records.each do |record|
83
+ @target.delete(record)
84
+ callback(:after_remove, record)
85
+ end
86
+ end
87
+
88
+ @owner.send(:cache_write, @reflection, self) if @reflection.options[:cached]
89
+ end
90
+
91
+ # Removes all records from this association. Returns +self+ so method calls may be chained.
92
+ def clear
93
+ return self if length.zero? # forces load_target if it hasn't happened already
94
+
95
+ if @reflection.options[:dependent] && @reflection.options[:dependent] == :destroy
96
+ destroy_all
97
+ else
98
+ delete_all
99
+ end
100
+
101
+ @owner.send(:cache_write, @reflection, self) if @reflection.options[:cached]
102
+
103
+ self
104
+ end
105
+
75
106
  # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and
76
107
  # calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero
77
108
  # and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length.
78
109
  def size
79
110
  if @reflection.options[:cached]
80
- result = @owner.send(:cache_read, @reflection)
81
- return result.to_ary.size if result
111
+ returning result = self.to_ary do
112
+ @reflection.options[:uniq] ? result.uniq.size : result.size
113
+ end
82
114
  end
83
115
 
84
116
  if @owner.new_record? || (loaded? && !@reflection.options[:uniq])
@@ -36,7 +36,10 @@ module ActiveRecord
36
36
  end
37
37
 
38
38
  def cache_write(reflection, value)
39
- cached_associations[reflection.name] = rails_cache.write(reflection_cache_key(reflection), value)
39
+ # This is a workaround for:
40
+ # http://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/1239-railscachewrite-returns-false-with-memcachestore
41
+ rails_cache.write(reflection_cache_key(reflection), value)
42
+ cached_associations[reflection.name] = true
40
43
  end
41
44
 
42
45
  def cache_delete(reflection)
@@ -63,7 +66,8 @@ module ActiveRecord
63
66
  end
64
67
 
65
68
  def cached_associations
66
- @cached_associations ||= {}
69
+ cached_associations = (Thread.current[:cached_associations] ||= {})
70
+ cached_associations[cache_key] ||= {}
67
71
  end
68
72
  end
69
- end
73
+ end
@@ -1,9 +1,11 @@
1
+ # RAILS_ENV = "test"
2
+
1
3
  require 'rubygems'
2
4
  require 'active_record'
3
5
  require 'active_record/fixtures'
4
6
 
5
7
  path_to_fixtures = File.dirname(__FILE__) + '/../test/fixtures'
6
- fixtures = %w( authors blogs posts comments tags )
8
+ fixtures = %w( addresses authors blogs posts categories categories_posts comments tags )
7
9
 
8
10
  desc 'Run default task (test)'
9
11
  task :cached_models => 'cached_models:test'
@@ -21,6 +23,17 @@ namespace :cached_models do
21
23
  desc 'Create CachedModels test database tables'
22
24
  task :create_tables => :environment do
23
25
  ActiveRecord::Schema.define do
26
+ create_table :addresses, :force => true do |t|
27
+ t.integer :author_id
28
+ t.string :street
29
+ t.string :zip
30
+ t.string :city
31
+ t.string :state
32
+ t.string :country
33
+
34
+ t.timestamps
35
+ end
36
+
24
37
  create_table :authors, :force => true do |t|
25
38
  t.integer :blog_id
26
39
  t.string :first_name
@@ -32,7 +45,7 @@ namespace :cached_models do
32
45
 
33
46
  create_table :blogs, :force => true do |t|
34
47
  t.string :title
35
-
48
+
36
49
  t.timestamps
37
50
  end
38
51
 
@@ -46,19 +59,30 @@ namespace :cached_models do
46
59
  t.timestamps
47
60
  end
48
61
 
62
+ create_table :categories, :force => true do |t|
63
+ t.string :name
64
+
65
+ t.timestamps
66
+ end
67
+
68
+ create_table :categories_posts, :force => true do |t|
69
+ t.integer :category_id
70
+ t.integer :post_id
71
+ end
72
+
49
73
  create_table :comments, :force => true do |t|
50
74
  t.integer :post_id
51
75
  t.string :email
52
76
  t.text :text
53
-
77
+
54
78
  t.timestamps
55
79
  end
56
-
80
+
57
81
  create_table :tags, :force => true do |t|
58
82
  t.integer :taggable_id
59
83
  t.string :taggable_type
60
84
  t.string :name
61
-
85
+
62
86
  t.timestamps
63
87
  end
64
88
  end
@@ -66,10 +90,13 @@ namespace :cached_models do
66
90
 
67
91
  desc 'Drops CachedModels test database tables'
68
92
  task :drop_tables => :environment do
93
+ ActiveRecord::Base.connection.drop_table :addresses
69
94
  ActiveRecord::Base.connection.drop_table :authors
70
95
  ActiveRecord::Base.connection.drop_table :posts
71
96
  ActiveRecord::Base.connection.drop_table :comments
72
97
  ActiveRecord::Base.connection.drop_table :tags
98
+ ActiveRecord::Base.connection.drop_table :categories
99
+ ActiveRecord::Base.connection.drop_table :categories_posts
73
100
  end
74
101
 
75
102
  desc 'Load fixtures'
@@ -0,0 +1,12 @@
1
+ require File.dirname(__FILE__) + '/../../test_helper'
2
+
3
+ class HasAndBelongsToManyAssociationTest < Test::Unit::TestCase
4
+ include ActiveRecord::Associations
5
+
6
+ def test_should_not_raise_exception
7
+ assert_nothing_raised ArgumentError do
8
+ posts(:welcome).categories
9
+ categories(:announcements).posts
10
+ end
11
+ end
12
+ end
@@ -10,7 +10,7 @@ class HasManyAssociationTest < Test::Unit::TestCase
10
10
 
11
11
  uses_mocha 'HasManyAssociationTest' do
12
12
  def test_should_always_use_cache_for_all_instances_which_reference_the_same_record
13
- cache.expects(:fetch).with("#{cache_key}/cached_posts").times(2).returns association_proxy
13
+ cache.expects(:read).with("#{cache_key}/cached_posts").returns association_proxy
14
14
  expected = authors(:luca).cached_posts
15
15
  actual = Author.first.cached_posts
16
16
  assert_equal expected, actual
@@ -20,26 +20,10 @@ class HasManyAssociationTest < Test::Unit::TestCase
20
20
  author = authors(:luca)
21
21
  old_cache_key = author.cache_key
22
22
 
23
- cache.expects(:fetch).with("#{cache_key}/cached_posts").times(2).returns association_proxy
23
+ cache.expects(:read).with("#{cache_key}/cached_posts").returns association_proxy
24
24
  cache.expects(:delete).with("#{cache_key}/cached_posts").returns true
25
-
26
- author.cached_posts # force cache loading
27
- author.update_attributes :first_name => author.first_name.upcase
28
-
29
- # assert_not_equal old_cache_key, author.cache_key
30
- assert_equal posts_by_author(:luca), authors(:luca).cached_posts
31
- end
32
-
33
- def test_should_not_expire_cache_on_update_on_missing_updated_at
34
- author = authors(:luca)
35
- old_cache_key = author.cache_key
36
-
37
- author.stubs(:[]).with(:updated_at).returns nil
38
- author.expects(:[]).with('blog_id').returns author.blog_id
39
- cache.expects(:fetch).with("#{cache_key}/cached_posts").times(2).returns association_proxy
40
- cache.expects(:delete).with("#{cache_key}/cached_posts").never
41
- cache.expects(:delete).with("#{cache_key}/cached_comments").never
42
- cache.expects(:delete).with("#{cache_key}/cached_posts_with_comments").never
25
+ cache.expects(:delete).with("#{cache_key}/cached_comments").returns true
26
+ cache.expects(:delete).with("#{cache_key}/cached_posts_with_comments").returns true
43
27
 
44
28
  author.cached_posts # force cache loading
45
29
  author.update_attributes :first_name => author.first_name.upcase
@@ -49,90 +33,70 @@ class HasManyAssociationTest < Test::Unit::TestCase
49
33
  end
50
34
 
51
35
  def test_should_use_cache_when_find_with_scope
52
- cache.expects(:fetch).with("#{cache_key}/cached_posts").returns association_proxy
53
-
36
+ cache.expects(:read).with("#{cache_key}/cached_posts").returns association_proxy
54
37
  post = authors(:luca).cached_posts.find(posts(:welcome).id)
55
38
  assert_equal posts(:welcome), post
56
39
  end
57
40
 
58
41
  def test_should_use_cache_when_find_with_scope_using_multiple_ids
59
- cache.expects(:fetch).with("#{cache_key}/cached_posts").returns association_proxy
60
-
42
+ cache.expects(:read).with("#{cache_key}/cached_posts").returns association_proxy
61
43
  ids = posts_by_author(:luca).map(&:id)
62
- assert_equal posts_by_author(:luca),
63
- authors(:luca).cached_posts.find(ids)
44
+ assert_equal posts_by_author(:luca), authors(:luca).cached_posts.find(ids)
64
45
  end
65
46
 
66
47
  def test_should_use_cache_when_fetch_first_from_collection
67
- cache.expects(:fetch).with("#{cache_key}/cached_posts").returns association_proxy
68
- # cache.expects(:read).with("#{cache_key}/cached_posts").returns posts_by_author(:luca)
69
-
70
- assert_equal [ posts_by_author(:luca).first ],
71
- authors(:luca).cached_posts.first(1)
48
+ cache.expects(:read).with("#{cache_key}/cached_posts").returns association_proxy
49
+ assert_equal [ posts_by_author(:luca).first ], authors(:luca).cached_posts.first(1)
72
50
  end
73
51
 
74
52
  def test_should_use_cache_when_fetch_last_from_collection
75
- cache.expects(:fetch).with("#{cache_key}/cached_posts").returns association_proxy
76
- # cache.expects(:read).with("#{cache_key}/cached_posts").returns posts_by_author(:luca)
77
-
78
- assert_equal [ posts_by_author(:luca).last ],
79
- authors(:luca).cached_posts.last(1)
53
+ cache.expects(:read).with("#{cache_key}/cached_posts").returns association_proxy
54
+ assert_equal [ posts_by_author(:luca).last ], authors(:luca).cached_posts.last(1)
80
55
  end
81
56
 
82
- def test_should_use_cache_when_reset_collection
83
- cache.expects(:fetch).with("#{cache_key}/cached_posts").times(2).returns association_proxy
84
-
57
+ def test_should_unload_cache_when_reset_collection
58
+ cache.expects(:read).with("#{cache_key}/cached_posts").times(2).returns association_proxy
85
59
  assert_false authors(:luca).cached_posts.reset
86
60
  assert_equal posts_by_author(:luca), authors(:luca).cached_posts
87
61
  end
88
62
 
89
- def test_should_expire_cache_when_delete_all_elements_from_collection
90
- cache.expects(:fetch).with("#{cache_key}/cached_posts").times(2).returns association_proxy
91
- # cache.expects(:read).with("#{cache_key}/cached_posts").returns posts_by_author(:luca)
92
-
93
- authors(:luca).cached_posts.delete_all
94
- assert_equal posts_by_author(:luca), authors(:luca).cached_posts
95
- end
96
-
97
- def test_should_expire_cache_when_destroy_all_elements_from_collection
98
- cache.expects(:fetch).with("#{cache_key}/cached_posts").times(2).returns association_proxy
99
- # cache.expects(:read).with("#{cache_key}/cached_posts").returns posts_by_author(:luca)
100
-
101
- authors(:luca).cached_posts.destroy_all
102
- assert_equal posts_by_author(:luca), authors(:luca).cached_posts
103
- end
104
-
105
- def test_should_use_cache_on_collection_sum
106
- cache.expects(:fetch).with("#{blogs(:weblog).cache_key}/authors").returns authors_association_proxy
107
-
108
- assert_equal authors_by_blog(:weblog).map(&:age).sum,
109
- blogs(:weblog).authors.sum(:age)
63
+ def test_should_not_use_cache_on_collection_sum
64
+ # calculations aren't supported for now
65
+ cache.expects(:read).with("#{blogs(:weblog).cache_key}/authors").never
66
+ assert_equal authors_by_blog(:weblog).map(&:age).sum, blogs(:weblog).authors.sum(:age)
110
67
  end
111
68
 
112
69
  def test_should_not_use_cache_on_false_cached_option
113
- cache.expects(:fetch).never
70
+ cache.expects(:read).never
114
71
  authors(:luca).posts
115
72
  authors(:luca).posts(true) # force reload
116
73
  end
117
74
 
118
75
  def test_should_cache_associated_objects
119
- cache.expects(:fetch).with("#{cache_key}/cached_posts").times(2).returns(posts_by_author(:luca))
120
-
76
+ cache.expects(:read).with("#{cache_key}/cached_posts").times(2).returns(posts_by_author(:luca))
121
77
  posts = authors(:luca).cached_posts
122
78
  assert_equal posts, authors(:luca).cached_posts
123
79
  end
80
+
81
+ def test_should_safely_use_pagination
82
+ # pagination for now bypass cache and using database.
83
+ # the expectation is due to #cached_posts invocation.
84
+ cache.expects(:read).with("#{cache_key}/cached_posts").returns association_proxy
85
+ posts = authors(:luca).cached_posts.paginate(:all, :page => 1, :per_page => 1)
86
+ assert_equal [ posts_by_author(:luca).first ], posts
87
+ end
124
88
 
125
89
  def test_should_reload_association_and_refresh_the_cache_on_force_reload
126
- cache.expects(:fetch).with("#{cache_key}/cached_posts").times(2).returns(posts_by_author(:luca))
127
-
90
+ cache.expects(:read).with("#{cache_key}/cached_posts").times(2).returns(posts_by_author(:luca))
91
+ cache.expects(:write).times(3).returns true
128
92
  reloaded_posts = authors(:luca).cached_posts(true)
129
93
  assert_equal reloaded_posts, authors(:luca).cached_posts
130
94
  end
131
95
 
132
96
  def test_should_cache_associated_ids
133
- cache.expects(:fetch).with("#{cache_key}/cached_posts").times(2).returns association_proxy
134
- cache.expects(:fetch).with("#{cache_key}/cached_post_ids").times(2).returns(posts_by_author(:luca).map(&:id))
135
- ids = authors(:luca).cached_post_ids
97
+ ids = posts_by_author(:luca).map(&:id)
98
+ cache.expects(:read).with("#{cache_key}/cached_posts").returns(posts_by_author(:luca))
99
+ cache.expects(:fetch).with("#{cache_key}/cached_post_ids").returns ids
136
100
  assert_equal ids, authors(:luca).cached_post_ids
137
101
  end
138
102
 
@@ -142,94 +106,67 @@ class HasManyAssociationTest < Test::Unit::TestCase
142
106
  end
143
107
 
144
108
  def test_should_cache_all_eager_loaded_objects
145
- cache.expects(:fetch).with("#{cache_key}/cached_posts_with_comments").times(2).returns(posts_by_author(:luca, true))
109
+ cache.expects(:read).with("#{cache_key}/cached_posts_with_comments").returns(posts_by_author(:luca, true))
146
110
  posts = authors(:luca).cached_posts_with_comments
147
111
  assert_equal posts, authors(:luca).cached_posts_with_comments
148
112
  end
149
113
 
150
114
  def test_should_not_cache_eager_loaded_objects_on_false_cached_option
151
- cache.expects(:fetch).never
115
+ cache.expects(:read).never
152
116
  authors(:luca).posts_with_comments
153
117
  end
154
118
 
155
119
  def test_should_cache_polymorphic_associations
156
- cache.expects(:fetch).with("#{posts(:cached_models).cache_key}/cached_tags").times(2).returns(tags_by_post(:cached_models))
120
+ cache.expects(:read).with("#{posts(:cached_models).cache_key}/cached_tags").returns(tags_by_post(:cached_models))
157
121
  tags = posts(:cached_models).cached_tags
158
122
  assert_equal tags, posts(:cached_models).cached_tags
159
123
  end
160
124
 
161
125
  def test_should_not_cache_polymorphic_associations_on_false_cached_option
162
- cache.expects(:fetch).never
126
+ cache.expects(:read).never
163
127
  posts(:cached_models).tags
164
128
  end
165
129
 
166
130
  def test_should_cache_habtm_associations
167
- cache.expects(:fetch).with("#{cache_key}/cached_comments").times(2).returns(comments_by_author(:luca))
131
+ cache.expects(:read).with("#{cache_key}/cached_comments").returns(comments_by_author(:luca))
168
132
  comments = authors(:luca).cached_comments
169
133
  assert_equal comments, authors(:luca).cached_comments
170
134
  end
171
135
 
172
136
  def test_should_not_cache_habtm_associations_on_false_cached_option
173
- cache.expects(:fetch).never
137
+ cache.expects(:read).never
174
138
  authors(:luca).comments
175
139
  end
176
140
 
177
141
  def test_should_refresh_cache_when_associated_elements_change
178
- cache.expects(:fetch).with("#{cache_key}/cached_posts").times(2).returns association_proxy
179
-
142
+ cache.expects(:read).with("#{cache_key}/cached_posts").never
143
+ cache.expects(:delete).with("#{cache_key}/cached_posts").returns true
180
144
  post = authors(:luca).cached_posts.last # force cache loading and fetch a post
181
145
  post.update_attributes :title => 'Cached Models!'
182
-
183
146
  assert_equal posts_by_author(:luca), authors(:luca).cached_posts
184
147
  end
185
148
 
186
149
  def test_should_refresh_cache_when_pushing_element_to_association
187
- cache.expects(:fetch).with("#{cache_key}/cached_posts").times(2).returns association_proxy
150
+ cache.expects(:read).with("#{cache_key}/cached_posts").times(2).returns association_proxy
188
151
  cache.expects(:write).with("#{cache_key}/cached_posts", association_proxy).returns true
189
-
190
152
  post = create_post :author_id => nil
191
153
  authors(:luca).cached_posts << post
192
-
193
- assert_equal posts_by_author(:luca), authors(:luca).cached_posts
194
- end
195
-
196
- def test_should_refresh_caches_when_pushing_element_to_association_belonging_to_another_model
197
- cache.expects(:fetch).with("#{authors(:chuck).cache_key}/cached_posts").times(2).returns association_proxy(:chuck)
198
- cache.expects(:fetch).with("#{cache_key}/cached_posts").times(2).returns association_proxy
199
-
200
- post = authors(:chuck).cached_posts.last
201
- authors(:luca).cached_posts << post
202
-
203
154
  assert_equal posts_by_author(:luca), authors(:luca).cached_posts
204
- assert_equal posts_by_author(:chuck), authors(:chuck).cached_posts
205
- end
206
-
207
- def test_should_refresh_caches_when_pushing_element_to_polymorphic_association_belonging_to_another_model
208
- cache.expects(:fetch).with("#{posts(:welcome).cache_key}/cached_tags").times(2).returns tags_association_proxy
209
- cache.expects(:fetch).with("#{posts(:cached_models).cache_key}/cached_tags").times(2).returns tags_association_proxy(:cached_models)
210
- tag = posts(:welcome).cached_tags.last
211
-
212
- posts(:cached_models).cached_tags << tag
213
-
214
- # NOTE for some weird reason the assertion fails, even if the collections are equals.
215
- # I forced the comparision between the ids.
216
- assert_equal tags_by_post(:cached_models).map(&:id).sort,
217
- posts(:cached_models).cached_tags.map(&:id).sort
218
- assert_equal tags_by_post(:welcome), posts(:welcome).cached_tags
219
155
  end
220
156
 
221
157
  def test_should_not_use_cache_when_pushing_element_to_association_on_false_cached_option
222
158
  cache.expects(:write).never
223
-
224
159
  post = create_post :author_id => nil
225
160
  authors(:luca).posts << post
226
161
  end
227
162
 
228
163
  def test_should_not_use_cache_when_pushing_element_to_association_belonging_to_anotner_model_on_false_cached_option
229
164
  cache.expects(:delete).with("#{blogs(:weblog).cache_key}/posts").never
165
+ cache.expects(:delete).with("#{cache_key}/cached_posts_with_comments").never
166
+ cache.expects(:delete).with("#{cache_key}/cached_posts").returns true
167
+ cache.expects(:delete).with("#{posts(:cached_models).cache_key}/cached_tags").returns true
230
168
  post = blogs(:weblog).posts.last
231
169
  blogs(:blog).posts << post
232
-
233
170
  assert_equal posts_by_blog(:blog), blogs(:blog).posts
234
171
  end
235
172
 
@@ -237,109 +174,126 @@ class HasManyAssociationTest < Test::Unit::TestCase
237
174
  cache.expects(:delete).never
238
175
  tag = posts(:welcome).tags.last
239
176
  posts(:cached_models).tags << tag
240
-
241
177
  assert_equal tags_by_post(:cached_models), posts(:cached_models).tags
242
178
  end
243
-
244
- def test_should_update_cache_when_pushing_element_with_build
245
- cache.expects(:fetch).with("#{cache_key}/cached_posts").times(2).returns association_proxy
246
179
 
180
+ def test_should_update_cache_when_directly_assigning_a_new_collection
181
+ posts = [ posts_by_author(:luca).first ]
182
+ cache.expects(:read).with("#{cache_key}/cached_posts").times(2).returns association_proxy
183
+ authors(:luca).cached_posts = posts
184
+ assert_equal posts_by_author(:luca), authors(:luca).cached_posts
185
+ end
186
+
187
+ def test_should_use_cache_for_collection_size
188
+ cache.expects(:read).with("#{cache_key}/cached_posts").returns association_proxy
189
+ assert_equal posts_by_author(:luca).size, authors(:luca).cached_posts.size
190
+ end
191
+
192
+ def test_should_use_cache_and_return_uniq_records_for_collection_size_on_uniq_option
193
+ cache.expects(:read).with("#{cache_key}/uniq_cached_posts").never # wuh?!
194
+ assert_equal posts_by_author(:luca).size, authors(:luca).uniq_cached_posts.size
195
+ end
196
+
197
+ def test_should_use_cache_for_collection_length
198
+ cache.expects(:read).with("#{cache_key}/cached_posts").returns association_proxy
199
+ assert_equal posts_by_author(:luca).length, authors(:luca).cached_posts.length
200
+ end
201
+
202
+ def test_should_use_cache_for_collection_empty
203
+ cache.expects(:read).with("#{cache_key}/cached_posts").returns association_proxy
204
+ assert_equal posts_by_author(:luca).empty?, authors(:luca).cached_posts.empty?
205
+ end
206
+
207
+ def test_should_use_cache_for_collection_any
208
+ cache.expects(:read).with("#{cache_key}/cached_posts").returns association_proxy
209
+ assert_equal posts_by_author(:luca).any?, authors(:luca).cached_posts.any?
210
+ end
211
+
212
+ def test_should_use_cache_for_collection_include
213
+ cache.expects(:read).with("#{cache_key}/cached_posts").returns association_proxy
214
+ post = posts_by_author(:luca).first
215
+ assert authors(:luca).cached_posts.include?(post)
216
+ end
217
+ end
218
+
219
+ uses_memcached 'HasManyAssociationTest' do
220
+ def test_should_refresh_caches_when_pushing_element_to_association_belonging_to_another_model
221
+ post = authors(:chuck).cached_posts.last
222
+ authors(:luca).cached_posts << post
223
+ assert_equal posts_by_author(:luca), authors(:luca).cached_posts
224
+ assert_equal posts_by_author(:chuck), authors(:chuck).cached_posts
225
+ end
226
+
227
+ def test_should_refresh_caches_when_pushing_element_to_polymorphic_association_belonging_to_another_model
228
+ tag = posts(:welcome).cached_tags.last
229
+ posts(:cached_models).cached_tags << tag
230
+
231
+ # NOTE for some weird reason the assertion fails, even if the collections are equals.
232
+ # I forced the comparision between the ids.
233
+ assert_equal tags_by_post(:cached_models).map(&:id).sort,
234
+ posts(:cached_models).cached_tags.map(&:id).sort
235
+ end
236
+
237
+ def test_should_update_cache_when_pushing_element_with_build
247
238
  author = authors(:luca)
248
239
  post = author.cached_posts.build post_options
249
240
  post.save
250
-
251
241
  assert_equal posts_by_author(:luca), author.cached_posts
252
242
  end
253
-
254
- def test_should_update_cache_when_pushing_element_with_create
255
- cache.expects(:fetch).with("#{cache_key}/cached_posts").times(2).returns association_proxy
256
243
 
244
+ def test_should_update_cache_when_pushing_element_with_create
257
245
  author = authors(:luca)
258
246
  author.cached_posts.create post_options(:title => "CM Overview")
259
-
260
247
  assert_equal posts_by_author(:luca), author.cached_posts
261
248
  end
262
249
 
263
250
  def test_should_update_cache_when_pushing_element_with_create_bang_method
264
- cache.expects(:fetch).with("#{cache_key}/cached_posts").times(2).returns association_proxy
265
-
266
251
  author = authors(:luca)
267
252
  author.cached_posts.create! post_options(:title => "CM Overview!!")
268
-
269
253
  assert_equal posts_by_author(:luca), author.cached_posts
270
254
  end
271
255
 
272
- def test_should_update_cache_when_deleting_element_from_collection
273
- cache.expects(:fetch).with("#{cache_key}/cached_posts").times(2).returns association_proxy
256
+ def test_should_expire_cache_when_delete_all_elements_from_collection
257
+ authors(:luca).cached_posts.delete_all
258
+ assert_equal posts_by_author(:luca), authors(:luca).cached_posts
259
+ end
274
260
 
275
- authors(:luca).cached_posts.delete(posts_by_author(:luca).first)
261
+ def test_should_expire_cache_when_destroy_all_elements_from_collection
262
+ authors(:luca).cached_posts.destroy_all
276
263
  assert_equal posts_by_author(:luca), authors(:luca).cached_posts
277
264
  end
278
265
 
279
266
  def test_should_update_cache_when_clearing_collection
280
- cache.expects(:fetch).with("#{cache_key}/cached_posts").times(2).returns association_proxy
281
- authors(:luca).cached_posts.clear
282
-
267
+ authors(:luca).cached_posts.clear
283
268
  assert_equal posts_by_author(:luca), authors(:luca).cached_posts
284
269
  end
285
270
 
286
271
  def test_should_update_cache_when_clearing_collection_with_dependent_destroy_option
287
- cache.expects(:fetch).with("#{cache_key}/cached_dependent_posts").times(2).returns association_proxy
288
272
  authors(:luca).cached_dependent_posts.clear
289
-
290
273
  assert_equal posts_by_author(:luca), authors(:luca).cached_dependent_posts
291
274
  end
292
-
293
- def test_should_update_cache_when_directly_assigning_a_new_collection
294
- posts = [ posts_by_author(:luca).first ]
295
- cache.expects(:fetch).with("#{cache_key}/cached_posts").times(2).returns association_proxy
296
- authors(:luca).cached_posts = posts
297
275
 
276
+ def test_should_update_cache_when_deleting_element_from_collection
277
+ authors(:luca).cached_posts.delete(posts_by_author(:luca).first)
298
278
  assert_equal posts_by_author(:luca), authors(:luca).cached_posts
299
279
  end
300
280
 
301
281
  def test_should_update_cache_when_replace_collection
302
282
  post = create_post; post.save
303
283
  posts = [ posts_by_author(:luca).first, post ]
304
- cache.expects(:fetch).with("#{cache_key}/cached_posts").times(2).returns association_proxy
305
284
  authors(:luca).cached_posts.replace(posts)
306
-
307
285
  assert_equal posts_by_author(:luca), authors(:luca).cached_posts
308
286
  end
309
287
 
310
- def test_should_use_cache_for_collection_size
311
- # cache.expects(:fetch).with("#{cache_key}/cached_posts").times(2).returns association_proxy
312
-
313
- assert_equal posts_by_author(:luca).size,
314
- authors(:luca).cached_posts.size
315
- end
316
-
317
- def test_should_use_cache_for_collection_length
318
- cache.expects(:fetch).with("#{cache_key}/cached_posts").returns association_proxy
319
-
320
- assert_equal posts_by_author(:luca).length,
321
- authors(:luca).cached_posts.length
322
- end
323
-
324
- def test_should_use_cache_for_collection_empty
325
- cache.expects(:fetch).with("#{cache_key}/cached_posts").returns association_proxy
326
-
327
- assert_equal posts_by_author(:luca).empty?,
328
- authors(:luca).cached_posts.empty?
329
- end
330
-
331
- def test_should_use_cache_for_collection_any
332
- cache.expects(:fetch).with("#{cache_key}/cached_posts").returns association_proxy
333
-
334
- assert_equal posts_by_author(:luca).any?,
335
- authors(:luca).cached_posts.any?
336
- end
288
+ def test_should_not_expire_cache_on_update_on_missing_updated_at
289
+ author = authors(:luca)
290
+ old_cache_key = author.cache_key
337
291
 
338
- def test_should_use_cache_for_collection_include
339
- cache.expects(:fetch).with("#{cache_key}/cached_posts").returns association_proxy
292
+ author.cached_posts # force cache loading
293
+ author.update_attributes :first_name => author.first_name.upcase
340
294
 
341
- post = posts_by_author(:luca).first
342
- assert authors(:luca).cached_posts.include?(post)
295
+ # assert_not_equal old_cache_key, author.cache_key
296
+ assert_equal posts_by_author(:luca), authors(:luca).cached_posts
343
297
  end
344
298
  end
345
299
 
@@ -0,0 +1,12 @@
1
+ require File.dirname(__FILE__) + '/../../test_helper'
2
+
3
+ class HasOneAssociationTest < Test::Unit::TestCase
4
+ include ActiveRecord::Associations
5
+
6
+ def test_should_not_raise_exception_when_use_has_one
7
+ assert_nothing_raised ArgumentError do
8
+ authors(:luca).address
9
+ addresses(:luca).author
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,7 @@
1
+ luca:
2
+ author_id: 1
3
+ street: 1 infinite loop
4
+ zip: 95014
5
+ city: Cupertino
6
+ state: California
7
+ country: United States
@@ -0,0 +1,3 @@
1
+ announcements:
2
+ id: 1
3
+ name: Announcements
@@ -0,0 +1,3 @@
1
+ welcome_announcements:
2
+ category_id: 1
3
+ post_id: 1
@@ -0,0 +1,3 @@
1
+ class Address < ActiveRecord::Base
2
+ belongs_to :author
3
+ end
@@ -2,9 +2,11 @@ class Author < ActiveRecord::Base
2
2
  belongs_to :blog, :cached => true
3
3
  has_many :posts
4
4
  has_many :cached_posts, :cached => true, :class_name => 'Post'
5
+ has_many :uniq_cached_posts, :cached => true, :class_name => 'Post', :uniq => true
5
6
  has_many :cached_dependent_posts, :cached => true, :class_name => 'Post', :dependent => :destroy
6
7
  has_many :posts_with_comments, :class_name => 'Post', :include => :comments
7
8
  has_many :cached_posts_with_comments, :class_name => 'Post', :include => :comments, :cached => true
8
9
  has_many :comments, :through => :posts
9
10
  has_many :cached_comments, :through => :posts, :source => :comments, :cached => true
11
+ has_one :address
10
12
  end
@@ -0,0 +1,3 @@
1
+ class Category < ActiveRecord::Base
2
+ has_and_belongs_to_many :posts
3
+ end
@@ -4,4 +4,5 @@ class Post < ActiveRecord::Base
4
4
  has_many :comments
5
5
  has_many :tags, :as => :taggable
6
6
  has_many :cached_tags, :as => :taggable, :class_name => 'Tag', :cached => true
7
+ has_and_belongs_to_many :categories
7
8
  end
@@ -1,4 +1,4 @@
1
- ENV["RAILS_ENV"] = "test"
1
+ RAILS_ENV = "test" unless defined? RAILS_ENV
2
2
 
3
3
  require 'test/unit'
4
4
  require 'rubygems'
@@ -17,6 +17,28 @@ require 'post'
17
17
  Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures"
18
18
  ActionController::IntegrationTest.fixture_path = Test::Unit::TestCase.fixture_path
19
19
 
20
+ module WillPaginate #:nodoc:
21
+ def paginate(*args)
22
+ options = args.extract_options!
23
+ current_page, per_page = options[:page], options[:per_page]
24
+ offset = (current_page - 1) * per_page
25
+
26
+ count_options = options.except :page, :per_page
27
+ find_options = count_options.except(:count).update(:offset => offset, :limit => per_page)
28
+
29
+ args << find_options
30
+ @reflection.klass.find(*args)
31
+ end
32
+ end
33
+
34
+ module ActiveRecord
35
+ module Associations
36
+ class AssociationCollection < AssociationProxy #:nodoc:
37
+ include WillPaginate
38
+ end
39
+ end
40
+ end
41
+
20
42
  class Test::Unit::TestCase
21
43
  self.use_transactional_fixtures = true
22
44
  self.use_instantiated_fixtures = false
@@ -40,3 +62,27 @@ def uses_mocha(description)
40
62
  rescue LoadError
41
63
  $stderr.puts "Skipping #{description} tests. `gem install mocha` and try again."
42
64
  end
65
+
66
+ def uses_memcached(description)
67
+ require 'memcache'
68
+ MemCache.new('localhost').stats
69
+ yield
70
+ rescue MemCache::MemCacheError
71
+ $stderr.puts "Skipping #{description} tests. Start memcached and try again."
72
+ end
73
+
74
+ if ENV['SKIP_MOCHA'] == 'true'
75
+ class Object
76
+ def expects(*args)
77
+ self
78
+ end
79
+
80
+ def method_missing(method_name, *args, &block)
81
+ end
82
+ end
83
+
84
+ class NilClass
85
+ def method_missing(method_name, *args, &block)
86
+ end
87
+ end
88
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cached-models
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Luca Guidi
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-10-10 00:00:00 +02:00
12
+ date: 2008-10-22 00:00:00 +02:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -60,15 +60,22 @@ files:
60
60
  - lib/cached_models.rb
61
61
  - setup.rb
62
62
  - tasks/cached_models_tasks.rake
63
+ - test/active_record/associations/has_and_belongs_to_many_association_test.rb
63
64
  - test/active_record/associations/has_many_association_test.rb
65
+ - test/active_record/associations/has_one_association_test.rb
64
66
  - test/active_record/base_test.rb
67
+ - test/fixtures/addresses.yml
65
68
  - test/fixtures/authors.yml
66
69
  - test/fixtures/blogs.yml
70
+ - test/fixtures/categories.yml
71
+ - test/fixtures/categories_posts.yml
67
72
  - test/fixtures/comments.yml
68
73
  - test/fixtures/posts.yml
69
74
  - test/fixtures/tags.yml
75
+ - test/models/address.rb
70
76
  - test/models/author.rb
71
77
  - test/models/blog.rb
78
+ - test/models/category.rb
72
79
  - test/models/comment.rb
73
80
  - test/models/post.rb
74
81
  - test/models/tag.rb
@@ -101,5 +108,7 @@ signing_key:
101
108
  specification_version: 2
102
109
  summary: Transparent caching policy for your models
103
110
  test_files:
111
+ - test/active_record/associations/has_and_belongs_to_many_association_test.rb
104
112
  - test/active_record/associations/has_many_association_test.rb
113
+ - test/active_record/associations/has_one_association_test.rb
105
114
  - test/active_record/base_test.rb