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
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
Seeing the associations in action
Before firing up the console, let's create some seed data. Inside of db/seeds.rb, add the following:
Create three "attendees" that we can assign tickets to for a gig.
Create one artist that can have many gigs.
Create two gigs for that artist.
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!