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 Parked at Loopia)

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 Susser - has_many :through - The other side of polymorphic :through associations, 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

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

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 Susser'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 Susser – 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

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 Susser'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 Susser – 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: has_many :through - The other side of polymorphic :through associations

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

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