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 F. 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,

Marnen Laibow-Koser
http://www.marnen.org
[email protected]

On Dec 7, 2010, at 11:34 AM, Fearless F. wrote:

As an example, I just wrote this monstrosity:
SymbolValue.create(:symbol_name_id => symbol_name.id,
this, but what do the mavens of style in this forum suggest?
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

class ActiveRecord::Base

def self.create_or_update(attrs_to_match, attrs_to_update = {})
if (incumbent = self.first(:conditions => attrs_to_match))
incumbent.update_attributes(attrs_to_update)
incumbent
else
self.create!(attrs_to_match.merge(attrs_to_update))
end
end

end

… but maybe there is something more appropriate in the new query
interface or AREL models?

Fearless F. 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,

Marnen Laibow-Koser
http://www.marnen.org
[email protected]

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

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?