What does has_one :through get me?

Apparently I assumed functionality that doesn't exist for this
association. Should I not be able to reference the target model as an
attribute?

Here are my models:

class User < ActiveRecord::Base
  has_one :membership
  has_one :role, :through => :membership
end

class Membership < ActiveRecord::Base
  belongs_to :user
  belongs_to :role
end

class Role < ActiveRecord::Base
end

But, the "role" attribute for an instance of User always contains nil:

user = User.new

=> #<User id: nil, created_at: nil, updated_at: nil>

user.save!

=> true

membership = user.create_membership

=> #<Membership id: 1, user_id: 1, role_id: nil, created_at:
"2009-05-20 05:40:1
7", updated_at: "2009-05-20 05:40:17">

role = membership.create_role

=> #<Role id: 1, created_at: "2009-05-20 05:40:33", updated_at:
"2009-05-20 05:4
0:33">

user.membership

=> #<Membership id: 1, user_id: 1, role_id: 1, created_at: "2009-05-20
05:40:17"
, updated_at: "2009-05-20 05:40:17">

user.membership.role

=> #<Role id: 1, created_at: "2009-05-20 05:40:33", updated_at:
"2009-05-20 05:4
0:33">

user.role

=> nil

quit

Why does user.membership.role contain a record, but user.role does not?

Thanks for the tip, Manasi. This didn't solve the problem. After
defining them as you suggest I do the following:
    user = User.new
    user.save!
    membership = user.memberships.create
    role = membership.create_role

And then in the debugger:
(rdb:1) user
#<User id: 1, created_at: "2009-05-20 17:34:08", updated_at:
"2009-05-20 17:34:0
8">
(rdb:1) user.memberships.find(:first)
#<Membership id: 996332878, user_id: 1, role_id: nil, created_at:
"2009-05-20 17
:34:08", updated_at: "2009-05-20 17:34:08">
(rdb:1) membership
#<Membership id: 996332878, user_id: 1, role_id: 1, created_at:
"2009-05-20 17:3
4:08", updated_at: "2009-05-20 17:34:08">
(rdb:1) membership.role
#<Role id: 1, created_at: "2009-05-20 17:34:08", updated_at:
"2009-05-20 17:34:0
8">
(rdb:1) user.role
nil

Now the interesting thing is that user.memberships.find(:first) has no
role_id, but membership does. I'm not sure what this means, but I'll
explore it further. I suspect my problem might be cached records,
but:

(rdb:1) user.memberships(true).find(:first)
#<Membership id: 996332878, user_id: 1, role_id: nil, created_at:
"2009-05-20 17
:34:08", updated_at: "2009-05-20 17:34:08">

I thought the 'true' caused this to be reloaded, so I don't have an
explanation for the discrepency.

I'm a little relieved this didn't work, since the combination of
has_many memberships and has_one role doesn't make sense to me.

I don't know if this is the problem, but I think you forgot Role
has_many :memberships.

I don't think methods on the Role class should affect this in anyway.
I should only define the association if I benefit from the methods it
provides, right? Or are there other effects I should be aware of?

Anyways, I gave this a try and the results are the same.

I'll admit, though, that in this case I don't really see that the
Membership model is any use. Why aren't you just connecting User and
Role directly?

Fair question.

You're probably right that this should be redesigned. The reasons I
ended up with this are:
1) Originally I had planned to have a has_many relationship between
user and role. When my plan changed I was lazy and didn't change
models/schema.
2) Personal taste. I don't like to have empty fields in my database
by design. If a record exists, all of the fields should have a
value. This design lets a user have 0 or 1 roles and I test for this
by looking for the existance of a record in the join table. I don't
know why this bothers me and it's probably irrational. :slight_smile:
3) The example is somewhat contrived, though it does mimic an
application I'm writing. I left out all of the fields for these
tables except the IDs so that I could demostrate the problem here.

Now I'm especially stumped. I rewrote this as a has_many :through:

class User < ActiveRecord::Base
  has_many :memberships
  has_many :roles, :through => :memberships
end

class Membership < ActiveRecord::Base
  belongs_to :user
  belongs_to :role
end

class Role < ActiveRecord::Base
  has_many :memberships
end

But when I create a user, membership, and role, I still cannot access
role through user:

    user = User.new
    user.save!
    membership = user.memberships.create
    role = membership.create_role

When I look at the value of user.roles(true) I get an empty array.

Obviously I'm doing something very wrong.

This may be a dumb question, but what version of Rails are you on? I
know that versions earlier than 2.2.2 had some difficulties with
has_one :through...

Yeah, I was looking at some related bug fixes, but I think they are
all incorporated. I'm using 2.3.2. There are a couple things I'll
try later tonight when I have a chance to look again:

First, the debugger is giving me something strange in the
AssociationProxy class in the reload method. After calling reset (it
uses AssociationCollection#reset) @loaded == true according to the
debugger. If loaded really is true, it might expliain this, but I'm
still under the assumption that I'm misinterpreting the debugger, or
the debugger is wrong, because otherwise I misunderstand inheritance
in ruby and there's a bug in rails (this seems less likely). I
haven't been able to duplicate this with a smaller repro, but I'll try
again tonight.

Second, I think it's very likely that my scenario isn't all that
common. And it's very likely that the association implementation
wasn't designed with this in mind. I'm running into this in a unit
test. Instead of creating models in memory, I'll load up data from
the test DB, and I'm guessing user.roles suddenly starts working.
Then at least I know I can work around the problem with test fixtures.

In the meantime, if anyone else has an idea of what's going on, let me
know.

To follow-up, this works fine with test fixtures. It also works fine
if I do things in a slightly different order:

user=User.new
user.build_membership
user.membership.build_role
user.save!

When I do this, user.role no longer returns nil, it returns the role
as expected. I think things weren't cached quite right, and for
whatever reason I couldn't force a proper reload. Maybe a bug in
rails?

Yep, working with AssociationProxy stuff in either the debugger or the
console is nasty. Things have a tendency to happen without being told
to (especially loading the target).

Also, have you tried calling membership.save in your original example?
The code you show will set the FK in membership (via
membership.create_role), but it's not on the DB until you save it...

--Matt Jones

Yes, I figured out what you meant in your original mail. Even when I
used has_many I wasn't able to get anything useful from user.roles.

By debugger I mean the ruby-debug gem, though I've also used the
console for a lot of this too.

This lead me to believe otherwise:
http://guides.rubyonrails.org/association_basics.html#has-one-association-reference

Specifically, "The create_association method returns a new object of
the associated type. This object will be instantiated from the passed
attributes, and the link through its foreign key will be set. In
addition, the associated object will be saved (assuming that it passes
any validations)."

Is the guide wrong about this being saved? In any case, I actually
did try this with the build_association followed by a .save! and got
exactly the same results.

What you're describing would look like:

membership.build_role.save!

What I'm talking about is:

membership.create_role
membership.save

--Matt Jones