No Pugs

they're evil

In Ruby on Rails, it’s common to have a resource have a one-to-one correspondence with a model. Often you will have a create action that looks something like this:

class UsersController < ApplicationController
  # ... other actions, etc

  def create
    User.transaction do
      @user = User.new(params[:user])

      if @user.save
        flash[:notice] = 'User was successfully created.'
        redirect_to(@user)
      else
        render :action => "new"
      end
    end
  end

  # ... other actions
end

Now what if we have another model object that is also managed by this action, perhaps a profile object? Perhaps using the fields_for helper in the view?

Then we might change our code so that it looks something like this:

class UsersController < ApplicationController
  # ... other actions, etc

  def create
    User.transaction do
      @user = User.new(params[:user])
      @profile = Profile.new(params[:profile])

      user.profile = @profile

      if @user.save && @profile.save
        flash[:notice] = 'User was successfully created.'
        redirect_to(@user)
      else
        User.connection.rollback_db_transaction
        render :action => "new"
      end
    end
  end

  # ... other actions
end

And our new.html.erb view will have a form that looks something like this:

<% form_for(@user) do |f| %>
  <%= f.error_messages %>

  <p>
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </p>

  <%# Other fields using the "f" builder %>

  <% fields_for(@profile) do |builder| -%>
    <%= builder.error_messages %>
    <p>
      <%= builder.label :favorite_movie %><br />
      <%= builder.text_field :favorite_movie %>
    </p>

    <%# Other fields using the "builder" builder %>

  <% end %>
  <p>
    <%= f.submit "Create" %>
  </p>
<% end %>

There should be no problems here right? Wrong. The problem is that in edit.html.erb, we are going to also have a form_for(@user) and fields_for(@profile). So how does new.html.erb and edit.html.erb know to create a form action of create_user_url and update_user_url, respectively? It does this by checking the new_record? method of the passed in model.

Here’s where a problem is created. Lets revisit part of our controller code for the create action:

      if @user.save && @profile.save
        flash[:notice] = 'User was successfully created.'
        redirect_to(@user)
      else
        User.connection.rollback_db_transaction
        render :action => "new"
      end

If @user.save fails, @user.new_record? will still be true. This is what we want since we will be calling render :action => 'new' and we want the form generated there to call create_user_url, not update_user_url.

But what happens when @user.save works and @profile.save fails? @profile.new_record? will still be true, which is good, but @user.new_record? will be false, because it successfully saved! The user row will not exist in the database, because we rolled back our transaction, but only @profile knew to revert to an unsaved active record state. @user has no idea what’s going on.

So then, when new.html.erb renders form_for(@user), it will create a form with the update_user_url instead of create_user_url! This will result in an exception like this:

 ActiveRecord::RecordNotFound in UsersController#update

Couldn't find User with ID=2

To understand how to fix this, we need to understand why it works with only one model. It works because @user.save calls rollback_active_record_state! which records the object’s state, with respect to things like new_recodr?, and takes a block that it executes in a begin/rescue block. If it catches an exception, it reverts to the state it was in before it yielded to the block, and reraises the exception. The exception is usually ActiveRecord::Rollback, which is gobbled up by a transaction that @user.save starts before calling rollback_active_record_state!

We can fix the problem by using this method on both objects, and throwing an exception if any of the models fail to save.

The above if statement becomes something like this:

      #an anonymous Exception class, it's best to create a real class for use
      #in multiple locations
      ec = Class.new(:Exception)

      begin
        @user.rollback_active_record_state! do
          @profile.rollback_active_record_state! do
            if @user.save && @profile.save
              flash[:notice] = 'User was successfully created.'
              redirect_to(@user)
            else
              User.connection.rollback_db_transaction
              raise ec.new
            end
          end
        end
      rescue ec
        render :action => "new"
      end

I have created a plugin that helps replace this type of code. It works with any number of model objects across any number of database connections.

It is at git://github.com/azimux/multimodel_transactions.git

With it installed, the above code becomes:

ax_multimodel_if([@user, @profile],
  :if => proc {
    @user.save && @profile
  },
  :is_true => proc {
    flash[:notice] = 'User was successfully created.'
    redirect_to(@user)
  },
  :is_false => proc {
    render :action => "new"
  }
)

Note to Smalltalk users: if you need an example of ruby’s block passing syntax making certain method calls look a little ugly, there you go :P

There is also another method in the plugin that helps with models being in multiple databases (or the future possibility of such a thing happening.): ax_multimodel_transaction

To apply it to the above call to ax_multimodel_if, it would look somewhat like this:

ax_multimodel_transaction [@profile], :already_in => @user do
  ax_multimodel_if([@user, @profile],
    :if => proc {
      @user.save && @profile
    },
    :is_true => proc {
      flash[:notice] = 'User was successfully created.'
      redirect_to(@user)
    },
    :is_false => proc {
      render :action => "new"
    }
  )
end

h1. Gotchas

1) If @profile has the user_id foreign key, it’s important to do:

@user.profile = @profile

and not:

@profile.user = @user

When neither of the objects have been saved yet. I’m not exactly sure why this is, I just know from experience that if you do it the other way around, @profile will be saved with a nil user_id

2) Like always when doing things sensitive to transactions and rollbacks, you might wish to add a “self.use_transactional_fixtures = false” to the relevant functional test. This is really only necessary if your test runs commands that execute queries that are not expecting to be inside of a rolledback transaction after the call to “post :create” or “post :update” or whatever. It is only very rarely necessary to do, but it’s good to keep in mind when writing more complex functional tests.

Published on 01/27/2010 at 03:01PM under , .

2 comments

It looks like the exception_notification plugin is undergoing a pretty big overhaul to get it working well with rails3. The official git repository for that plugin only has one branch, master. What should you do for your existing rails2 applications that are not ready for migration to rails3?

If you are getting something somewhat like this:

rake aborted!
uninitialized constant ApplicationController::ExceptionNotifiable
.../rails/activesupport/lib/active_support/dependencies.rb:102:in `const_missing'

You can fix it by using commit e8b603e523c14f145da7b3a1729f5cc06eba2dd1 of that plugin.

Something like this should do the trick:

cd vendor/plugins/exception_notification
git checkout -b rails2 e8b603e523c14f145da7b3a1729f5cc06eba2dd1

That particular commit is from November 13, 2008. It is the last commit not having anything to do with rails3, and it is a very stable commit and unlikely to need modification for existing projects.

if you are using externals to manage your subprojects, you can quickly fix it like this (from the main project directory):

ext freeze exception_notification e8b603e523c14f145da7b3a1729f5cc06eba2dd1

Published on 01/26/2010 at 06:26PM under , , .

79 comments

Powered by Typo – Thème Frédéric de Villamil | Photo Glenn