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