Through Rails Associations

Following up on the previous post, we will be looking at the last through associations that come baked into Rails.

On this page

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.

Prerequisites

  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!

Summary

Today's post demonstrated how to ...

Resources and further reading

Photo credit: adrienolichon