Through Rails Associations
The previous post, we looked at four of the six supported associations in Rails (
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.
- Read the previous post "Understanding Rails Associations".
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.
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
Next, we need to update our model files.
class Artist < ApplicationRecord has_many :gigs end
class Attendee < ApplicationRecord has_many :tickets has_many :gigs, through: :tickets end
class Gig < ApplicationRecord belongs_to :artist has_many :tickets has_many :attendees, through: :tickets end
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:
attendee_one = Attendee.create(name: 'Jane', email: 'email@example.com') attendee_two = Attendee.create(name: 'Jim', email: 'firstname.lastname@example.org') attendee_three = Attendee.create(name: 'Joey', email: 'email@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
The above does 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.
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
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: "firstname.lastname@example.org", 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: "email@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
1,200+ PEOPLE ALREADY JOINED ❤️️
Get fresh posts + news direct to your inbox.
No spam. We only send you relevant content.