style question: "update or create" active record

I find that I'm frequently writing code that could be described in
English as "If there is an incumbent ActiveRecord that meets some
specific criteria, then update one or more of its fields. Otherwise
create a new record with the same criteria and new field values."

ActiveRecord's dynamic finder methods (find_or_create_by_xxx) are not
usually expressive enough to do this. But I haven't been able to create
a general method that smells right. It's been bugging me, so I turn to
the mavens of style in this forum for suggestions.

As an example, I just wrote this monstrosity:

def set_xattribute(name, v1, v2)
  symbol_name = self.class.intern_symbol_name(name)
  incumbent = SymbolValue.
    where(:symbol_values => {:symbol_name_id => symbol_name.id}).
    where(:symbol_values => {:owner_id => self.id}).
    where(:symbol_values => {:xclass_id => self.class.xclass_id}).first
  if (incumbent)
    incumbent.update_attributes(:v1 => v1, :v2 => v2)
  else
    SymbolValue.create(:symbol_name_id => symbol_name.id,
                       :owner_id => self.id,
                       :xclass_id => self.class.xclass_id,
                       :v1 => v1,
                       :v2 => v2)
  end
end

Note the code fragments repeated among the "finder" (incumbent = ...),
the "updater" (update_attributes(...), and the "creator"
(SymbolValue.create(...)). There may be a clever way to use scopes for
this, but what do the mavens of style in this forum suggest?

- ff

Fearless Fool wrote in post #966959:

I find that I'm frequently writing code that could be described in
English as "If there is an incumbent ActiveRecord that meets some
specific criteria, then update one or more of its fields. Otherwise
create a new record with the same criteria and new field values."

ActiveRecord's dynamic finder methods (find_or_create_by_xxx) are not
usually expressive enough to do this.

find_or_create_by_* will do exactly this. You can specify additional
fields for the "create" part of the action. Please see the docs.

If that won't do the trick, then please explain further.

Best,

You might play around with find_or_instantiator_by_attributes

http://apidock.com/rails/ActiveRecord/FinderMethods/find_or_instantiator_by_attributes

BTW, the best I've come with so far is:

file: ar_extensions.rb

Marnen Laibow-Koser wrote in post #966963:

find_or_create_by_* will do exactly this. You can specify additional
fields for the "create" part of the action. Please see the docs.

If that won't do the trick, then please explain further.

Hi Marnen:

Despite multiple re-reads of the documentation, I haven't figured out
how to get it to update specific fields. Maybe I'm just being dense.
Consider this AR class (slightly different than the example I gave, but
you'll get the idea):

SymbolValue.first

=> #<SymbolValue id: 1, symbol_name_id: 2, owner_id: 1,
owner_class_name_id: 1, value: "soso">

Let's say I want to discriminate on :symbol_name_id, :owner_id and
:owner_class_name_id to select the record, and I want to update the
:value field. Putting aside any complaints about the length of the
dynamic method name, here are two attempts that do NOT update :value:

SymbolValue.find_or_create_by_symbol_name_id_and_owner_id_and_owner_class_name_id(2,
1, 1, :value => "new_soso")
=> #<SymbolValue id: 1, symbol_name_id: 2, owner_id: 1,
owner_class_name_id: 1, value: "soso">

SymbolValue.find_or_create_by_symbol_name_id_and_owner_id_and_owner_class_name_id(2,
1, 1) {|s| s.value = "new_soso"}
=> #<SymbolValue id: 1, symbol_name_id: 2, owner_id: 1,
owner_class_name_id: 1, value: "soso">

As far as I can tell, the dynamic finder methods only set :value if a
new record is created, not if an existing record is found. While that
may be useful, it's not what I was asking for in the OP.

What am I missing?

Fearless Fool wrote in post #966995:

Marnen Laibow-Koser wrote in post #966963:

find_or_create_by_* will do exactly this. You can specify additional
fields for the "create" part of the action. Please see the docs.

If that won't do the trick, then please explain further.

Hi Marnen:

Despite multiple re-reads of the documentation, I haven't figured out
how to get it to update specific fields. Maybe I'm just being dense.
Consider this AR class (slightly different than the example I gave, but
you'll get the idea):

SymbolValue.first

=> #<SymbolValue id: 1, symbol_name_id: 2, owner_id: 1,
owner_class_name_id: 1, value: "soso">

Let's say I want to discriminate on :symbol_name_id, :owner_id and
:owner_class_name_id to select the record, and I want to update the
:value field. Putting aside any complaints about the length of the
dynamic method name, here are two attempts that do NOT update :value:

SymbolValue.find_or_create_by_symbol_name_id_and_owner_id_and_owner_class_name_id(2,

1, 1, :value => "new_soso")
=> #<SymbolValue id: 1, symbol_name_id: 2, owner_id: 1,
owner_class_name_id: 1, value: "soso">

SymbolValue.find_or_create_by_symbol_name_id_and_owner_id_and_owner_class_name_id(2,

1, 1) {|s| s.value = "new_soso"}
=> #<SymbolValue id: 1, symbol_name_id: 2, owner_id: 1,
owner_class_name_id: 1, value: "soso">

As far as I can tell, the dynamic finder methods only set :value if a
new record is created, not if an existing record is found. While that
may be useful, it's not what I was asking for in the OP.

Oh, I see now. I misunderstood your original requirement. Yeah,
find_or_create_by_* won't do that.

I think your create_or_update approach is reasonable. Just don't call
the file ar_extensions; there's already a plugin by that name.

What am I missing?

Nothing, apparently. :slight_smile:

Best,

Well, I couldn't rest until I figured out an implementation of
create_or_update (see version 1 above) using the oh-so-nifty
ActiveRecord::Relation framework.

Here it is:

class ActiveRecord::Base

  def self.create_or_update(relation, attrs_to_update)
    if (incumbent = relation.first).nil?
      relation.create!(attrs_to_update)
    else
      incumbent.update_attributes(attrs_to_update)
      incumbent
    end
  end

end

What's nice about this is that the relation can be a more expressive
selector than a simple hash