Has_many :through polymorphic with STI in the polymorphic target tables

has_many :through polymorphic with STI in the polymorphic target
tables

Hello,
In our data model we have Users that can be members of Schools,
Organizations, etc.
The Schools table uses STI to support Colleges and High Schools,

The has_many code is pretty straightforward, here is the school side
(school_memberships inherits from memberships):
(all the models and schema at the bottom or
http://pastie.caboo.se/193409)

class School < ActiveRecord::Base
has_many :school_memberships, :as => :member
has_many :users, :through => :school_memberships
end

Now if I do:

some_school.users it runs the following query:
SELECT users.* FROM users
INNER JOIN memberships ON users.id = memberships.user_id
WHERE ((memberships.member_type = ‘School’) AND (memberships.member_id
= 3) AND ((memberships.type = ‘SchoolMembership’)))

The problem here is that there is no need for a
memberships.member_type column, the memberships.type column is
handling the polymorphism of the table. If I add this column in the
migration anyway, it causes a problem because Rails doesn’t populate
it reliably (it populates it if you add a user to a school, but not if
you add a school to a user – some_user.schools << some_school).

My question, then, is how can I get Rails to not use the
memberships.member_type column without overriding the built in
methods?

I looked for people doing this and found some similar things:
Josh S. -
http://blog.hasmanythrough.com/2006/4/3/polymorphic-through,
and
Pratik Naik - http://m.onkey.org/2007/8/14/excuse-me-wtf-is-polymorphs
but they are focused on making polymorphism work at all (i.e. joining
to multiple tables) and my problem seems to stem from having one of
the target tables have STI on it.

Can anyone point me to where to look for anything on this, or to
someone who has done it?
Here’s what works and does not work when I keep the membership_type
column in more detail:

a_school.users << a_user
it works perfectly and inserts a SchoolMembership into the memberships
table
#<SchoolMembership id: 1, user_id: 1, type: “SchoolMembership”,
member_id: 2, member_type: “School”>
(in this case: #<User id: 1, name: “Joseph”> and #<College id: 2,
name: “NYU”, type: “College”> )

  • here School puts “School” in member_type and “SchoolMembership” in
    the type

But, if I do:
a_user.schools << a_school
the SchoolMembership that gets created does not have a member_type:
#<SchoolMembership id: 1, user_id: 1, type: “SchoolMembership”,
member_id: 2, member_type: nil>

  • User only puts “SchoolMembership” in the type

The Code:

class User < ActiveRecord::Base
has_many :school_memberships
has_many :schools, :through => :school_memberships
end

class School < ActiveRecord::Base
has_many :school_memberships, :as => :member
has_many :users, :through => :school_memberships
end

Table name: schools

id :integer

name :string

type :string

class College < School
end

class Membership < ActiveRecord::Base
belongs_to :user
end

Table name: memberships

id :integer

member_id :integer

member_type :string

type :string

class SchoolMembership < Membership
belongs_to :school, :foreign_key => :member_id
end

Hi,

Okay… first of all, if you don’t have a darn good reason for using
STI on your memberships table then get rid of it.

Second of all, you’re missing some things to tell rails about your
polymorphism. Take a look at the following code (untested, off the
top of my head):

class Membership
belongs_to :user
belongs_to :member, :polymorphic => true
end

Note - that means that membership belongs to a user (duh!) and to a
‘member’ which can be anything - School, Business, Club,
FluffyKitten - any active record object.

class User
has_many :memberships
has_many :schools, :through => :memberships, :source => :member,
:source_type => ‘School’
has_many :clubs, :through => :memberships, :source => :member,
:source_type => ‘Club’
has_many :fluffy_kittens, :through => :memberships, :source =>
:member, :source_type => ‘FluffyKitten’
end

class School
has_many :memberships, :as => :member
has_many :users, :through => :memberships
end

class Club
has_many :memberships, :as => :member
has_many :users, :through => :memberships
end

class FluffyKitten … okay, you get the idea.

Basically, if you want to use a polymorphic belongs_to - you have to
actually declare it as such if you want the free setup of the
polymorph_type field.

I can understand why you went down your original route - and if you
really know what you’re doing you can torture ActiveRecord enough to
do your bidding. But you’d be fighting the framework and it would be
a source of confusion and pain.

IMHO, of course.

Hope that helps,
Trevor

On 5/7/08, Joseph S. [email protected] wrote:

(school_memberships inherits from memberships):
SELECT users.* FROM users

the target tables have STI on it.
#<SchoolMembership id: 1, user_id: 1, type: “SchoolMembership”,
member_id: 2, member_type: nil>
end

name :string

end

Trevor S.
http://somethinglearned.com

Hi Trevor,
Thanks a lot for your reply,
I think I’ve tried it that way and still had troubles.
It seems like the STI in the memberships table is fine and works
smoothly - the STI in the target table is the headache.
The type column in memberships is not the issue,
it’s the member_type column that doesn’t seem to work correctly.

It’s really interesting that actually I don’t think you need the
:polymorphic => true part.
I’ve put that in my membership model before and it doesn’t seem to
change anything (if I hit ‘undo’ once it pops back in so I’ve been
playing with it a lot).
In fact if you read through the comments on Josh S.'s has_many
through blog post about it someone mentions that (it is however 2
years
old):

François Simond – 2006-04-22 15:25:09 :
“While experimenting i just found that belongs_to :publication,
:polymorphic => true has no effect.
You can use belongs_to :foobar, :polymorphic => true or simply delete
the line, it does not change anything.”

Josh S. – 2006-04-24 08:22:22:
“@François: Wow, that’s odd. But some of that association code is
pretty bizarre.”

So really the trouble is that the target table uses STI and the School
model wants a member_type = “School” in the memberships table in
addition to having type = “SchoolMembership” - but the User model only
knows to put: type = “SchoolMembership.”
It would be amazing if School would just look for type =
“SchoolMembership” instead of both which is redundant.
I thought there might be some option that I could add in the school
class to get it to do that.

I could make a before_save filter or something like that to make it
work but if I go down that road I feel like I may as well go back to
the messier/less-DRY stuff I had before and not re-write my model
files with the more elegant (but flawed) new solution.

Thanks a lot for your reply,
Much appreciated,
All the best,
Joseph

Joseph,

okay, in the nicest possible way, and bearing in mind that all I have
to go on is the information you presented about your code… you’re
doing it wrong.

You quoted: http://blog.hasmanythrough.com/2006/4/3/polymorphic-through

I wrote the patch that added the :source_type option to Rails in
response to that post.

So… a) the post is out of date - I fixed what he was talking about,
and b) I really do know what I’m talking about here.

