HABTM: support deferred saving of HABTM relationships #6480

urkle opened this Issue May 25, 2012 · 4 comments


None yet

3 participants


This is a reopening of issue #674, which seems to have been auto closed on import fro Lighthouse. This is still very much an issue and IMHO, the current way of auto-saving HABTM relationships is contrary to what is expected. And the fact that things are auto-saved has bitten out several hours of tracking down a bug in our application which resulted in me finding that HABTM saving doesn't work as I expect they should.


def Sprocket < ActiveRecord::Base
  has_and_belongs_to_many :users

#  later... in a controller

def create
  @sprocket = Sprocket.new params[:sprocket]
  if @sprocket.save
     # we saved correctly  blah blah
   # we failed blah blah

Now if I post with this hash data

{ :sprocket => {
    :name => 'My new sprocket'
    :user_ids => [ 3, 54 ]

a new Sprocket will be created but with NO users associated with it!!!

if I save first and then clear and set the user_ids member then it'll be saved..
(Note this is on Rails 3.0.x that the application is currently written against)

Ruby on Rails member

Are you able to test agains 3-2 to check this? Rails 3.0 is currently not receiving fixes other than security ones, unless someone's willing to do the work and send a pull request.

Also, what's your complete model code? Does it include :user_ids as accessible attribute?


it does seem that the "bug" I described doesn't occur in 3.2,(habtm's are queued for NEW model instances in 3.2.3) however the request is still valid as HABTM's are still updated instantaneously instead of deferred for already persisted models, which is very undesirable.

Ruby on Rails member

@urkle thanks for reporting back.

This is how the association works, it's very well documented in guides, just access the link and scroll down a bit to "4.4.3 When are Objects Saved?". It explains that when you assign a persisted object to an habtm association, it's automatically saved to update the join table. If the parent is new, then it'll save when saving the parent.

I personally don't think this is something that should be changed, I think it's fine how it currently works. In any case, if you want to show case better examples that would prove the point and would like to try out a new discussion in the Ruby on Rails Core mailing list to see other opinions, please feel free.

I'm closing here since now we're talking about a feature request. Thanks!


@carlosantoniodasilva I find the current behaviour odd. Why not automatically save when assigning an object to a belongs_to, or even when assigning a value to a string typed attribute, document it very well and claim it's fine then. Because that would be madness: you must pass validation tests first before committing.

The same is true when objects are assigned to has_many and habtm. Blindly accepting any such assignments without first validating it's allowed to assign those specific objects is just plain wrong. As usual you'd want any validations to happen when @parent.valid? is called.

I really think this has been a design mistake from day one. It should have been kept consistent: regardless of the association type, when an object is assigned or added to the association it should not be saved until the parent passes validation and executes it's save method.

As such behaviour would break existing rails apps :autosave => false was introduced (which naming is confusing too, see discussion in issue #674). Default has_one and belongs_to has :autosave => false you could say, and has_many and habtm has :autosave => true.

I've been using my own patch for years now, to my own delight. ;-) I've got loads of good examples. Here's just one:

class Person < ActiveRecord::Base
  has_and_belongs_to_many :teams, :uniq => true, :autosave => false

  validate :teams_trusted?
  validate :teams_enabled?


    def teams_trusted?
      if teams.any? { |team| !team.account_id.in?(trusted_account_ids) }
        errors.add(:teams, "Team must be in a trusted account.")

    def teams_enabled?
      disabled_teams = teams.select { |t| t.disabled? }
      if disabled_teams.any?
        errors.add(:teams, "Person cannot be assigned to a disabled team (#{disabled_teams.map(&:name).join(", ")})")


In the controller we simply have the usual:

class PeopleController < ApplicationController

  def update


An example of a form post is (team field is simply a multiselect box for example in the UI):

{ "person": { "id" => 12345, "name" => "John Doe", "team_ids" => [123, 456, 999] } }

What would be an alternative implementation using current rails behaviour? How do you prevent a user posting (ie hacking) e.g. this:

{ "person": { "id" => 12345, "name" => "John Doe", "team_ids" => [123, 456, 1] } }

where Team with ID=1 is a disabled team.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment