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,