The reason adding :polymorphic => true makes no difference is that
you’ve written your code to ignore it (this is why I gave you full
examples).

You have:

class User < ActiveRecord::Base
has_many :school_memberships
has_many :schools, :through => :school_memberships
end

class Membership < ActiveRecord::Base
belongs_to :user
end

class SchoolMembership < Membership
belongs_to :school, :foreign_key => :member_id
belongs_to :user
end

And you’ve said that “a_user.schools << a_school” doesn’t fill in
member_type.

Of course it doesn’t. Instead it’s doing exactly what you told it to do:

  • create a SchoolMembership
  • assign a_user to school_membership.user
    ** which assigns a_user.id to school_membership.user_id
  • assign a_school to school_membership.school
    ** which assigns a_school.id to school_membership.membership_id

Go back and look at the code I sent you. With my code, this is what
“a_user.schools << a_school” does:

  • create a Membership
  • assign a_user to membership.user
    ** which assigns a_user.id to membership.user_id
  • assign a_school to membership.member
    ** which assigns a_school.id to membership.member_id
    ** and which assigns a_school.class.name to membership.member_type

Regards,
Trevor

On 5/8/08, Joseph S. [email protected] wrote:

:polymorphic => true part.
:polymorphic => true has no effect.
addition to having type = “SchoolMembership” - but the User model only

Okay… first of all, if you don’t have a darn good reason for using

has_many :fluffy_kittens, :through => :memberships, :source =>
has_many :users, :through => :memberships
do your bidding. But you’d be fighting the framework and it would be

handling the polymorphism of the table. If I add this column in the
and
a_school.users << a_user
a_user.schools << a_school
end

type :string

id :integer


Trevor S.http://somethinglearned.com

Trevor S.
http://somethinglearned.com

Hi Trevor,
Thanks a lot for the clarification. I think I have it now.
The :source_type was the piece that I wasn’t getting.
Indeed I was a bit worried about the 2 year old aspect of those blog
entries.

Before I went over the edge with the SchoolMembership class we had:

class User < ActiveRecord::Base
has_many :memberships
has_many :schools, :through => :memberships, :conditions =>
“memberships.member_type = ‘College’ OR memberships.member_of_type =
‘HighSchool’”
end

That was not pretty and we couldn’t use built in methods, etc, so it
felt wrong.
Your active record patch is awesome and thank you for getting me back
on track.
All the best,
Joseph

This forum is not affiliated to the Ruby language, Ruby on Rails framework, nor any Ruby applications discussed here.

| Privacy Policy | Terms of Service | Remote Ruby Jobs