Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Track dirty for not-persisted attribute in an ActiveRecord object in rails

I have an object that inherits from ActiveRecord, yet it has an attribute that is not persisted in the DB, like:

 class Foo < ActiveRecord::Base
   attr_accessor :bar
 end

I would like to be able to track changes to 'bar', with methods like 'bar_changed?', as provided by ActiveModel Dirty. The problem is that when I try to implement Dirty on this object, as described in the docs, I'm getting an error as both ActiveRecord and ActiveModel have defined define_attribute_methods, but with different number of parameters, so I'm getting an error when trying to invoke define_attribute_methods [:bar].

I have tried aliasing define_attribute_methods before including ActiveModel::Dirty, but with no luck: I get a not defined method error.

Any ideas on how to deal with this? Of course I could write the required methods manually, but I was wondering if it was possible to do using Rails modules, by extending ActiveModel functionality to attributes not handled by ActiveRecord.

like image 487
Santiago Palladino Avatar asked Apr 14 '11 16:04

Santiago Palladino


3 Answers

I'm using the attribute_will_change! method and things seem to be working fine.

It's a private method defined in active_model/dirty.rb, but ActiveRecord mixes it in all models.

This is what I ended up implementing in my model class:

def bar
  @bar ||= init_bar
end
def bar=(value)
  attribute_will_change!('bar') if bar != value
  @bar = value
end
def bar_changed?
  changed.include?('bar')
end

The init_bar method is just used to initialise the attribute. You may or may not need it.

I didn't need to specify any other method (such as define_attribute_methods) or include any modules. You do have to reimplement some of the methods yourself, but at least the behaviour will be mostly consistent with ActiveModel.

I admit I haven't tested it thoroughly yet, but so far I've encountered no issues.

like image 161
Alessandro Avatar answered Nov 08 '22 15:11

Alessandro


ActiveRecord has the #attribute method (source) which once invoked from your class will let ActiveModel::Dirty to create methods such as bar_was, bar_changed?, and many others.

Thus you would have to call attribute :bar within any class that extends from ActiveRecord (or ApplicationRecord for most recent versions of Rails) in order to create those helper methods upon bar.

Edit: Note that this approach should not be mixed with attr_accessor :bar

Edit 2: Another note is that unpersisted attributes defined with attribute (eg attribute :bar, :string) will be blown away on save. If you need attrs to hang around after save (as I did), you actually can (carefully) mix with attr_reader, like so:

attr_reader :bar
attribute :bar, :string

def bar=(val)
  super
  @bar = val
end
like image 39
Cristiano Mendonça Avatar answered Nov 08 '22 13:11

Cristiano Mendonça


I figured out a solution that worked for me...

Save this file as lib/active_record/nonpersisted_attribute_methods.rb: https://gist.github.com/4600209

Then you can do something like this:

require 'active_record/nonpersisted_attribute_methods'
class Foo < ActiveRecord::Base
  include ActiveRecord::NonPersistedAttributeMethods
  define_nonpersisted_attribute_methods [:bar]
end

foo = Foo.new
foo.bar = 3
foo.bar_changed? # => true
foo.bar_was # => nil
foo.bar_change # => [nil, 3]
foo.changes[:bar] # => [nil, 3]

However, it looks like we get a warning when we do it this way:

DEPRECATION WARNING: You're trying to create an attribute `bar'. Writing arbitrary attributes on a model is deprecated. Please just use `attr_writer` etc.

So I don't know if this approach will break or be harder in Rails 4...

like image 2
Tyler Rick Avatar answered Nov 08 '22 13:11

Tyler Rick