Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add touch plugin, which adds Model#touch for updating an instance's t…
…imestamp, as well as touching associations when an instance is updated or destroyed The touch plugin adds a touch method to model instances, which saves the object with a modified timestamp. By default, it uses the :updated_at column, but you can set which column to use. It also supports touching of associations, so that when the current model object is updated or destroyed, the associated rows in the database can have their modified timestamp updated to the current timestamp. Since the instance touch method works on model instances, it uses Time.now for the timestamp. The association touching works on datasets, so it updates all related rows in a single query, using the SQL standard CURRENT_TIMESTAMP. Both of these can be overridden easily if necessary.
- Loading branch information
1 parent
bc59602
commit 400a917
Showing
4 changed files
with
276 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
module Sequel | ||
module Plugins | ||
# The touch plugin adds a touch method to model instances, which saves | ||
# the object with a modified timestamp. By default, it uses the | ||
# :updated_at column, but you can set which column to use. | ||
# It also supports touching of associations, so that when the current | ||
# model object is updated or destroyed, the associated rows in the | ||
# database can have their modified timestamp updated to the current | ||
# timestamp. | ||
# | ||
# Since the instance touch method works on model instances, | ||
# it uses Time.now for the timestamp. The association touching works | ||
# on datasets, so it updates all related rows in a single query, using | ||
# the SQL standard CURRENT_TIMESTAMP. Both of these can be overridden | ||
# easily if necessary. | ||
module Touch | ||
# The default column to update when touching | ||
TOUCH_COLUMN_DEFAULT = :updated_at | ||
|
||
# Set the touch_column and touched_associations variables for the model. | ||
# Options: | ||
# * :associations - The associations to touch when a model instance is | ||
# updated or destroyed. Can be a symbol for a single association, | ||
# a hash with association keys and column values, or an array of | ||
# symbols and/or hashes. If a symbol is used, the column used | ||
# when updating the associated objects is the model's touch_column. | ||
# If a hash is used, the value is used as the column to update. | ||
# * :column - The column to modify when touching a model instance. | ||
def self.configure(model, opts={}) | ||
model.touch_column = opts[:column] || TOUCH_COLUMN_DEFAULT if opts[:column] || !model.touch_column | ||
model.instance_variable_set(:@touched_associations, {}) | ||
model.touch_associations(opts[:associations]) if opts[:associations] | ||
end | ||
|
||
module ClassMethods | ||
# The column to modify when touching a model instance, as a symbol. Also used | ||
# as the default column when touching associations, if | ||
# the associations don't specify a column. | ||
attr_accessor :touch_column | ||
|
||
# A hash specifying the associations to touch when instances are | ||
# updated or destroyed. Keys are association dataset method name symbols and values | ||
# are column name symbols. | ||
attr_reader :touched_associations | ||
|
||
# Set the touch_column for the subclass to be the same as the current class. | ||
# Also, create a copy of the touched_associations in the subclass. | ||
def inherited(subclass) | ||
super | ||
subclass.touch_column = touch_column | ||
subclass.instance_variable_set(:@touched_associations, touched_associations.dup) | ||
end | ||
|
||
# Add additional associations to be touched. See the :association option | ||
# of the Sequel::Plugin::Touch.configure method for the format of the associations | ||
# arguments. | ||
def touch_associations(*associations) | ||
associations.flatten.each do |a| | ||
a = {a=>touch_column} if a.is_a?(Symbol) | ||
a.each do |k,v| | ||
raise(Error, "invalid association: #{k}") unless r = association_reflection(k) | ||
touched_associations[r.dataset_method] = v | ||
end | ||
end | ||
end | ||
end | ||
|
||
module InstanceMethods | ||
# Touch all of the model's touched_associations when destroying the object. | ||
def after_destroy | ||
super | ||
touch_associations | ||
end | ||
|
||
# Touch all of the model's touched_associations when updating the object. | ||
def after_update | ||
super | ||
touch_associations | ||
end | ||
|
||
# Touch the model object. If a column is not given, use the model's touch_column | ||
# as the column. If the column to use is not one of the model's columns, just | ||
# save the changes to the object instead of attempting to a value that doesn't | ||
# exist. | ||
def touch(column=nil) | ||
if column | ||
set(column=>touch_instance_value) | ||
else | ||
column = model.touch_column | ||
set(column=>touch_instance_value) if columns.include?(column) | ||
end | ||
save_changes | ||
end | ||
|
||
private | ||
|
||
# The value to use when modifying the touch column for the association datasets. Uses | ||
# the SQL standard CURRENT_TIMESTAMP. | ||
def touch_association_value | ||
Sequel::CURRENT_TIMESTAMP | ||
end | ||
|
||
# Directly update the database using the association dataset for each association. | ||
def touch_associations | ||
model.touched_associations.each do |meth, column| | ||
send(meth).update(column=>touch_association_value) | ||
end | ||
end | ||
|
||
# The value to use when modifying the touch column for the model instance. | ||
# Uses Time.now to work well with typecasting. | ||
def touch_instance_value | ||
Time.now | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
require File.join(File.dirname(__FILE__), "spec_helper") | ||
|
||
describe "Touch plugin" do | ||
before do | ||
@c = Class.new(Sequel::Model) do | ||
def _refresh(*); end | ||
end | ||
p = proc{def touch_instance_value; touch_association_value; end} | ||
@Artist = Class.new(@c, &p).set_dataset(:artists) | ||
@Album = Class.new(@c, &p).set_dataset(:albums) | ||
|
||
@Artist.columns :id, :updated_at, :modified_on | ||
@Artist.one_to_many :albums, :class=>@Album, :key=>:artist_id | ||
|
||
@Album.columns :id, :updated_at, :modified_on, :artist_id, :original_album_id | ||
@Album.one_to_many :followup_albums, :class=>@Album, :key=>:original_album_id | ||
@Album.many_to_one :artist, :class=>@Artist | ||
|
||
@a = @Artist.load(:id=>1) | ||
MODEL_DB.reset | ||
end | ||
|
||
specify "should default to using Time.now when setting the column values for model instances" do | ||
c = Class.new(Sequel::Model).set_dataset(:a) | ||
c.plugin :touch | ||
c.columns :id, :updated_at | ||
c.load(:id=>1).touch | ||
MODEL_DB.sqls.first.should =~ /UPDATE a SET updated_at = '[-0-9 :.]+' WHERE \(id = 1\)/ | ||
end | ||
|
||
specify "should allow #touch instance method for updating the updated_at column" do | ||
@Artist.plugin :touch | ||
@a.touch | ||
MODEL_DB.sqls.should == ["UPDATE artists SET updated_at = CURRENT_TIMESTAMP WHERE (id = 1)"] | ||
end | ||
|
||
specify "should have #touch take an argument for the column to touch" do | ||
@Artist.plugin :touch | ||
@a.touch(:modified_on) | ||
MODEL_DB.sqls.should == ["UPDATE artists SET modified_on = CURRENT_TIMESTAMP WHERE (id = 1)"] | ||
end | ||
|
||
specify "should be able to specify the default column to touch in the plugin call using the :column option" do | ||
@Artist.plugin :touch, :column=>:modified_on | ||
@a.touch | ||
MODEL_DB.sqls.should == ["UPDATE artists SET modified_on = CURRENT_TIMESTAMP WHERE (id = 1)"] | ||
end | ||
|
||
specify "should be able to specify the default column to touch using the touch_column model accessor" do | ||
@Artist.plugin :touch | ||
@Artist.touch_column = :modified_on | ||
@a.touch | ||
MODEL_DB.sqls.should == ["UPDATE artists SET modified_on = CURRENT_TIMESTAMP WHERE (id = 1)"] | ||
end | ||
|
||
specify "should be able to specify the associations to touch in the plugin call using the :associations option" do | ||
@Artist.plugin :touch, :associations=>:albums | ||
@a.touch | ||
MODEL_DB.sqls.should == ["UPDATE artists SET updated_at = CURRENT_TIMESTAMP WHERE (id = 1)", | ||
"UPDATE albums SET updated_at = CURRENT_TIMESTAMP WHERE (albums.artist_id = 1)"] | ||
end | ||
|
||
specify "should be able to give an array to the :associations option specifying multiple associations" do | ||
@Album.plugin :touch, :associations=>[:artist, :followup_albums] | ||
@Album.load(:id=>4, :artist_id=>1).touch | ||
MODEL_DB.sqls.shift.should == "UPDATE albums SET updated_at = CURRENT_TIMESTAMP WHERE (id = 4)" | ||
MODEL_DB.sqls.sort.should == ["UPDATE albums SET updated_at = CURRENT_TIMESTAMP WHERE (albums.original_album_id = 4)", | ||
"UPDATE artists SET updated_at = CURRENT_TIMESTAMP WHERE (artists.id = 1)"] | ||
end | ||
|
||
specify "should be able to give a hash to the :associations option specifying the column to use for each association" do | ||
@Artist.plugin :touch, :associations=>{:albums=>:modified_on} | ||
@a.touch | ||
MODEL_DB.sqls.should == ["UPDATE artists SET updated_at = CURRENT_TIMESTAMP WHERE (id = 1)", | ||
"UPDATE albums SET modified_on = CURRENT_TIMESTAMP WHERE (albums.artist_id = 1)"] | ||
end | ||
|
||
specify "should default to using the touch_column as the default touch column for associations" do | ||
@Artist.plugin :touch, :column=>:modified_on, :associations=>:albums | ||
@a.touch | ||
MODEL_DB.sqls.should == ["UPDATE artists SET modified_on = CURRENT_TIMESTAMP WHERE (id = 1)", | ||
"UPDATE albums SET modified_on = CURRENT_TIMESTAMP WHERE (albums.artist_id = 1)"] | ||
end | ||
|
||
specify "should allow the mixed use of symbols and hashes inside an array for the :associations option" do | ||
@Album.plugin :touch, :associations=>[:artist, {:followup_albums=>:modified_on}] | ||
@Album.load(:id=>4, :artist_id=>1).touch | ||
MODEL_DB.sqls.shift.should == "UPDATE albums SET updated_at = CURRENT_TIMESTAMP WHERE (id = 4)" | ||
MODEL_DB.sqls.sort.should == ["UPDATE albums SET modified_on = CURRENT_TIMESTAMP WHERE (albums.original_album_id = 4)", | ||
"UPDATE artists SET updated_at = CURRENT_TIMESTAMP WHERE (artists.id = 1)"] | ||
end | ||
|
||
specify "should be able to specify the associations to touch via a touch_associations_method" do | ||
@Album.plugin :touch | ||
@Album.touch_associations(:artist, {:followup_albums=>:modified_on}) | ||
@Album.load(:id=>4, :artist_id=>1).touch | ||
MODEL_DB.sqls.shift.should == "UPDATE albums SET updated_at = CURRENT_TIMESTAMP WHERE (id = 4)" | ||
MODEL_DB.sqls.sort.should == ["UPDATE albums SET modified_on = CURRENT_TIMESTAMP WHERE (albums.original_album_id = 4)", | ||
"UPDATE artists SET updated_at = CURRENT_TIMESTAMP WHERE (artists.id = 1)"] | ||
end | ||
|
||
specify "should touch associated objects when destroying an object" do | ||
@Album.plugin :touch | ||
@Album.touch_associations(:artist, {:followup_albums=>:modified_on}) | ||
@Album.load(:id=>4, :artist_id=>1).destroy | ||
MODEL_DB.sqls.shift.should == "DELETE FROM albums WHERE (id = 4)" | ||
MODEL_DB.sqls.sort.should == ["UPDATE albums SET modified_on = CURRENT_TIMESTAMP WHERE (albums.original_album_id = 4)", | ||
"UPDATE artists SET updated_at = CURRENT_TIMESTAMP WHERE (artists.id = 1)"] | ||
end | ||
|
||
specify "should not update a column that doesn't exist" do | ||
@Album.plugin :touch, :column=>:x | ||
a = @Album.load(:id=>1) | ||
a.touch | ||
MODEL_DB.sqls.should == [] | ||
a.artist_id = 1 | ||
a.touch | ||
MODEL_DB.sqls.should == ['UPDATE albums SET artist_id = 1 WHERE (id = 1)'] | ||
end | ||
|
||
specify "should raise an error if given a column argument in touch that doesn't exist" do | ||
@Artist.plugin :touch | ||
proc{@a.touch(:x)}.should raise_error(Sequel::Error) | ||
end | ||
|
||
specify "should raise an Error when a nonexistent association is given" do | ||
@Artist.plugin :touch | ||
proc{@Artist.plugin :touch, :associations=>:blah}.should raise_error(Sequel::Error) | ||
end | ||
|
||
specify "should work correctly in subclasses" do | ||
@Artist.plugin :touch | ||
c1 = Class.new(@Artist) | ||
c1.load(:id=>4).touch | ||
MODEL_DB.sqls.should == ["UPDATE artists SET updated_at = CURRENT_TIMESTAMP WHERE (id = 4)"] | ||
MODEL_DB.reset | ||
|
||
c1.touch_column = :modified_on | ||
c1.touch_associations :albums | ||
c1.load(:id=>1).touch | ||
MODEL_DB.sqls.should == ["UPDATE artists SET modified_on = CURRENT_TIMESTAMP WHERE (id = 1)", | ||
"UPDATE albums SET modified_on = CURRENT_TIMESTAMP WHERE (albums.artist_id = 1)"] | ||
MODEL_DB.reset | ||
|
||
@a.touch | ||
MODEL_DB.sqls.should == ["UPDATE artists SET updated_at = CURRENT_TIMESTAMP WHERE (id = 1)"] | ||
MODEL_DB.reset | ||
|
||
@Artist.plugin :touch, :column=>:modified_on, :associations=>:albums | ||
c2 = Class.new(@Artist) | ||
c2.load(:id=>4).touch | ||
MODEL_DB.sqls.should == ["UPDATE artists SET modified_on = CURRENT_TIMESTAMP WHERE (id = 4)", | ||
"UPDATE albums SET modified_on = CURRENT_TIMESTAMP WHERE (albums.artist_id = 4)"] | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters