🎉 I'm releasing 12 products in 12 months! If you love product, checkout my new blog workingoutloud.dev

Back to home

Through Rails Associations

The previous post, we looked at four of the six supported associations in Rails (belongs_to, has_one, has_many, has_and_belongs_to_many).

In this post, we will covering the has_one :through and has_many :through associations that map model connections through another interim data model.

Source code can be found here.


  1. Read the previous post "Understanding Rails Associations".

Getting started

We will be working off the previous work done. If you do not have that stage, you can clone and work from a designated branch on the repo:

# Clone the project $ git clone demo-rails-associations $ cd demo-rails-associations # Change into the starting point $ git checkout 1-basic-associations

At this stage, our project is now ready for changes.

Through associations

The "through" associations help us to map a relationship between four models.

In our example, we will have it where the first model will have one of a second model that is related through the third model. Going the other way, the second model will have many of the first model through the third. Sound confusing? A practical example should hopefully make more sense of things.

For example, let's model music "gigs" where each gig has many attendees and one artist assigned to that gig.

In this example, the attendees model will "have one" artist through the gig model. In the opposite direction, an artist will have many attendees through a gig.

First, let's create these three models:

$ bin/rails g model Gig title:string $ bin/rails g model Artist name:string $ bin/rails g model Attendee name:string email:string $ bin/rails g model Ticket $ bin/rails db:migrate

At this point, our database should have an entity-relationship diagram that looks like so (including the previous work):

Models without relations

Models without relations

Making our relationships

In order the marry up our models, we need to create a new migration.

# Create the new migration $ bin/rails g migration MapArtistToAttendeesThroughGig

Find the migration file within db/migrate. We need to update it for our relations.

Update the model to look like so:

class MapArtistToAttendeesThroughGig < ActiveRecord::Migration[7.0] def change # Each gig has one artist performing add_reference :gigs, :artist, foreign_key: true # Each ticket matches an attendee to a gig add_reference :tickets, :gig, foreign_key: true add_reference :tickets, :attendee, foreign_key: true end end

Run your migration with bin/rails db:migrate.

Next, we need to update our model files.

In app/models/artist.rb:

class Artist < ApplicationRecord has_many :gigs end

In app/models/attendee.rb:

class Attendee < ApplicationRecord has_many :tickets has_many :gigs, through: :tickets end

In app/models/gig.rb:

class Gig < ApplicationRecord belongs_to :artist has_many :tickets has_many :attendees, through: :tickets end

In app/models/ticket.rb:

class Ticket < ApplicationRecord belongs_to :gig belongs_to :attendee has_one :artist, through: :gig end

At this point, our ERD looks like the following:

Through associations are in

Through associations are in

Seeing the associations in action

Before firing up the console, let's create some seed data. Inside of db/seeds.rb, add the following:

attendee_one = Attendee.create(name: 'Jane', email: 'jane@example.com') attendee_two = Attendee.create(name: 'Jim', email: 'jim@example.com') attendee_three = Attendee.create(name: 'Joey', email: 'joey@example.com') artist_one = Artist.create(name: 'Fresh King Prawns') gig_one = Gig.create(title: 'FKP Gig One', artist_id: artist_one.id) gig_two = Gig.create(title: 'FKP Gig Two', artist_id: artist_one.id) ticket_one = Ticket.create(gig_id: gig_one.id, attendee_id: attendee_three.id) ticket_two = Ticket.create(gig_id: gig_one.id, attendee_id: attendee_one.id) ticket_three = Ticket.create(gig_id: gig_two.id, attendee_id: attendee_two.id)

Seed the database after with bin/rails db:seed.

The above does the following:

  1. Create three "attendees" that we can assign tickets to for a gig.
  2. Create one artist that can have many gigs.
  3. Create two gigs for that artist.
  4. Create three tickets: two for the first gig, one for the other.

Now we can start to see the results. Fire up the Rails sandbox console bin/rails c -s.

Trialling our has_one :through in the Rails console can be done by simply accessing the .artist property on a ticket (as we have now let Rails know about this association through our Ticket model):

irb(main):001:0> ticket_one = Ticket.first (0.1ms) SELECT sqlite_version(*) TRANSACTION (0.1ms) begin transaction Ticket Load (0.1ms) SELECT "tickets".* FROM "tickets" ORDER BY "tickets"."id" ASC LIMIT ? [["LIMIT", 1]] => #<Ticket:0x00007fc172afff38 ... irb(main):002:0> ticket_one.artist Artist Load (0.1ms) SELECT "artists".* FROM "artists" INNER JOIN "gigs" ON "artists"."id" = "gigs"."artist_id" WHERE "gigs"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] => #<Artist:0x00007fc16dc8cae0 id: 1, name: "Fresh King Prawns", created_at: Thu, 17 Mar 2022 05:13:27.532669000 UTC +00:00, updated_at: Thu, 17 Mar 2022 05:13:27.532669000 UTC +00:00>

Thanks to the has_one :through association, we did not have to grab the artist via the gig (which would be done with ticket_one.gig.artist).

As for our has_many :through association, this can be demonstrated with our Gig model:

irb(main):003:0> gig_one = Gig.first Gig Load (0.1ms) SELECT "gigs".* FROM "gigs" ORDER BY "gigs"."id" ASC LIMIT ? [["LIMIT", 1]] => #<Gig:0x00007fc16d4ae260 ... irb(main):004:0> gig_one.attendees Attendee Load (0.1ms) SELECT "attendees".* FROM "attendees" INNER JOIN "tickets" ON "attendees"."id" = "tickets"."attendee_id" WHERE "tickets"."gig_id" = ? [["gig_id", 1]] => [#<Attendee:0x00007fc16daf3698 id: 3, name: "Joey", email: "joey@example.com", created_at: Thu, 17 Mar 2022 05:13:27.525746000 UTC +00:00, updated_at: Thu, 17 Mar 2022 05:13:27.525746000 UTC +00:00>, #<Attendee:0x00007fc16d1676b0 id: 1, name: "Jane", email: "jane@example.com", created_at: Thu, 17 Mar 2022 05:13:27.517678000 UTC +00:00, updated_at: Thu, 17 Mar 2022 05:13:27.517678000 UTC +00:00>]

In the above, we have the example of being able to find the attendees for the gig without having to sift through the tickets!


Today's post demonstrated how to ...

Resources and further reading

Photo credit: adrienolichon

Personal image

Dennis O'Keeffe

  • Melbourne, Australia

Hi, I am a professional Software Engineer. Formerly of Culture Amp, UsabilityHub, Present Company and NightGuru.
I am currently working on Visibuild.


Get fresh posts + news direct to your inbox.

No spam. We only send you relevant content.

Through Rails Associations


Share this post