ActiveRecord Associations

Learning Goals

  • Write migrations in Rails.
  • Explain what a migration is, and how it relates to our schema.
  • Create one-to-many relationships at the database level using foreign keys.
  • Use has_many and belongs_to to create one-to-many relationship at the model level.
  • Create instance methods on a Rails model

Vocab

  • Migration
  • Schema
  • Relationships

Set Up

This lesson builds off of the Handling Requests Lessons. You can find the completed code from this lesson on the handling_requests branch of this repo

TDD Version

This tutorial does not make use of any testing. You can find a version of this tutorial using the TDD workflow here

WarmUp

  • In your own words, what is a migration?
  • What are some things that we can do with a migration?
  • What is the relationship between a migration and our database?

Models, Migrations, and Databases in Rails

In this lesson, we’ll be adding to our new SetList Rails app to demonstrate a one-to-many relationship.

We’ll add a table artists to our database, and connect them to our existing songs table. What might the relationships look like?

At the Database Level: Artists

We want to be able to create some artists with a name, so we’ll add an “Artists” table to our database in order to store this data. Take a look at our db/schema.rb; at this point, it should look something like this:

ActiveRecord::Schema.define(version: 20190430171832) do

  # These are extensions that must be enabled in order to support this database
  enable_extension "plpgsql"

  create_table "songs", force: :cascade do |t|
    t.string "title"
    t.integer "length"
    t.integer "play_count"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end
end

Currently, our schema does not have an Artists table, so we are going to have to update it. Whenever we need to alter the database schema, we are going to write a migration.

From the command line, run rails g migration CreateArtists name:string

The migration generator creates a migration and if we follow the working convention for rails the migration will be pre-populated.

Let’s look at the migration inside of db/migrate. We will also add the line t.timestamps to add created_at and updated_at timestamps to our table.

class CreateArtists < ActiveRecord::Migration[5.2]
  def change
    create_table :artists do |t|
      t.string :name

      t.timestamps
    end
  end
end

