No Pugs

they're evil

multimodel_transactions plugin: Saving and creating multiple model objects in a single controller action

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 , .

  • By dwplciu 12/05/2013 at 12:29AM

    cheap oakleys from china (PS:ah, and then be late, let .downFought and held up with other people, fault fault.)

    If be sealed an activity by metal arm, monster person the sword saint isn’t nervous, but the metal silk being been layer after layer by this tie up ascend, he some panic!Because in this numerous metal silk end, a connective uncanny metal piece, this looks like so-so have no strange, can only monster person then the sword saint can personally realize, the Dou spirit in the oneself body just is taken out by that numerous metal silks to walk at terrible speed!

    “Be getting more impending …!”Ma San again and by hand holds tight the shoulder of the remnants of to shout loudly:”Quickly, it is quick to say with me, actually and what is the row!”

    Only and obviously this Qin’s district magistrate has never heard a classic story of another a, that be-we buy of the exorbitant price medicine in fact come out from the pharmaceutical factory of time all very cheap-at least compared with their body’s time in the hospital want cheapness of many of many.

    wholesale oakley sunglasses Early discover that the Jin from a distance follows for sky with the real strenght of shadow Shang snow, she early wants to get into to try a dress, but each clothes ice Mu has no

    Chen Can shakes head, Ann just exposed of that declined an effort all alone be really was frightened to death he, “I said not to go or not, you tomorrow return to Xuan spring for me, this of matter I can handle by myself.”He hopes tiny and red double of eyes of snow Jis, suddenly soft in speaking come down:”The person is many on the contrary will be contrary to expectation, have no quasi- now Siam the emperor is setting out what trap wait we to drill, if my a person has an accident, you in the days to come had skill can also revenge for me, check my life experience, in case of we all …the result be so terrible we dare not think about it.”

    Zhang Bao’s waiting the tent of person’s place has already been burned down by the fire feather, everyone has been able to clearly see in the moment various miserable sights.Can say, the big camp of Huang Jin Jun in nowadays in, in addition to that for promising morale but being hard protected by Zhang Bao of handsome ensign, leave flame for, in addition to the flame is a flame.As for live a person, up to now seem to be completely could not see. twitter.com


  • By pzzhlmp 12/09/2013 at 05:03AM

    polo ralph lauren outlet store r>

    Fairyland’s seeming has been having no destination, is all white cloud to float everywhere, occasionally peep out so 12 families, also living a curling up in the air chimney smoke.

    The empty harbor that silently arrives at the thunder emperor Di hole, black iron city, the gold is small to open to very quickly and then find out the big summer that he park here to fly boat, certainly externally this flying boat has been under the disguise of an empty boat, as long as doing not log on inner part, the ordinary people basically can not see through camouflage.

    Hear wave green inquiry, Du’s flying don’t answer, but the radicle boon that quickly flies to the previous wave green driver-3 airships before, accepts it into concealed bead, then flies airship once, arrive at to turn around acceleration in the cockpit in quick time after closing hatch, now their urgent matter of the moment not is compute space wall the Zhang become thin place, but first escape from the at present fleet in front again say.

    polo ralph lauren coupons Is these reason, so it’s yellow to drag along thunder to purchase yesterday

    In the morning, the Du flies to naturally run about on the R country street in Tokyo and looks and those always cursory R the peoples seem to be a bit inharmonious.However the Du flies headgear Chu Chu, and the qualities pours naturally is drew on many R countries chemisette attention.

    “Hua!”The earth vitality,such as tidewater, separates and concealed therein big guy to finally peep out its real appearance! youtube.com


Comment multimodel_transactions plugin: Saving and creating multiple model objects in a single controller action

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