Custom Queries in Rails’ Bi-directional Associations

Scott Bewick
Aug 8, 2017 · 3 min read

Recently while working on a Rails project it became necessary to customize the the bi-directional associations in my user and event classes. It was slightly more complicated than the typical parent/child relationship in a has_many/belongs_to association.

The increased complexity arose from the need for an individual event to not only belong to a user when a user creates an event, but also for an individual event to have many users for users who will attend the event. Additionally, a user needs to have many events they have created and have many events they are attending

So how does an event both belong to a user and have many users? And how does an event have two separate types of users? One way to deal with the event belonging to a user and having many users would be to create a whole new class called Creator but this being Rails, there is a better way.

class Event < ActiveRecord::Base  belongs_to :creator, class_name: 'User'

In the Event class, simply write the belongs_to macro as if creator was its own class but specify that this is, in fact, an instance of the user class only with a different name with: class_name: 'User'

Next make sure to add creator to the database.

class CreateEvents < ActiveRecord::Migration
def change
create_table :events do |t|
t.integer :creator_id

Now I can call event.creator and it will distinguished from a ‘normal’ instance of the User class.

in the User class we add the has_many macro:

class User < ApplicationRecordhas_many :created_events, class_name: 'Event', foreign_key: :creator_id

As with attendee and user, I gave created_events a separate name from events to be more specific so it was necessary to add class_name 'Event' and since a creator has many created events, not a , it is necessary to specify a foreign key of creator_id. Otherwise we could have just used user_id in the database.

For the Event having many users as attendees, this can be accomplished through a join table.

class Event < ActiveRecord::Base  belongs_to :creator, class_name: 'User'
has_many :event_attendees
has_many :attendees, through: :event_attendees

Again the user in this case is attendees so now an event belongs_to a creator and has_many attendees. This sounds much better. The join table is a combination of the event and user class, event_attendees and we have many attendees through event_attendees.

In the EventAttendee model there will two belongs_to statements.

class EventAttendee < ActiveRecord::Basebelongs_to :event 
belongs_to :attendee, class_name: 'User', foreign_key: :attendee_id
end

As you can see, with attendee you must designate that it is an instance of the User class just like with the creator name earlier. This time however, it is also necessary to add the foreign key for attendee attendee_id. The event_attendee migration should contain both event_id and attendee_id

class CreateEventAttendees < ActiveRecord::Migration
def change
create_table :event_attendees do |t|
t.belongs_to :event
t.belongs_to :attendee
t.timestamps null: false
end
end
end

Back in the User class we can finish the other end of that join table.

class User < ApplicationRecord  has_many :event_attendees, foreign_key: 'attendee_id'
has_many :attending_events, through: :event_attendees, source: :event

here I added a has_many to the join table, event_attendees but in the User class as opposed to the Event class, we need to specify that this is for an attendee rather than a ‘normal’ instance of the User class. This join table is specifically for an attendee.

Finally, an instance of the User class will have many events they are attending, done by has_many :attending_events, through: event_attendees, source: event. Since I am using attending_events instead of just events I added the source keyword to specify where the association is coming from. You only need to use this option if the name of the source association cannot be automatically inferred from the association name.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade