Why do we need a through table explained with Rails?

Written by fer | Published 2020/10/20
Tech Story Tags: relationships | ruby-on-rails | database | optimization | infrastructure | rails | programming | coding

TLDR In a many to many relationship, it's just a table between the entities, but what is the purpose of this table to be between them? In Rails we can create two models and these 2 models can be connected just with a foreign key by Active Record Associations. The user_id is going to be our foreign key, remember that primary keys are unique between models, so in order to reference our User model, we can start saying "A User can have many events but an Event belongs to one single user"via the TL;DR App

In a many to many relationship, it's just a table between the entities, but what is the purpose of this table to be between them?
Let's explain it with an example, in Rails we can create two models and these 2 models can be connected just with a foreign key by Active Record Associations.
rails g model User name  
rails g model Event name user_id:integer
Now that we have created these 2 models (Don't forget to migrate the models with rails db:migrate) Event and User, how do we connect them? Well, the user_id is going to be our foreign key, remember that primary keys are unique between models, so in order to reference our User model. We can start saying "A User can have many events but an Event belongs to one single user". Therefore, to make that happen we need to use the reserved words has_many and belongs_to like this:
class User < ApplicationRecord
    has_many :events
end
class Event < ApplicationRecord
    belongs_to :user
end
Great! Now we can start playing with our associations and Rails will make 2 assumptions :
1) The class of the model your association points to is based directly off of the name of the association.
2) The foreign key in any belongs_to relationship will be called yourassociationname_id.
In our Rails console we can now do something like this:
user = User.new;
user.name = "Fernando"
user.save

event = Event.new
event.name = "The big Party"
event.user = user
Ok, so we created a new User called "Fernando" and we created a new Event called "The big Party" thanks to our belongs_to we can use
event.user
and tell that specific event that it belongs to that specific user. So after we save our event with
event.save
We can get our user for that specific event! as an Active Record with
event.user
and it will return that specific user for that specific event
#<User id: 1, name: "Fernando", created_at: "2020-10-06 17:14:30", updated_at: "2020-10-06 17:14:30">
Now to get all the events that this User has we can do
user.events
#<ActiveRecord::Associations::CollectionProxy [#<Event id: 1, name: "The biggest party", user_id: 1, created_at: "2020-10-06 17:33:01", updated_at: "2020-10-06 17:33:01">]>
Do you see the difference? The difference is that in one query we received an Active_Record while on the other query related to the events we received a CollectionProxy, CollectionProxy is nothing more than an array since a user can have multiple events, but remember an event can have only 1 User.

Now let's complicate things a little bit more...

Imagine that you want your "users to attend many events, but only one user to be the owner for that specific event?" We can think in a way to create another model called
Invitee
, and do another relation between
Invitee
and
Event
but what will be the difference between
Invitee
and
User
? They both will have the same attributes (Invitee,User) the main difference is that one is attending the event and the other one isn't? So we need to think in a way to use our model User to be distinguished as an "Owner" and as an "Invitee".
So we can say something like this: "A User can have many events and can attend many events" and "An Event can have many attendees but it only belongs to one single user ". We can now see clearly that we have a Many/Many relationship between these 2 models. So what approach should we take?
First, we need 2 Foreign-Keys one for the Owner and one for the Attendee, we can add a new column to our Event model with the command:
rails g migration add_column_owner_id_to_events owner_id:integer
Don't forget to migrate it with
rails db:migrate.
Ok, now that we have a new column called
owner_id
we need to specify that instead to look for
user_id
to look for a column called
owner_id 
foreign_key to identify that owner_id for that user is meant to be the host of that event.
class User < ApplicationRecord
    has_many :events, foreign_key: "owner_id", class_name: "Event"
    has_many :attending_events, class_name: "Event"
