Fearless Fool
2010-Apr-22 20:25 UTC
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!! :) 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?) 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. 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? - ff -- Posted via http://www.ruby-forum.com/. -- You received this message because you are subscribed to the Google Groups "Ruby on Rails: Talk" group. To post to this group, send email to rubyonrails-talk-/JYPxA39Uh5TLH3MbocFF+G/Ez6ZCGd0@public.gmane.org To unsubscribe from this group, send email to rubyonrails-talk+unsubscribe-/JYPxA39Uh5TLH3MbocFF+G/Ez6ZCGd0@public.gmane.org For more options, visit this group at http://groups.google.com/group/rubyonrails-talk?hl=en.
Marnen Laibow-Koser
2010-Apr-23 12:34 UTC
Re: AR create_or_update_if_needed rant -- must be a better w
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!! :)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.> - ffBest, -- Marnen Laibow-Koser http://www.marnen.org marnen-sbuyVjPbboAdnm+yROfE0A@public.gmane.org -- Posted via http://www.ruby-forum.com/. -- You received this message because you are subscribed to the Google Groups "Ruby on Rails: Talk" group. To post to this group, send email to rubyonrails-talk-/JYPxA39Uh5TLH3MbocFF+G/Ez6ZCGd0@public.gmane.org To unsubscribe from this group, send email to rubyonrails-talk+unsubscribe-/JYPxA39Uh5TLH3MbocFF+G/Ez6ZCGd0@public.gmane.org For more options, visit this group at http://groups.google.com/group/rubyonrails-talk?hl=en.