Many to Many Relationships in Rails

So me and my awesome Rails Meetup Group have been following Michael Hartl Rails Tutorial chapter by chapter.  More or less we have been discussing a chapter week and see where people are having problems, confusions, or just think something is interesting.

The last meeting was to go over the second half of Chapter 12. There was some confusions on what was really going on in the User Model and what the difference relationships meant.

Relationship/Associations Methods

Lets step back and look at what we had used before with a has_many relationship (association).

This would allow us access a user’s posts through @user.microposts like we used in Chapter 11 of the tutorial.

The magic of Rails obfuscates whats really going on because of some handy conventions we have. If you look at the has_many method documentation you can learn the a bunch of ‘options’  and defaults exist. So let me write the model different way but has the same results.

Options on the has_many method [doc]

  • class_name: Specify the class name of the association. (Model)
  • :foreign_key Specify the foreign key used for the association.
  • :primary_key Specify primary key for the association.
  • :dependent Controls what happens to the associated objects when their owner is destroyed.
  • :dependent  :destroy causes all the associated objects to also be destroyed.

Convention over Configuration

We are creating a relationship on a method named ‘microposts’ that maps to the Micropost model where the User’s primary key (id) is mapped to the foreign key in the micropost table (user_id). Since this is a has_many relationship a collection of Micropost objects will be returned.

We normally don’t supply the class_name, foreign_key, or primary_key because we just use the defaults based on conventions of rails. Setting options means you are configuration your associations. For obvious reasons you want to be careful and not go against conventions unless it makes sense.

Fun Fact: At  this point we could (but don’t) rename :microposts to :posts and it would work since we configured the options needed to set the correct relationship. Meaning we could write @user.posts in our controller and it would return a collection of microposts.  This is why the active/passive methods i’ll show later work.

Many to Many Relationships

There are two ways to create a many-to-many relationship. You can use the :has_and_belongs_to_many or the :has_many :through methods. The choice to choose one over the other has to do if the join table has a model class attached do it or not. Thats it. There is some thought that you shouldn’t use HABTM relationships but i’ll start off with them.

The Role-User Example Using ‘has_and_belongs_to_many’

If we wanted to relate users to roles we’d have to think a both sides of the relationship. Given a single user how many roles can they have (many). Given a single role how many users can they have (many). Thus we have a many-to-many relationship. That means we can’t merely throw the user Primary Key into any models table. Instead we need a join table where we can store both primary keys.

The conventions would mean the User’s primary key would be stored as a foreign key (user_id) in a join table (role_user) which would contain the role_id of the ‘many’ part relationship’. No other intermediate model is involved.

Lets rewrite that again using the options found in the document we could see what we’d have to write if we didn’t have conventions.

The User-User Example Using ‘has_and_belongs_to_many

On Twitter if one users followers a user then we have a relationship between two users. Given a single user how many followers can they have (many). Given a single user, lets call it the phantom user, how many people can follow them (many). So this is just another many-to-many relationship. What gets interesting is we start using terms like following and follower in the relationship. This means we have an active and a passive relationship.

Following normal conventions we’d be declaring this:

We’d have a user_user join table, with user_id and… ummmm user_id? as the PK and FK…. you see why this isn’t going to work. Also using @user.users isn’t very meaningful to us either. So lets call the table relationships and the keys follower_id and following_id.

Conversely, we could reverse the foreign and associated keys and get the ‘followers’ relationship.

The User-User (Relationship) Example Using ‘has many through”

Recall the :has_many :through requires you to have an intermediate model. So what if instead of user_user we had a Relationship model mapped to a relationships table like in Hartl’s Listing 12.3

One the User model we can’t just say the user has many relationships because we aren’t using ‘user_id’ as the key in relationships but follower_id and followed_id.

In the end Hartl split up the ‘active’ and ‘passive’ relationships and make them methods we can use for testing, form building, record creating, and to use in other relationships.

Lets look at the documentation on has_many to see some of these new options.

  • through: Specifies an association through which to perform the query. This can be any other type of association, including other :through associations.
  • source: Specifies the source association name used by has_many :through queries.

So through means we are using an association, which could be a model or  (as we use here) other relationship functions we’ve specified ourselves. We declared the active_relationships relationship (association) and then use it in the has many through association.

I’ll have to play with source a little more to really understand it.

Summary

  • Rails conventions are awesome and they save us a lot of time
  • You can override those conventions with options available on the association methods.
  • There are two ways to define a many-to-many relationship in Rails, and which one you use depends on if you have a model table that is used a join table.
  • Some people think you shouldn’t use HABTM

Leave a Reply

Your email address will not be published. Required fields are marked *