cached-models 0.0.2 → 0.0.3

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/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