- Understand how and why Active Record implements associations between models.
- Use Active Record migrations and methods to build out a domain model that associates classes via the has-many/belongs-to and the many-to-many (or has-many-through) relationships.
We already know that we can build our classes such that they associate to one another. We also know that it takes a lot of code to do it. Active Record associations allow us to associate models and their analogous database tables without having to write tons of code.
Additionally, Active Record associations make actually working with our associated objects even quicker, neater and easier.
Sounds great, right? Now that we have you totally hooked, let's take a look at how we use these AR associations.
Active Record makes it easy to implement the following relationships between models:
- belongs_to
- has_one
- has_many
- has_many :through
- has_one :through
- has_and_belongs_to_many
We don't need to worry about most of these right now. We'll concern ourselves with relationships that should sound familiar:
- belongs to
- has many
- has many through
In order to implement these relationships we will need to do two things:
- Write a migration that creates tables with associations. For example, if a cat belongs to an owner, the cats table should have an
owner_id
column. - Use Active Record macros in the models.
We'll go through both of these steps together, using our Playlister domain model.
In this walk-through, we'll build building out a domain model for our fictitious music playing app, Playlister. This app will catalogue songs and their associated artists and genres.
We'll have three models: Artists, Songs, and Genres. By writing a few migrations and making use of the appropriate ActiveRecord macros (more on that later), we will be able to:
- ask an Artist about its songs and genres
- ask a Song about its genre and its artist
- ask a Genre about its songs and artists.
The relationships between artists, songs and genres will be enacted as follows:
- Artists have many songs and a song belongs to an artist.
- Artists have many genres through songs.
- Songs belong to a genre.
- A genre has many songs.
- A genre has many artists through songs.
We will build these associations through the use of Active Record migrations and macros.
A song will belong to an artist and belong to a genre. Before we worry about the migration that will implement this in our songs table, let's think about what that table will look like:
id | name | artist_id | genre_id |
---|---|---|---|
2 | Shake It Off | 1 | 1 |
We can see that the songs table will have an artist_id
column and a genre_id
column. We will give a given song an artist_id
value of the artist it belongs to. The same goes for genre. These foreign keys, in conjunction with the ActiveRecord association macros will allow us query to get an artist's songs or genres, a song's artist or genre, and a genre's songs and artists entirely through ActiveRecord provided methods on our classes.
Let's write the migration that will make this happen.
- Create a file,
db/migrations/001_create_song_table.rb
- Write the following migration:
class CreateSongs < ActiveRecord::Migration
def change
create_table :songs do |t|
t.string :name
t.integer :artist_id
t.integer :genre_id
end
end
end
An artist will have many songs and it will have many genres through songs. These associations will be taken care of entirely through AR macros, which we'll get to in a bit. What do we mean by through songs? The table songs is the JOIN
table! Remember that from previous labs? That means that songs has both an artist_id
and a genre_id
to combine those two tables together in a many to many relationship.
Let's take a look at what our artists table will need to look like:
id | name |
---|---|
1 | Taylor Swift |
Our artists table just needs a name
column. Let's write the migration. In db/migrate/002_create_artists_table.rb
:
class CreateArtists < ActiveRecord::Migration
def change
create_table :artists do |t|
t.string :name
end
end
end
A genre will have many songs and it will have many artists through songs. These associations will be taken care of entirely through AR macros, which we'll get to in a bit.
Let's take a look at what our genres table will need to look like:
id | name |
---|---|
1 | pop |
Let's write our migration. In db/migrate/003_create_genres_table.rb
:
class CreateGenres < ActiveRecord::Migration
def change
create_table :genres do |t|
t.string :name
end
end
end
Great! Now go ahead and run rake db:migrate
in your terminal to execute our table creations.
A macro is a method that writes code for us (think metaprogramming). By invoking a few methods that come with Active Record, we can implement all of the associations we've been discussing.
We'll be using the following AR macros (or methods):
Let's get started.
Create a file, app/models/song.rb
. Define your Song
class to inherit from ActiveRecord::Base
. This is very important! If we don't inherit from Active Record Base, we won't get our fancy macro methods.
class Song < ActiveRecord::Base
end
We need to tell the Song
class that it will produce objects that can belong to an artist. We will do it with the belongs_to
macro:
class Song < ActiveRecord::Base
belongs_to :artist
end
Songs also belong to a genre, so we'll use the same macro to implement that relationship:
class Song < ActiveRecord::Base
belongs_to :artist
belongs_to :genre
end
Create a file, app/models/artist.rb
. Define your Artist
class to inherit from ActiveRecord::Base
:
class Artist < ActiveRecord::Base
end
We need to tell the Artist
class that each artist object can have many songs. We will use the has_many
macro to do it.
class Artist < ActiveRecord::Base
has_many :songs
end
And that's it! Now, because our songs table has an artist_id
column and because our Artist
class uses the has_many
macro, an artist has many songs!
It is also true that an artist has many genres through songs. We will use the has_many through
macro to implement this:
class Artist < ActiveRecord::Base
has_many :songs
has_many :genres, through: :songs
end
Create a file app/models/genre.rb
. In it, define a class, Genre
, to inherit from ActiveRecord::Base
.
class Genre < ActiveRecord::Base
end
A genre can have many songs. Let's implement that with the has_many
macro:
class Genre < ActiveRecord::Base
has_many :songs
end
A genre also has many artists through its songs. Let's implement this relationship with the has_many through
macro:
class Genre < ActiveRecord::Base
has_many :songs
has_many :artists, through: :songs
end
And that's it!
Go ahead and run the test suite and you'll see that we are passing all of our tests! Amazing! Our associations are all working, just because of our migrations and use of macros.
Let's play around with our code.
In your console, run rake console
. Now we are in a Pry console that accesses our models.
Let's make a few new songs:
[1]pry(main)> hello = Song.new(name: "Hello")
=> #<Song:0x007fc75a8de3d8 id: nil, name: "Hello", artist_id: nil, genre_id: nil>
[2]pry(main)> hotline_bling = Song.new(name: "Hotline Bling")
=> #<Song:0x007fc75b9f3a38 id: nil, name: "Hotline Bling", artist_id: nil, genre_id: nil>
Okay, here we have two songs. Let's make some artists to associate them to. In the same PRY sessions as above:
[3] pry(main)> adele = Artist.new(name: "Adele")
=> #<Artist:0x007fc75b8d9490 id: nil, name: "Adele">
[4] pry(main)> drake = Artist.new(name: "Drake")
=> #<Artist:0x007fc75b163c60 id: nil, name: "Drake">
So, we know that an individual song has an artist_id
attribute. We could associate hello
to adele
by setting hello.artist_id=
equal to the id
of the adele
object. BUT! Active Record makes it so easy for us. The macros we implemented in our classes allow us to associate a song object directly to an artist object:
[5] pry(main)> hello.artist = adele
=> #<Artist:0x007fc75b8d9490 id: nil, name: "Adele">
Now, we can ask hello
who its artist is:
[6] pry(main)> hello.artist
=> #<Artist:0x007fc75b8d9490 id: nil, name: "Adele">
We can even chain methods to ask hello
for the name of its artist:
[7] pry(main)> hello.artist.name
=> "Adele"
Wow!
Go ahead and do the same for hotline_bling
and drake
.
We can also ask our artists what songs they have. Let's make a second song for adele first:
[8] pry(main)> someone_like_you = Song.new(name: "Someone Like You")
=> #<Song:0x007fc75b5cabc8 id: nil, name: "Someone Like You", artist_id: nil, genre_id: nil>
[8] pry(main)> someone_like_you.artist = adele
=> #<Artist:0x007fc75b8d9490 id: nil, name: "Adele">
Now let's ask adele
for her songs:
[9] pry(main)> adele.songs
=> []
Huh? How can adele
's collection of songs be empty? We associated two songs with adele
! Here's the thing, and this is important to remember:
The model that has_many
is considered the parent. The model that belongs_to
is considered the child. If you tell the child that it belongs to the parent, the parent won't know about that relationship. If you tell the parent that a certain child object has been added to its collection, both the parent and the child will know about the association.
Let's see this in action. Let's create another new song and add it to adele
's songs collection:
[10] pry(main)> rolling_in_the_deep = Song.new(name: "Rolling in the Deep")
=> #<Song:0x007fc75bb4d1e0 id: nil, name: "Rolling in the Deep", artist_id: nil, genre_id: nil>
[11] pry(main)> adele.songs << rolling_in_the_deep
=> [ #<Song:0x007fc75bb4d1e0 id: nil, name: "Rolling in the Deep", artist_id: nil, genre_id: nil>]
[12] pry(main)> rolling_in_the_deep.artist
=> #<Artist:0x007fc75b8d9490 id: nil, name: "Adele">
We added rolling_in_the_deep
to adele
's collection of songs and we can see the adele
knows it has that song in the collection and rolling_in_the_deep
knows about its artist.
Notice that adele.songs
returns an array of songs. When a model has_many
of something, it will store those objects in an array. To add to that collection, we use the shovel operator, <<
, to operate on that collection, treat adele.songs
like any other array.
Let's play around with some genres and our has many through association.
[13] pry(main)> pop = Genre.create(name: "pop")
=> #<Genre:0x007fa34338d270 id: 1, name: "pop">
[14] pry(main)> pop.songs << rolling_in_the_deep
=> [#<Song:0x007fc75bb4d1e0 id: nil, name: "Rolling in the Deep", artist_id: nil, genre_id: nil>]
[15] pry(main)> pop.songs
=> [#<Song:0x007fc75bb4d1e0 id: nil, name: "Rolling in the Deep", artist_id: nil, genre_id: nil>]
[16] pry(main)> rolling_in_the_deep.genre
=> #<Genre:0x007fa34338d270 id: 1, name: "pop">
[17] pry(main)> pop.artists
=> [#<Artist:0x007fa342e34dc8 id: 1, name: "Adele">]
It's working!