find_or_create_by_xxx!()

All,

There's a bit of behavior I'd like to tweak around the find_or_create_by methods to help make it more obvious when a developer (especially newbies) screws up by forgetting to initialize required fields. Let's say you have some code like this:

car = dealership.cars.find_or_create_by_model("corvette") car.modelyear = 2003 car.save dealership.save

and let's say that Car looks like this: class Car < ActiveRecord::Base validates_presence_of :modelyear belongs_to :dealership end

At the moment (checked in ActiveRecord 2.2.2 and 2.3.2) the code above will silently create your car, but won't save it to the database and won't attach it to your dealership. If you change line 1939 of ActiveRecord::Base from this:                     #{'record.save' if instantiator == :create} to this (note addition of bang):                     #{'record.save!' if instantiator == :create} then you get the much more helpful error message: "Validation failed: Modelyear can't be blank" instead of a silent failure.

So, my proposal is that we change the behavior of find_or_create_by_xxx () so that it can also be called with a bang like this: find_or_create_by_xxx!() and will throw an exception if the save fails. This would be in line with the current behavior of find_by_xxx! () that will throw an exception if it doesn't find anything.

Yes, I know I could solve the problem by doing something like this:

car = dealership.cars.find_or_create_by_model("corvette") raise(ValidationError) if car.new_record?

but that's a lot of code for something that would only truly fail if I screwed up in my original development.

I tried to put my proposed patch to ActiveRecord::Base.method_missing in like this: if match.bang?        #{'record.save!' if instantiator == :create} else        #{'record.save' if instantiator == :create} end

But that fails because "match" is not in scope? Don't know because my ruby foo isn't quite strong enough to understand everything that's going on in that method.

You should take a look into dynamic_finder_match.rb file too. In this file you will see that they already check when a bang is sent,but not in the find_or_create cases. You can refactor it a little bit to check in any case.

Then you would have to change the method missing method this way:

          #{'record.save' if instantiator == :create && !match.bang?}           #{'record.save!' if instantiator == :create && match.bang?}

The match is not in scope because you are inside the method inside the class eval, that's why you should put everything inside the interpolation.