How can I make a many-to-many relationship with the same model in rails?
For example, each post is connected to many posts.
Sign Up to our social questions and Answers Engine to ask questions, answer people’s questions, and connect with other people.
Login to our social questions & Answers Engine to ask questions answer people’s questions & connect with other people.
Lost your password? Please enter your email address. You will receive a link and will create a new password via email.
Please briefly explain why you feel this question should be reported.
Please briefly explain why you feel this answer should be reported.
Please briefly explain why you feel this user should be reported.
There are several kinds of many-to-many relationships; you have to ask yourself the following questions:
(If post A is connected to post B, then post B is also connected to post A.)
That leaves four different possibilities. I’ll walk over these below.
For reference: the Rails documentation on the subject. There’s a section called “Many-to-many”, and of course the documentation on the class methods themselves.
Simplest scenario, uni-directional, no additional fields
This is the most compact in code.
I’ll start out with this basic schema for your posts:
For any many-to-many relationship, you need a join table. Here’s the schema for that:
By default, Rails will call this table a combination of the names of the two tables we’re joining. But that would turn out as
posts_postsin this situation, so I decided to takepost_connectionsinstead.Very important here is
:id => false, to omit the defaultidcolumn. Rails wants that column everywhere except on join tables forhas_and_belongs_to_many. It will complain loudly.Finally, notice that the column names are non-standard as well (not
post_id), to prevent conflict.Now in your model, you simply need to tell Rails about these couple of non-standard things. It will look as follows:
And that should simply work! Here’s an example irb session run through
script/console:You’ll find that assigning to the
postsassociation will create records in thepost_connectionstable as appropriate.Some things to note:
a.posts = [b, c], the output ofb.postsdoes not include the first post.PostConnection. You normally don’t use models for ahas_and_belongs_to_manyassociation. For this reason, you won’t be able to access any additional fields.Uni-directional, with additional fields
Right, now… You’ve got a regular user who has today made a post on your site about how eels are delicious. This total stranger comes around to your site, signs up, and writes a scolding post on regular user’s ineptitude. After all, eels are an endangered species!
So you’d like to make clear in your database that post B is a scolding rant on post A. To do that, you want to add a
categoryfield to the association.What we need is no longer a
has_and_belongs_to_many, but a combination ofhas_many,belongs_to,has_many ..., :through => ...and an extra model for the join table. This extra model is what gives us the power to add additional information to the association itself.Here’s another schema, very similar to the above:
Notice how, in this situation,
post_connectionsdoes have anidcolumn. (There’s no:id => falseparameter.) This is required, because there’ll be a regular ActiveRecord model for accessing the table.I’ll start with the
PostConnectionmodel, because it’s dead simple:The only thing going on here is
:class_name, which is necessary, because Rails cannot infer frompost_aorpost_bthat we’re dealing with a Post here. We have to tell it explicitly.Now the
Postmodel:With the first
has_manyassociation, we tell the model to joinpost_connectionsonposts.id = post_connections.post_a_id.With the second association, we are telling Rails that we can reach the other posts, the ones connected to this one, through our first association
post_connections, followed by thepost_bassociation ofPostConnection.There’s just one more thing missing, and that is that we need to tell Rails that a
PostConnectionis dependent on the posts it belongs to. If one or both ofpost_a_idandpost_b_idwereNULL, then that connection wouldn’t tell us much, would it? Here’s how we do that in ourPostmodel:Besides the slight change in syntax, two real things are different here:
has_many :post_connectionshas an extra:dependentparameter. With the value:destroy, we tell Rails that, once this post disappears, it can go ahead and destroy these objects. An alternative value you can use here is:delete_all, which is faster, but will not call any destroy hooks if you are using those.has_manyassociation for the reverse connections as well, the ones that have linked us throughpost_b_id. This way, Rails can neatly destroy those as well. Note that we have to specify:class_namehere, because the model’s class name can no longer be inferred from:reverse_post_connections.With this in place, I bring you another irb session through
script/console:Instead of creating the association and then setting the category separately, you can also just create a PostConnection and be done with it:
And we can also manipulate the
post_connectionsandreverse_post_connectionsassociations; it will neatly reflect in thepostsassociation:Bi-directional looped associations
In normal
has_and_belongs_to_manyassociations, the association is defined in both models involved. And the association is bi-directional.But there is just one Post model in this case. And the association is only specified once. That’s exactly why in this specific case, associations are uni-directional.
The same is true for the alternative method with
has_manyand a model for the join table.This is best seen when simply accessing the associations from irb, and looking at the SQL that Rails generates in the log file. You’ll find something like the following:
To make the association bi-directional, we’d have to find a way to make Rails
ORthe above conditions withpost_a_idandpost_b_idreversed, so it will look in both directions.Unfortunately, the only way to do this that I know of is rather hacky. You’ll have to manually specify your SQL using options to
has_and_belongs_to_manysuch as:finder_sql,:delete_sql, etc. It’s not pretty. (I’m open to suggestions here too. Anyone?)