end
Note here that I changed to
:attending_events
to clarify that he is attending to many events but in order to do that we need to specify the class_name, and it will go ahead and look by default user_id foreign_key into the Event Model. So if we type
User.first.attending_events 
we will be getting the events that the first user is attending into our Rails App.
Now we need to create some owners for those events, lets create 3 more users as we did before and also 1 more event. If you did everything right ... You should now have 3 Users. Now let's assign those events an owner.
ev1 = Event.first
ev2 = Event.second

ev1.owner = User.first
ev2.owner = User.second

# Update the events with the **save** method

ev1.save
ev2.save
class Event < ApplicationRecord
    belongs_to :owner, class_name: "User"
    has_many :attendees,foreign_key: "id", class_name: "User"
end
Now we can see the owner for each event by saying for the first event :
ev1.owner
it will return us the ActiveRecord for that specific event, that's because we are saying to Rails to look for on
owner_id
in the
Event Model
and locate that
id
in User class, but now how can we see the attendees for that specific event? We can just type
ev1.attendees 
and Rails will go to the User class and match with
user.id
since we specificied on
foreign_key: "id"
, else it would be looking for a field called event_id.
Now, let's analyze some problems that we have here ... For example, if we create a new Attendee for the same event, we will need to create the same event but with a different user_id on the Event model, imagine if we have 1,000 attendees we will have 1,000 event instance of the same event, but with different user_ids on the same model making it impossible to query it!

What should we do?

Through table.....
Yes, a Through table is what we need, to keep our attendees in a separate table and to leave our unique Event in its own model. So how does it works?
First let's get one thing straight... Let's remove the
user_id
from Event model with :
rails g migration remove_user_id_from_events user_id:integer
and again.. Don't forget to migrate it with
rails db:migrate
(at this point im going to skip to say rails db:migrate every time we type a command on the terminal )now we have our Event model without the user_id column, remember! that was our field to take our attendees.
Let's generate our new model to work as a communication between User & Event. By conversion this table is called by the name of the 2 models (User & Event) concatenated. So it can be called EventUser, UserEvent. I'm going to call it EventAttendee, since we want to show all of our attendees for a specific event. We generate our new model as before with:
rails g model EventAttendee event_id:integer attendee_id:integer
Now we set up our through table saying that the
event_id
will belong to Event model and that
attendee_id
will belong to User model :
class EventAttendee < ApplicationRecord
    belongs_to :event, class_name: "Event"
    belongs_to :attendee, class_name: "User"
end
Now, let's configure our User model to communicate with our EventAttendee through table.
class User < ApplicationRecord
    has_many :events, foreign_key: "owner_id", class_name: "Event"
    has_many :event_attendees, foreign_key: "attendee_id"
    has_many :attending_events, through: :event_attendees, source: :attendee
end
Ok.. Let's explain what we did here...
has_many :event_attendees, foreign_key: "attendee_id" 
we are saying that a "A user can attend many events" and to search it by
attendee_id
and on
has_many :attending_events, through: :event_attendees, source: attendee
the name of the method will be be
:attending_events 
so we can use it as E.G: User.first.event_attendees and look it through
:event_attendees
,note the
source: ...
attribute? that attribute is neccessary because if not we are going to look for a column called
:attending_events
but in our model EventAttendee our column is called
attendee_id
, so that's why we specify
source: :attendee
to go look for
attendee_id column 
but the method name is called
:attending_events
Now let's configure our Event model to communicate with our EventAttendee through table.
class Event < ApplicationRecord
    belongs_to :owner, class_name: "User"
    has_many :event_attendees, foreign_key: "event_id"
    has_many :attendees, through: :event_attendees, source: :event
end
Let's explain again but now with Event model
has_many :event_attendees, foreign_key: "event_id"
we are saying"A event can have many users(attendees)" that's why we are calling our method
:attendees
so we can use it as E.G: Event.first.attendees and to look it through
:event_attendees
model, since our method is called
:attendees
we specify with source: to look for event_id with
:event
Now we can see each of our events attendees, events owners, the events that the user is attending, and the infrastructure is pretty well organized that let us query single events for the Event model. Just by adding another table through our model in our many to many relationship...

Published by HackerNoon on 2020/10/20