Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to raise an ActiveRecord::Rollback exception and return a value together?

I have a model that uses a acts_as_nested_set fork, and I've added a method to the model to save the model and move the node into the set in one transaction. This method calls a validation method to make sure the move is valid, which returns true or false. If the validation fails, I want my save method to raise ActiveRecord::Rollback to rollback the transaction, but also return false to the caller.

My model looks like this:

class Category < ActiveRecord::Base
  acts_as_nested_set :dependent => :destroy, :scope => :journal

  def save_with_place_in_set(parent_id)
    Category.transaction do
      return false if !save_without_place_in_set

      if !validate_move parent_id
        raise ActiveRecord::Rollback and return false
      else
        place_in_nested_set parent_id
        return true
      end
    end
  end

  alias_method_chain :save, :place_in_set

  def validate_move(parent_id)
    # return true or false if the move is valid
    # ...
  end

  def place_in_nested_set(parent_id)
    # place the node in the correct place in the set
    # ...
  end
end

However, when I call save in a situation that would fail, the transaction is rolled back but the function returns nil:

>> c = Category.new(:name => "test") 
=> #<Category id: nil, name: "test" parent_id: nil, lft: nil, rgt: nil>
>> c.save_with_place_in_set 47
=> nil
>> c.errors.full_messages
=> ["The specified parent is invalid"]
like image 698
Daniel Vandersluis Avatar asked Jun 29 '09 14:06

Daniel Vandersluis


3 Answers

You could store the value you want returned from the function in a variable and return that outside the transaction block. E.g.

  def save_with_place_in_set(parent_id)
    return_value = false
    Category.transaction do
      if !save_without_place_in_set
        return_value = false
      elsif !validate_move parent_id
        return_value = false
        raise ActiveRecord::Rollback
      else
        place_in_nested_set parent_id
        return_value = true
      end
    end
    return return_value
  end

I've set the return_value to false initially as the only other way you can get out of that transaction block is if one of the other methods raises ActiveRecord::Rollback I believe.

like image 179
Shadwell Avatar answered Sep 30 '22 21:09

Shadwell


Because the ActiveRecord::Rollback exception is handled, but not re-raised by ActiveRecord::Transaction, I could move my return out of the transaction block, and thus return a value after the transaction is rolled back.

With a little refactoring:

def save_with_place_in_set(parent_id = nil)
  Category.transaction do
    return false if !save_without_place_in_set
    raise ActiveRecord::Rollback if !validate_move parent_id

    place_in_nested_set parent_id
    return true
  end

  return false
end
like image 31
Daniel Vandersluis Avatar answered Sep 30 '22 21:09

Daniel Vandersluis


I know it may be a little late, but i ran into the same problem and just found out, that within a transaction block you can simply raise an Exception and rescue that one...Rails implicitly rollbacks the whole transaction. So there is no need for ActiveRecord::Rollback.

For example:

def create
  begin
    Model.transaction do
      # using create! will cause Exception on validation errors
      record = Model.create!({name: nil})
      check_something_afterwards(record)
      return true
    end
  rescue Exception => e
    puts e.message
    return false
  end
end

def check_something_afterwards(record)
  # just for demonstration purpose
  raise Exception, "name is missing" if record.name.nil?
end

I'm working with Rails 3.2.15 and Ruby 1.9.3.

like image 28
Matteo Avatar answered Sep 30 '22 19:09

Matteo