Fat models and validation

Hi all,

I’m trying my best to adhere to the thin controller/fat model
philosophy - in particular, I’m trying to make sure my validation
stays inside my models, so it can be reused in multiple places.

My current problem is that I have an action that creates several
objects all at once. In my app, there are Sources (which represent an
entire web site) and Resources (which map to a single URL). Each
Source has_many Resources (so each Resource belongs_to a Source).

In my action, I need to create both a Source and a linked Resource.
However, each has some validation that needs to occur. If either the
Source or Resource has a problem, neither should be created.

My initial thought was to use a transaction. I would use save! so that
if anything went wrong, I could just print out the error in the flash.
My controller action contained code that looked something like this:

begin
Source.transaction do
@resource =Resource.new(:source => Source.new(:name =>
params[:name]), :url => params[:url])
@source = @resource.source
@source.save!
@resource.save!
end

do some stuff

rescue ActiveRecord::RecordInvalid, AssertionFailure => error
# put the error message in the flash
render :action => ‘new’
end

If there is something wrong in Source (e.g. the name is empty), this
works fine - it prints out the nice error message I have in the Source
model.

However, if something is wrong in the Resource, Source just gives the
following error: ‘Validation failed: Resources is invalid’. That’s not
very helpful to the user.

Of course, I can do @resource.save! first, but if there is a problem
with the Source, ActiveRecord complains that the source_id cannot be
null (since it could not save).

Clearly my approach is very wrong here. My goal is to

  1. Only save the Source and Resource if both are valid
  2. Not duplicate a bunch of validation logic in the controller

Anyone have ideas for how I can accomplish this?

Thanks in advance!

Ben

For this:

@resource =Resource.new(:source => Source.new(:name =>
params[:name]), :url => params[:url])
@source = @resource.source
@source.save!
@resource.save!

You don’t need these lines:

@source = @resource.source
@source.save!

You could alternatively take Source.new out from Resource.new and make
it:

@source = Source.new(:name => params[:name])

and then
@resource = Resource.new(:source => @source)

As source is saved along with the resource.


Ryan B.

Ryan,

You’re right, there is definitely some unnecessary code in there.
However, even if I simplify it as you suggested, there is still the
problem that doing @source.save! gives the following exception

‘Validation failed: Resources is invalid’.

which isn’t very useful. I’d like it to give the actual validation
failure in the Resource … something like ‘URL is not valid’. Am I
totally approaching this problem the wrong way? It feels like I’m
working way too hard to do something reasonably simple - which usually
means I’m doing something pretty boneheaded :).

Ben

Ryan,

You’re right, there is definitely some unnecessary code in there.
However, even if I simplify it as you suggested, there is still the
problem that doing @source.save! gives the following exception

‘Validation failed: Resources is invalid’.

which isn’t very useful. I’d like it to give the actual validation
failure in the Resource … something like ‘URL is not valid’. Am I
totally approaching this problem the wrong way? It feels like I’m
working way too hard to do something reasonably simple - which usually
means I’m doing something pretty boneheaded :).

Ben

Whoops, it looks like my browser did something funky. Sorry about
that.

If the Source has a validation error, the problem with calling
@resource.save! is that it throws an exception saying it cannot write
the resource to DB because source_id is null. Which makes sense -
since the Source is not valid, it won’t write to the DB and therefore
does not have a valid ID.

Hopefully this one will not double post…

Any reason for the double post?

If you want to give the error of the resource then calling
@resource.save!
should return it. Never call @source.save!

On Dec 7, 2007 12:29 PM, bhbrinckerhoff [email protected]
wrote:

failure in the Resource … something like ‘URL is not valid’. Am I

@resource =Resource.new(:source => Source.new(:name =>
You could alternatively take Source.new out from Resource.new and make
Ryan B.


Ryan B.

In that case:

(assuming resource has many sources)

@resource = Resource.new
@resource.sources.build = Source.new(:name => “Source”)
@resource.save!

Should work.

If I got it the wrong way around just swap source with resource and you
should get the idea.

On Dec 7, 2007 2:00 PM, bhbrinckerhoff [email protected] wrote:

Hopefully this one will not double post…

‘Validation failed: Resources is invalid’.

For this:
@source.save!
As source is saved along with the resource.


Ryan B.


Ryan B.


Ryan B.

Ryan,

Hey thanks for the tip. It’s not working in my example quite yet
(probably due to some other factors I didn’t include in my post), but
it looks promising. Unfortunately, I’m leaving for a trip tomorrow, so
I may not get to work on it until Monday. In any case, I’ll report
back with what I find.

Thanks!
Ben

Whoops,

that’s @resources.sources.build(:name => “Source”)

A bit too eager to hit the send button.

On Dec 7, 2007 2:14 PM, Ryan B. [email protected] wrote:

If I got it the wrong way around just swap source with resource and you

@resource.save! is that it throws an exception saying it cannot write
resource.save!

usually

@source.save!


Ryan B.


Ryan B.


Ryan B.

About the Source being really invalid but the user getting errors for
the Resource.source_id also, you could do

instead of @resource.save!
use @resource.save! if @source.valid?

Apparently your problem is with telling the user what the problem is,
right?

Wouldn’t adding <%= error_messages_for ‘source’ %> and <%=
error_messages_for ‘resource’ %> in the view help you with that?