views:

122

answers:

6

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 :

  • Add MyModelLog.log_something in the something method of MyModel

  • Call MyModelLog.log_something on the after_validation callback of MyModel

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?

A: 

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.

Rob Di Marco
I might not understand your point, but if I do a nested transaction for log_something, then when the "parent" transaction gets rolled back, the child gets rolled back as well
marcgg
+1  A: 

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?

nathanvda
thanks for the answer but I might end up with a lot of those logs in different places so I would prefer to keep them in my model
marcgg
The advantage of doing it in the controller is that it is explicit, and belongs to the handling of an error. To avoid to much code duplication, you could mixin behaviour, make some method that logs the error, and sets the flash, for instance in one command? On the other hand, i like the idea of the observers too, not sure if they operate in a different transaction though.
nathanvda
+2  A: 

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...

DGM
Good idea, I'll think about and see if it would fit what I'm trying to do
marcgg
Further research shows this may not work - Even observers take place inside the transaction. Look at after_commit in rails 3 to run something after the commit... there's also an after_rollback
DGM
+3  A: 

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.

Swanand
thanks for the answer. however this seems very dependent of mysql and I'm not sure if it would be a viable solution. Also I want to log errors within the validation methods since I wouldn't be only logging the content of errors but also the results of some API calls. Still, good point
marcgg
+1  A: 

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!!

Deepak N
+1  A: 

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.

vrish88