marcgg
marcgg

Reputation: 66435

How to save something to the database after failed ActiveRecord validations?

Basically what I want to do is to log an action on MyModel in the table of MyModelLog. Here's some pseudo code:

class MyModel < ActiveRecord::Base
  validate :something

  def something
     # test
     errors.add(:data, "bug!!")
  end
end

I also have a model looking like this:

class MyModelLog < ActiveRecord::Base

  def self.log_something
    self.create(:log => "something happened")
  end

end

In order to log I tried to :

In both cases the creation is rolled back when the validation fails because it's in the validation transaction. Of course I also want to log when validations fail. I don't really want to log in a file or somewhere else than the database because I need the relationships of log entries with other models and ability to do requests.

What are my options?

Upvotes: 5

Views: 1127

Answers (6)

vrish88
vrish88

Reputation: 21437

I've solved a problem like this by taking advantage of Ruby's variable scoping. Basically I declared an error variable outside of a transaction block then catch, store log message, and raise the error again.

It looks something like this:

def something
    error = nil
    ActiveRecord::Base.transaction do
        begin
            # place codez here
        rescue ActiveRecord::Rollback => e
            error = e.message
            raise ActiveRecord::Rollback
        end
    end
    MyModelLog.log_something(error) unless error.nil?
end

By declaring the error variable outside of the transaction scope the contents of the variable persist even after the transaction has exited.

Upvotes: 2

Deepak N
Deepak N

Reputation: 2571

MyModelLog.log_something should be done using a different connection.

You can make MyModelLog model always use a different connection by using establish_connection.

class MyModelLog < ActiveRecord::Base
  establish_connection Rails.env # Use different connection

  def self.log_something
    self.create(:log => "something happened")
  end
end

Not sure if this is the right way to do logging!!

Upvotes: 1

Swanand
Swanand

Reputation: 12426

Nested transactions do seem to work in MySQL.

Here is what I tried on a freshly generated rails (with MySQL) project:

./script/generate model Event title:string --skip-timestamps --skip-fixture

./script/generate model EventLog error_message:text --skip-fixture

class Event < ActiveRecord::Base                                                                                                                                       
  validates_presence_of :title                                                                                                                                         
  after_validation_on_create :log_errors                                                                                                                               

  def log_errors                                                                                                                                                       
    EventLog.log_error(self) if errors.on(:title).present?                                                                                                             
  end                                                                                                                                                                  
end  

class EventLog < ActiveRecord::Base                                                                                                                                    
  def self.log_error(event)                                                                                                                                            
    connection.execute('BEGIN') # If I do transaction do then it doesn't work.
    create :error_message => event.errors.on(:title)                                                                                            
    connection.execute('COMMIT')                                                                                                                                       
  end                                                                                                                                                                  
end 

# And then in script/console:
>> Event.new.save
=> false
>> EventLog.all
=> [#<EventLog id: 1, error_message: "can't be blank", created_at: "2010-10-22 13:17:41", updated_at: "2010-10-22 13:17:41">]
>> Event.all
=> []

Maybe I have over simplified it, or missing some point.

Upvotes: 8

DGM
DGM

Reputation: 26979

Would this be a good fit for an Observer? I'm not sure, but I'm hoping that exists outside of the transaction... I have a similar need where I might want to delete a record on update...

Upvotes: 3

nathanvda
nathanvda

Reputation: 50057

I am not sure if it applies to you, but i assume you are trying to save/create a model from your controller. In the controller it is easy to check the outcome of that action, and you most likely already do to provide the user with a useful flash; so you could easily log an appropriate message there.

I am also assuming you do not use any explicit transactions, so if you handle it in the controller, it is outside of the transaction (every save and destroy work in their own transaction).

What do you think?

Upvotes: 1

Rob Di Marco
Rob Di Marco

Reputation: 44952

You could use a nested transaction. This way the code in your callback executes in a different transaction than the failing validation. The Rails documentations for ActiveRecord::Transactions::ClassMethods discusses how this is done.

Upvotes: 0

Related Questions