This migration looks good. Open up the schema.rb file again. Do you see the new “artists” table? Take a look at the version number next to ActiveRecord::Schema.define(version: . This version number should match the version of our latest migration file, but it doesn’t! Both of these mean that we have migrations that have not been run yet. Let’s do that now:

rails db:migrate

Now, if we go back to our schema, we should see create_table 'artists'. Our migrations have been applied.

Now that we have a database table, we’ll need to create a new model that can connect to this database table:

# app/models/artist.rb

class Artist < ApplicationRecord

end

Now that we have our model and database table set up, we should be able to create some new Artists using the rails console. Open up a new console from the command line using rails c and run the following to create some artists:

Artist.create(name: "Talking Heads")
Artist.create(name: "Prince")
Artist.create(name: "Britney Spears")

We should see some SQL output in our rails console confirming that each Artist is saved to the database. Double check that we can read all of those artists back from the database by running:

Artist.all

and make sure that all of your new Artists are returned.

At the Database Level: Songs

Since each Song should belong to an Artist, each song is going to need a foreign key that references its artist. If we open up our schema file, we don’t see a column for that foreign key on our Songs table, so we are going to need another migration to alter our schema:

rails g migration AddArtistsToSongs artist:references

Take a look at what this migration creates.

class AddArtistsToSongs < ActiveRecord::Migration[5.2]
  def change
    add_reference :songs, :artist, foreign_key: true
  end
end

Don’t forget to run your migration with rails db:migrate. Check your schema file to make sure you Songs table now has an artist_id column.

ActiveRecord Associations

One-to-Many Relationships the Hard Way

Now that we have set up our database, we should be able to create some songs and relate them to artists in the rails console:

prince = Artist.create!(name: 'Prince')
beret = Song.create!(title: 'Raspberry Beret', length: 345, play_count: 34, artist_id: prince.id)
rain = Song.create!(title: 'Purple Rain', length: 524, play_count: 19, artist_id: prince.id)

And what if we want to retrieve all of an Artist’s Songs? The ActiveRecord would like something like this:

prince = Artist.find_by(name: 'Prince')
prince_songs = Song.where(artist_id: prince.id)

Similarly, we could find the Artist for a Song:

purple_rain = Song.find_by(title: 'Purple Rain')
purple_rain_artist = Artist.find(purple_rain.artist_id)

This will work, but working with associated records is something we are going to have to do very often, and writing the queries to retrieve and create records this way can get overly verbose. Luckily ActiveRecord gives us some nice helper methods to make this much easier.

ActiveRecord Associations: The Path to Enlightenment

Rather than do things the hard way, we can create ActiveRecord Associations. In your Song model, add a line to associate it to the Artist Model:

class Song < ApplicationRecord
  belongs_to :artist
end

Similarly, add a line in your Artist model to associate it with the Song model:

class Artist < ApplicationRecord
  has_many :songs
end

Writing our migrations and altering our schema made this association at the database level. Now we have added ActiveRecord Associations to relate them at the Model level.

The association has_many :songs in our Artist model gives us the ability to call .songs on an Artist object. We can use this to more easily associate a Song to an Artist when creating a Song:

prince = Artist.create!(name: 'Prince')
beret = prince.songs.create!(title: 'Raspberry Beret', length: 345, play_count: 34)
rain = prince.songs.create!(title: 'Purple Rain', length: 524, play_count: 19)

We can also use this new .songs method to retrieve all the Songs related to an Artist:

prince_songs = prince.songs

Take a look at the SQL that is generated in the Rails Console when you run this command. You’ll notice that it is very similar to the query we wrote in the last section to retrieve an Artist’s Songs. So under the hood, the same SQL is still being executed, but the code we wrote to make it happen is now much more elegant and concise.

Similarly, the association belongs_to :artist in our Song model gives us the ability to call .artist on a Song object:

beret_artist = beret.artist

Finally, ActiveRecord associations will put some constraints or validations on our Models. Let’s try to create a Song without an Artist:

Song.create!(title: 'Raspberry Beret', length: 345, play_count: 34)

The bang ! on the end of the create method tells ActiveRecord to throw an error if anything goes wrong which is useful when developing or debugging. This command should produce an error that the Artist must exist. Now that a song belongs_to an artist, a song can not exist without an artist. So when we create a Song, we also have to tell it which Artist it belongs to.

Seeds

Now that our App is getting more complex, it would be good to add some seeds. Seeding your database is when you insert a set of data into the database. It is useful to have some seed data when we are experimenting and developing. You could consider what we’ve done in the Rails Console so far as a type of seeding, but doing things manually in the Rails Console can get very tedious, so what we will do instead is write a script to seed our database that we can reuse. Rails comes with a file for us to write this script in db/seeds.rb. Open up that file and add the following:

Song.destroy_all
Artist.destroy_all

prince = Artist.create!(name: 'Prince')
rtj = Artist.create!(name: 'Run The Jewels')
caamp = Artist.create!(name: 'Caamp')
jgb = Artist.create!(name: 'Jerry Garcia Band')
billie = Artist.create!(name: 'Billie Eilish')
lcd = Artist.create!(name: 'LCD Soundsystem')

prince.songs.create!(title: 'Raspberry Beret', length: 345, play_count: 34)
prince.songs.create!(title: 'Purple Rain', length: 524, play_count: 19)

rtj.songs.create!(title: 'Legend Has It', length: 2301, play_count: 2300000)
rtj.songs.create!(title: 'Talk to Me', length: 2301, play_count: 2300000)

caamp.songs.create!(title: '26', length: 940, play_count: 150000)
caamp.songs.create!(title: 'Vagabond', length: 240, play_count: 120000)

jgb.songs.create!(title: 'Aint No Bread In The Breadbox', length: 540, play_count: 12000)
jgb.songs.create!(title: 'The Harder They Come', length: 240, play_count: 120000)

billie.songs.create!(title: 'bury a friend', length: 340, play_count: 1200000)
billie.songs.create!(title: 'bad guy', length: 240, play_count: 100000)

lcd.songs.create!(title: 'Someone Great', length: 500, play_count: 1000000)
lcd.songs.create!(title: 'I Can Change', length: 640, play_count: 100000)

Now that we have our seeds file, we can run it with rails db:seed. Additionally, if we check this file into our version control system, other developers working on this app will be able to easily seed their local databases.

Notice that the first two lines of this seeds file will destroy all Songs and Artists from the database. The reason we want to do this is so that we know we are starting with an empty database every time we want to reseed our database. If we did not have these two lines, this script would create duplicate records every time we reran rails db:seed.

Adding Behaviors to Models

Now, we have two models that are related to each other with has_many and belongs_to, and these models can handle basic CRUD functionality through the methods that they inherit from ActiveRecord - but what if we need our models to be customized to perform behaviors related to our application? For example, what if we want to find the average length of an artist’s songs?

In our Artist model, we can create an instance method to perform this logic:

class Artist < ApplicationRecord
  has_many :songs

  def average_song_length
    songs.average(:length)
  end
end

Because we have has_many :songs in this class, we can call .songs on an Artist instance, and because we have defined an instance method, we can call songs inside of it to get all the Songs associated to the Artist object. We can then chain on the ActiveRecord method .average to average the length column of all of those associated records.

Let’s open the rails console up again and try out our new method:

prince = Artist.find_by(name: 'Prince')
prince.average_song_length

Checks for Understanding

  • What are two different types of table relationships that you might need to implement? In what scenario would you use each?
  • What is the syntax for the following migrations in Rails?
    • Create a table
    • Add a column to a table, with or without a data type
    • Add a reference from one table to another
  • What does a has_many association in a model do?
  • What does a belongs_to association in a model do?

Lesson Search Results

Showing top 10 results