AR create_or_update_if_needed rant -- must be a better way!

I'll preface this by saying I (still) consider myself a relative newcomer to Rails, and its likely that I'm missing something obvious in ActiveRecords, but this has been bugging me...

I've written a function that I use all the time, but I get a nagging feeling that this functionality MUST already exist, and even if it doesn't, there's a better way than I'm doing it. Anyway, thus:

  ActiveRecord.create_or_update_if_needed(attrs_to_match, attrs_to_update)

which lets me do things like:

  Address.create_or_update_if_needed({:street=>"1600 Pennsylvania Avenue, NW", :state=>"DC"}, {:zip=>"20500"})

If no records match on attrs_to_match then create one with those attributes AND with attrs_to_update. If a record does exist but doesn't match the given attrs_to_update, then update only those attributes. Otherwise, leave the record alone - no change needed.

If there's an obvious way to do this, you can stop reading here and simply give me the recipe!! :slight_smile:

Still reading? Really? Okay, here's how I've defined it:

Fearless Fool wrote:

I'll preface this by saying I (still) consider myself a relative newcomer to Rails, and its likely that I'm missing something obvious in ActiveRecords, but this has been bugging me...

I've written a function that I use all the time, but I get a nagging feeling that this functionality MUST already exist, and even if it doesn't, there's a better way than I'm doing it. Anyway, thus:

  ActiveRecord.create_or_update_if_needed(attrs_to_match, attrs_to_update)

which lets me do things like:

  Address.create_or_update_if_needed({:street=>"1600 Pennsylvania Avenue, NW", :state=>"DC"}, {:zip=>"20500"})

If no records match on attrs_to_match then create one with those attributes AND with attrs_to_update. If a record does exist but doesn't match the given attrs_to_update, then update only those attributes. Otherwise, leave the record alone - no change needed.

If there's an obvious way to do this, you can stop reading here and simply give me the recipe!! :slight_smile:

Well, update_all with the :conditions option will get you most of the way there. And what do you want to have happen if more than one record matches the conditions?

Still reading? Really? Okay, here's how I've defined it:

class ActiveRecord::Base

  def self.create_or_update_if_needed(attrs_to_match, attrs_to_update)     record = find(:first, :conditions=>attrs_to_match)     if (record.nil?)

Lose the parentheses -- if isn't a function.

      record = create(attrs_to_match.merge(attrs_to_update))     elsif (!record.attributes_match?(attrs_to_update))       record.update_attributes(attrs_to_update)     end     record   end

  def attributes_match?(attrs)     # use new() to convert attribute keys and values     mangled_attrs = self.class.new(attrs).attributes     mangled_attrs.all? {|k,v| v.nil? || self.attributes[k] == v }   end

end

Note that the "obvious" definition of attributes_match? would NOT work:

  def attributes_match?(attrs)     attrs.all? {|k,v| self.attributes[k] == v}   end

... because self.attributes use strings as hash keys, not symbols, and attribute values may be subject to type conversions.

Then try HashWithIndifferentAccess or stringify_keys. You're overcomplicating this.

So I create a new record, using the given attrs, and let the ActiveRecord mechanism handle the conversions.

But by the time I've gone to this level of hackery, it seems prudent to harness the wisdom of the masses: is there a better way?

Yes! Work *with* the framework instead of around it, as I suggested above.

- ff

Best,