Problem with has_and_belongs_to_many

hello,

I have an m:n relationsship (has_and_belongs_to_many) between the models
CoreUser and CoreGroup. So from some rails experience I thought I'd
be able to add a (user,group)-relation simply by doing
agroup.core_users << auser.

Unfortunately the following test-case fails on the last assertion
(unless I include the line that is commented out):

  def test_add_remove
    someuser = CoreUser.create!(:username => 'test2s', ...)
    ag = CoreGroup.find_by_path("/Administrators")
    assert !ag.core_users.include?(someuser)
    assert !someuser.core_groups.include?(ag)

    ag.core_users << someuser
# someuser.core_groups << ag
    assert ag.core_users.include?(someuser)
    assert someuser.core_groups.include?(ag)
  end

Shouldn't it work with the line being commented out (one direction)?

The models look like this:

class CoreUser < ActiveRecord::Base

  has_one :core_page

  has_many :core_artifacts
  has_many :core_rating1s
  has_many :core_rating2s
  has_many :core_messages
  has_many :core_widgets
  has_and_belongs_to_many :core_groups

  validates_presence_of :username, :first_name, :last_name, :email_address, :password_hash
  validates_uniqueness_of :username, :email_address
  [...]

class CoreGroup < ActiveRecord::Base

  has_one :core_page

  has_and_belongs_to_many :core_users
  acts_as_tree :order => :name # sort children by :name

  validates_presence_of :name
  validates_uniqueness_of :name, :scope => :parent_id

thanks,

Felix Natter <felix.natter-HLWg83l9DMfnMc1oM6GxaBvVK+yQ3ZXh@public.gmane.org> writes:

hello,

I have an m:n relationsship (has_and_belongs_to_many) between the models
CoreUser and CoreGroup. So from some rails experience I thought I'd
be able to add a (user,group)-relation simply by doing
agroup.core_users << auser.

Unfortunately the following test-case fails on the last assertion
(unless I include the line that is commented out):

  def test_add_remove
    someuser = CoreUser.create!(:username => 'test2s', ...)
    ag = CoreGroup.find_by_path("/Administrators")
    assert !ag.core_users.include?(someuser)
    assert !someuser.core_groups.include?(ag)

    ag.core_users << someuser
# someuser.core_groups << ag
    assert ag.core_users.include?(someuser)
    assert someuser.core_groups.include?(ag)
  end

Shouldn't it work with the line being commented out (one direction)?

The models look like this:

class CoreUser < ActiveRecord::Base

  has_one :core_page

  has_many :core_artifacts
  has_many :core_rating1s
  has_many :core_rating2s
  has_many :core_messages
  has_many :core_widgets
  has_and_belongs_to_many :core_groups

  validates_presence_of :username, :first_name, :last_name, :email_address, :password_hash
  validates_uniqueness_of :username, :email_address
  [...]

class CoreGroup < ActiveRecord::Base

  has_one :core_page

  has_and_belongs_to_many :core_users
  acts_as_tree :order => :name # sort children by :name

  validates_presence_of :name
  validates_uniqueness_of :name, :scope => :parent_id

here is some additional information from a console session:

AR is like that. if you set an association, rails does not magically update the other in memory object with the reverse association (but it is there in the database of course).

Fred

AR is like that. if you set an association, rails does not magically
update the other in memory object with the reverse association (but it
is there in the database of course).

I don't know if anyone mentioned to throw a 'reload()' call in somewhere...

"Phlip" <phlip2005@gmail.com> writes:

AR is like that. if you set an association, rails does not magically
update the other in memory object with the reverse association (but it
is there in the database of course).

I don't know if anyone mentioned to throw a 'reload()' call in somewhere...

That was it, thanks to both of you! So I should change this code:

clas CoreGroup...
  def add(user)
    core_users << user if !core_users.include?(user)
  end

  def remove(user)
    core_users.delete(user)
  end

to this:

class CoreGroup...
  def add(user)
    core_users << user if !core_users.include?(user)
    user.save
    user.reload # make ActiveRecord update relation info
  end

  def remove(user)
    core_users.delete(user)
    user.save
    user.reload # make ActiveRecord update relation info
  end

? Or is there a method to just reload the relation info?

thanks in advance,

? Or is there a method to just reload the relation info?

I have seen this around:

  user.core_groups(:reload)

If it does what it says, then it only repopulates the controlled array
inside core_groups, without reloading the entire user object, or forcing a
reload of everything else

Insert standard complaint here about how awesome ActiveRecord associations
look just before you put a cycle in your code and discover the risk of dirty
data bugs! --> [ ]

Strictly speaking, I don't believe that it's necessary to call save on
the user object. I realize that you're probably doing this because
the user object may contain unsaved data, but it brings a second
purpose to your method.

Also, if you ever need to use the results of CoreGroup#add or
CoreGroup#remove in subsequent processing then the results are going
to be a little muddied. As coded you will always save/reload the user
regardless of whether the association was added/deleted.
Consequently, the method will always return the result of the
user.reload call. You could get into confusing scenarios if the
results conflicted (e.g., association creaated but reload failed,
association failed but reload succeeded).

Maybe something like this:

def add(user)
  return false if core_users.include?(user)
  if core_users << user
    user.core_groups.reload
    return true
  end
  false
end

For your primary purpose you just need to know that you can call
'reload' on the association proxy to reload only that collection. The
rest of the code only guarantees that you get a pass/fail response
related to adding/removing on the collection.