Skip to content

Commit

Permalink
Add around hooks to Sequel::Model
Browse files Browse the repository at this point in the history
Around hooks wrap both the related before and after hooks.  In most
cases, before and after hooks are sufficient, but if you need to be
able to rescue exceptions raised by a before or after hook or
the wrapped behavior (e.g. the actual insert/update/delete), you
need to use an around hook.  For example, you could use an around
hook to turn DatabaseErrors caused by constraints into validation
errors, assuming you could correctly parse error messages from the
database.

This only contains the specs and code changes, the update to the
hook documentation will come soon.
  • Loading branch information
jeremyevans committed May 24, 2011
1 parent bd0e3fd commit 2221e28
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 35 deletions.
5 changes: 5 additions & 0 deletions lib/sequel/model.rb
Expand Up @@ -80,6 +80,11 @@ class Model
# +super+ on the first line of your method, so later hooks are called after earlier hooks.
AFTER_HOOKS = [:after_initialize, :after_create, :after_update, :after_save, :after_destroy, :after_validation]

# Hooks that are called around an action. If overridden, these methods must call super
# exactly once if the behavior they wrap is desired. The can be used to rescue exceptions
# raised by the code they wrap or ensure that some behavior is executed no matter what.
AROUND_HOOKS = [:around_create, :around_update, :around_save, :around_destroy, :around_validation]

# Empty instance methods to create that the user can override to get hook/callback behavior.
# Just like any other method defined by Sequel, if you override one of these, you should
# call +super+ to get the default behavior (while empty by default, they can also be defined
Expand Down
86 changes: 51 additions & 35 deletions lib/sequel/model/base.rb
Expand Up @@ -687,10 +687,10 @@ def set_columns(new_columns)

# Sequel::Model instance methods that implement basic model functionality.
#
# * All of the methods in +HOOKS+ create instance methods that are called
# * All of the methods in +HOOKS+ and +AROUND_HOOKS+ create instance methods that are called
# by Sequel when the appropriate action occurs. For example, when destroying
# a model object, Sequel will call +before_destroy+, do the destroy,
# and then call +after_destroy+.
# a model object, Sequel will call +around_destory+, which will call +before_destroy+, do
# the destroy, and then call +after_destroy+.
# * The following instance_methods all call the class method of the same
# name: columns, dataset, db, primary_key, db_schema.
# * All of the methods in +BOOLEAN_SETTINGS+ create attr_writers allowing you
Expand All @@ -699,6 +699,7 @@ def set_columns(new_columns)
# gets the default value from the class by calling the class method of the same name.
module InstanceMethods
HOOKS.each{|h| class_eval("def #{h}; end", __FILE__, __LINE__)}
AROUND_HOOKS.each{|h| class_eval("def #{h}; yield end", __FILE__, __LINE__)}

# Define instance method(s) that calls class method(s) of the
# same name, caching the result in an instance variable. Define
Expand Down Expand Up @@ -1187,12 +1188,14 @@ def validate
# artist.errors.full_messages # => ['name cannot be Invalid']
def valid?(opts = {})
errors.clear
if before_validation == false
raise_hook_failure(:validation) if raise_on_failure?(opts)
return false
around_validation do
if before_validation == false
raise_hook_failure(:validation) if raise_on_failure?(opts)
return false
end
validate
after_validation
end
validate
after_validation
errors.empty?
end

Expand All @@ -1214,9 +1217,11 @@ def _delete_dataset
# Internal destroy method, separted from destroy to
# allow running inside a transaction
def _destroy(opts)
raise_hook_failure(:destroy) if before_destroy == false
_destroy_delete
after_destroy
around_destroy do
raise_hook_failure(:destroy) if before_destroy == false
_destroy_delete
after_destroy
end
self
end

