33

In rails 3.2+, you can do this :

SomeModel.some_scope.first_or_initialize

Which means you can also do :

OtherModel.some_models.first_or_initialize

I find this pretty useful, but i'd like to have a first_or_build method on my has_many associations, which would act like first_or_initialize but also add a new record to the association as build does when needed.

update for clarification : yes, i know about first_or_initializeand first_or_create. Thing is, first_or_initializedoes not add the initialized record to the association's target as build does, and first_or_create... well... creates a record, which is not the intent.

I have a solution that works, using association extensions :

class OtherModel < ActiveRecord::Base

  has_many :some_models do 
    def first_or_build( attributes = {}, options = {}, &block )
      object = first_or_initialize( attributes, options, &block )
      proxy_association.add_to_target( object ) if object.new_record?
      object
    end
  end

end

I just wonder if :

  • built-in solutions to this problem already exist ?
  • my implementation has flaws i do not see ?
m_x
  • 12,357
  • 7
  • 46
  • 60
  • What if you want perform eager loader on `OtherModel.some_models` at some point? I would rather perform such initialization in an `after_initialize` callback, its much cleaner approach IMO. – Rajesh Kolappakam Sep 09 '13 at 06:26

1 Answers1

22

I'm not sure if there is anything built into rails that will do exactly what you want, but you could mimic the first_or_initialize code with a more concise extension than you are currently using, which I believe does what you want, and wrap it into a reusable extension such as the following. This is written with the Rails 3.2 extend format.

module FirstOrBuild
  def first_or_build(attributes = nil, options = {}, &block)
    first || build(attributes, options, &block)
  end
end

class OtherModel < ActiveRecord::Base
  has_many :some_models, :extend => FirstOrBuild
end
Bryan Corey
  • 740
  • 7
  • 12
  • Quite right. I don't know why I overingeneered this that way, this is obviously better. – m_x Sep 11 '13 at 06:52
  • 5
    In order to get this to work in Rails 4.1.0.beta, I had to specify the proxy association inside the scoping context; such that the extension reads `first || scoping{ proxy_association.build(attributes, &block) }` (without options anywhere) and the invocation looks like `Post.first.comments.where(foo: 'bar').first_or_build`. Also I used the new syntax has_many :posts, -> { extending FirstOrBuild } on Post.) – Chris Keele Feb 04 '14 at 04:28
  • @ChrisKeele thanks for the update. Does this still work as intended when you do things like `foo.bars.where(title: 'buzz').first_or_build` ? (i.e. a new bar is added to foo.bars, and it has 'buzz' as title). As a side note, I just noticed that first_or_create does not add the record to the proxy_association's target, either. Maybe I should file a ticket to the rails team, the current state of the API does not follow the principle of least surprise IMHO – m_x Mar 26 '14 at 09:43
  • I also find it surprising, though there my be some reason why it's not default behaviour. – Chris Keele Mar 26 '14 at 15:29
  • 1
    `foo.bars.where(title: 'buzz').first_or_build(desc: 'baz')` still finds by `title` if it exists, builds a record with the expected `title` and `desc` if not, and adds the `bar` to `foo`'s collection of them. – Chris Keele Mar 26 '14 at 15:31
  • 1
    I'm using Rails 3.2.17 and I had to use @ChrisKeele's code (which works like a charm). Just a head's up to other folks reading this--the accepted answer won't work for some versions of Rails 3.2. – ideaoforder Aug 26 '14 at 15:36