require File.join(File.dirname(File.expand_path(__FILE__)), "spec_helper") describe Sequel::Model, "caching" do before do @cache_class = Class.new(Hash) do attr_accessor :ttl def set(k, v, ttl); self[k] = v; @ttl = ttl; end def get(k); self[k]; end end cache = @cache_class.new @cache = cache @memcached_class = Class.new(Hash) do attr_accessor :ttl def set(k, v, ttl); self[k] = v; @ttl = ttl; end def get(k); if self[k] then return self[k]; else raise ArgumentError; end end def delete(k); if self[k] then super; else raise ArgumentError; end end end cache2 = @memcached_class.new @memcached = cache2 @c = Class.new(Sequel::Model(:items)) @c.class_eval do plugin :caching, cache def self.name; 'Item' end columns :name, :id end @c3 = Class.new(Sequel::Model(:items)) @c3.class_eval do plugin :caching, cache2 def self.name; 'Item' end columns :name, :id end @c4 = Class.new(Sequel::Model(:items)) @c4.class_eval do plugin :caching, cache2, :ignore_exceptions => true def self.name; 'Item' end columns :name, :id end @c2 = Class.new(@c) do def self.name; 'SubItem' end end [@c, @c2, @c3, @c4].each do |c| c.dataset = c.dataset.with_fetch(:name => 'sharon'.dup, :id => 1).with_numrows(1) end @c.db.reset end it "should set the model's cache store" do @c.cache_store.wont_be_nil @c2.cache_store.wont_be_nil end it "should have a default ttl of 3600" do @c.cache_ttl.must_equal 3600 @c2.cache_ttl.must_equal 3600 end it "should take a ttl option" do c = Class.new(Sequel::Model(:items)) c.plugin :caching, @cache, :ttl => 1234 c.cache_ttl.must_equal 1234 Class.new(c).cache_ttl.must_equal 1234 end it "should allow overriding the ttl option via a plugin :caching call" do @c.plugin :caching, @cache, :ttl => 1234 @c.cache_ttl.must_equal 1234 Class.new(@c).cache_ttl.must_equal 1234 end it "should offer a set_cache_ttl method for setting the ttl" do @c.cache_ttl.must_equal 3600 @c.set_cache_ttl 1234 @c.cache_ttl.must_equal 1234 Class.new(@c).cache_ttl.must_equal 1234 end it "should generate a cache key appropriate to the class via the Model#cache_key" do m = @c.new m.values[:id] = 1 m.cache_key.must_equal "#{m.class}:1" m = @c2.new m.values[:id] = 1 m.cache_key.must_equal "#{m.class}:1" # custom primary key @c.set_primary_key :ttt m = @c.new m.values[:ttt] = 333 m.cache_key.must_equal "#{m.class}:333" c = Class.new(@c) m = c.new m.values[:ttt] = 333 m.cache_key.must_equal "#{m.class}:333" # composite primary key @c.set_primary_key [:a, :b, :c] m = @c.new m.values[:a] = 123 m.values[:c] = 456 m.values[:b] = 789 m.cache_key.must_equal "#{m.class}:123,789,456" c = Class.new(@c) m = c.new m.values[:a] = 123 m.values[:c] = 456 m.values[:b] = 789 m.cache_key.must_equal "#{m.class}:123,789,456" end it "should generate a cache key via the Model.cache_key method" do @c.cache_key(1).must_equal "#{@c}:1" @c.cache_key([1, 2]).must_equal "#{@c}:1,2" end it "should raise error if attempting to generate cache_key and primary key value is null" do m = @c.new proc {m.cache_key}.must_raise(Sequel::Error) m.values[:id] = 1 m.cache_key m = @c2.new proc {m.cache_key}.must_raise(Sequel::Error) m.values[:id] = 1 m.cache_key end it "should not raise error if trying to save a new record" do @c.new(:name=>'blah').save @c.create(:name=>'blah') @c2.new(:name=>'blah').save @c2.create(:name=>'blah') end it "should set the cache when reading from the database" do @c.db.sqls.must_equal [] @cache.must_be :empty? m = @c[1] @c.db.sqls.must_equal ['SELECT * FROM items WHERE id = 1'] m.values.must_equal(:name=>"sharon", :id=>1) @cache[m.cache_key].must_equal m m2 = @c[1] @c.db.sqls.must_equal [] m2.must_equal m m2.values.must_equal(:name=>"sharon", :id=>1) m = @c2[1] @c.db.sqls.must_equal ['SELECT * FROM items WHERE id = 1'] m.values.must_equal(:name=>"sharon", :id=>1) @cache[m.cache_key].must_equal m m2 = @c2[1] @c.db.sqls.must_equal [] m2.must_equal m m2.values.must_equal(:name=>"sharon", :id=>1) end it "should handle lookups by nil primary keys" do @c[nil].must_be_nil @c.db.sqls.must_equal [] end it "should delete the cache when writing to the database" do m = @c[1] @cache[m.cache_key].must_equal m m.name = 'hey' m.save @cache.has_key?(m.cache_key).must_equal false @c.db.sqls.must_equal ["SELECT * FROM items WHERE id = 1", "UPDATE items SET name = 'hey' WHERE (id = 1)"] m = @c2[1] @cache[m.cache_key].must_equal m m.name = 'hey' m.save @cache.has_key?(m.cache_key).must_equal false @c.db.sqls.must_equal ["SELECT * FROM items WHERE id = 1", "UPDATE items SET name = 'hey' WHERE (id = 1)"] end it "should delete the cache when deleting the record" do m = @c[1] @cache[m.cache_key].must_equal m m.delete @cache.has_key?(m.cache_key).must_equal false @c.db.sqls.must_equal ["SELECT * FROM items WHERE id = 1", "DELETE FROM items WHERE id = 1"] m = @c2[1] @cache[m.cache_key].must_equal m m.delete @cache.has_key?(m.cache_key).must_equal false @c.db.sqls.must_equal ["SELECT * FROM items WHERE id = 1", "DELETE FROM items WHERE id = 1"] end it "should support #[] as a shortcut to #find with hash" do m = @c[:id => 3] @cache[m.cache_key].must_be_nil @c.db.sqls.must_equal ["SELECT * FROM items WHERE (id = 3) LIMIT 1"] m = @c[1] @cache[m.cache_key].must_equal m @c.db.sqls.must_equal ["SELECT * FROM items WHERE id = 1"] @c[:id => 4] @c.db.sqls.must_equal ["SELECT * FROM items WHERE (id = 4) LIMIT 1"] m = @c2[:id => 3] @cache[m.cache_key].must_be_nil @c.db.sqls.must_equal ["SELECT * FROM items WHERE (id = 3) LIMIT 1"] m = @c2[1] @cache[m.cache_key].must_equal m @c.db.sqls.must_equal ["SELECT * FROM items WHERE id = 1"] @c2[:id => 4] @c.db.sqls.must_equal ["SELECT * FROM items WHERE (id = 4) LIMIT 1"] end it "should support ignore_exception option" do c = Class.new(Sequel::Model(:items)) c.plugin :caching, @cache, :ignore_exceptions => true Class.new(c).cache_ignore_exceptions.must_equal true end it "should raise an exception if cache_store is memcached and ignore_exception is not enabled" do proc{@c3[1]}.must_raise ArgumentError m = @c3.new.save proc{m.update({:name=>'blah'})}.must_raise ArgumentError end it "should rescue an exception if cache_store is memcached and ignore_exception is enabled" do @c4[1].values.must_equal(:name => 'sharon', :id => 1) @c4.dataset = @c4.dataset.with_fetch(:name => 'sharon', :id => 1, :x=>1) m = @c4.new.save m.update({:name=>'blah'}) m.values.must_equal(:name => 'blah', :id => 1, :x => 1) end it "should support Model.cache_get_pk for getting a value from the cache by primary key" do @c.cache_get_pk(1).must_be_nil m = @c[1] @c.cache_get_pk(1).must_equal m end it "should support Model.cache_delete_pk for removing a value from the cache by primary key" do @c[1] @c.cache_get_pk(1).wont_equal nil @c.cache_delete_pk(1).must_be_nil @c.cache_get_pk(1).must_be_nil end it "should support overriding the cache key prefix" do c2 = Class.new(@c) def c2.cache_key_prefix; "ceetwo" end c3 = Class.new(c2) @c.cache_key(:id).wont_equal c2.cache_key(:id) c2.cache_key(:id).must_equal c3.cache_key(:id) @c[1] c2.cache_get_pk(1).must_be_nil m = c2[1] c2.cache_get_pk(1).values.must_equal @c[1].values c3.cache_get_pk(1).values.must_equal m.values m.name << m.name m.save c2[1].values.must_equal c3[1].values end end