Expand Down Expand Up @@ -1262,34 +1267,45 @@ def _refresh(dataset)
# Internal version of save, split from save to allow running inside
# it's own transaction.
def _save(columns, opts)
raise_hook_failure(:save) if before_save == false
if new?
raise_hook_failure(:create) if before_create == false
pk = _insert
@this = nil
@new = false
@was_new = true
after_create
was_new = false
pk = nil
around_save do
raise_hook_failure(:save) if before_save == false
if new?
was_new = true
around_create do
raise_hook_failure(:create) if before_create == false
pk = _insert
@this = nil
@new = false
@was_new = true
after_create
end
else
around_update do
raise_hook_failure(:update) if before_update == false
if columns.empty?
@columns_updated = if opts[:changed]
@values.reject{|k,v| !changed_columns.include?(k)}
else
_save_update_all_columns_hash
end
changed_columns.clear
else # update only the specified columns
@columns_updated = @values.reject{|k, v| !columns.include?(k)}
changed_columns.reject!{|c| columns.include?(c)}
end
_update_columns(@columns_updated)
@this = nil
after_update
end
end
after_save
end
if was_new
@was_new = nil
pk ? _save_refresh : changed_columns.clear
else
raise_hook_failure(:update) if before_update == false
if columns.empty?
@columns_updated = if opts[:changed]
@values.reject{|k,v| !changed_columns.include?(k)}
else
_save_update_all_columns_hash
end
changed_columns.clear
else # update only the specified columns
@columns_updated = @values.reject{|k, v| !columns.include?(k)}
changed_columns.reject!{|c| columns.include?(c)}
end
_update_columns(@columns_updated)
@this = nil
after_update
after_save
@columns_updated = nil
end
@modified = false
Expand Down
104 changes: 104 additions & 0 deletions spec/model/hooks_spec.rb
Expand Up @@ -269,3 +269,107 @@ def validate
MODEL_DB.sqls.should == []
end
end

describe "Model around filters" do
before do
MODEL_DB.reset

@c = Class.new(Sequel::Model(:items))
@c.class_eval do
columns :id, :x
def _save_refresh(*a) end
end
end

specify "around_create should be called around new record creation" do
@c.class_eval do
def around_create
MODEL_DB << 'ac_before'
super
MODEL_DB << 'ac_after'
end
end
@c.create(:x => 2)
MODEL_DB.sqls.should == [ 'ac_before', 'INSERT INTO items (x) VALUES (2)', 'ac_after' ]
end

specify "around_delete should be called around record destruction" do
@c.class_eval do
def around_destroy
MODEL_DB << 'ad_before'
super
MODEL_DB << 'ad_after'
end
end
@c.load(:id=>1, :x => 2).destroy
MODEL_DB.sqls.should == [ 'ad_before', 'DELETE FROM items WHERE (id = 1)', 'ad_after' ]
end

specify "around_update should be called around updating existing records" do
@c.class_eval do
def around_update
MODEL_DB << 'au_before'
super
MODEL_DB << 'au_after'
end
end
@c.load(:id=>1, :x => 2).save
MODEL_DB.sqls.should == [ 'au_before', 'UPDATE items SET x = 2 WHERE (id = 1)', 'au_after' ]
end

specify "around_update should be called around saving both new and existing records, around either after_create and after_update" do
@c.class_eval do
def around_update
MODEL_DB << 'au_before'
super
MODEL_DB << 'au_after'
end
def around_create
MODEL_DB << 'ac_before'
super
MODEL_DB << 'ac_after'
end
def around_save
MODEL_DB << 'as_before'
super
MODEL_DB << 'as_after'
end
end
@c.create(:x => 2)
MODEL_DB.sqls.should == [ 'as_before', 'ac_before', 'INSERT INTO items (x) VALUES (2)', 'ac_after', 'as_after' ]
MODEL_DB.sqls.clear
@c.load(:id=>1, :x => 2).save
MODEL_DB.sqls.should == [ 'as_before', 'au_before', 'UPDATE items SET x = 2 WHERE (id = 1)', 'au_after', 'as_after' ]
end

specify "around_validation should be called around validating records" do
@c.class_eval do
def around_validation
MODEL_DB << 'av_before'
super
MODEL_DB << 'av_after'
end
def validate
MODEL_DB << 'validate'
end
end
@c.new(:x => 2).valid?.should == true
MODEL_DB.sqls.should == [ 'av_before', 'validate', 'av_after' ]
end

specify "around_validation should be able to catch validation errors and modify them" do
@c.class_eval do
def validate
errors.add(:x, 'foo')
end
end
@c.new(:x => 2).valid?.should == false
@c.class_eval do
def around_validation
super
errors.clear
end
end
@c.new(:x => 2).valid?.should == true
end
end

0 comments on commit 2221e28

Please sign in to comment.