Migrations

Exercises

Objective

Oops—we forgot a few fields when we created the movies database table! In addition to the existing fields, a movie could also use a textual description and the date it was released. So we need to modify the database, and that means it's time for a new migration!

Migrations are used not only to create database tables, but also to incrementally modify them. Indeed, almost any modification you can make to a database using an admin tool or the database's native language can be done through a migration. The benefit of using a migration is you end up with a more repeatable (and automated) way for everyone on the project to keep their database in step with the code. And since migrations make it easy to add or change database columns later, we can quickly adapt to new requirements. You'll end up creating many migrations while developing a full-featured Rails app, so it's good to get more practice with them.

Here's what we need to do:

  1. Generate a new migration file that adds two fields to the movies database table

  2. Update the existing movies in the database to have values for the new fields

  3. Change the movie listing page to display the new movie fields

Notice that adding fields to the database has a small ripple effect. This is fairly typical, and it gives us an opportunity to learn how to realign things. It will also help reinforce a few things we learned in previous exercises.

Here are the changes we'll make (in red):

So let's jump right into it...

1. Add New Database Fields

First, we need to add the following columns and types to the movies database:

name type
description text
released_on date

Note that we're using a date type for the released_on field rather than datetime as we did for events. It makes more sense that a movie is released on a date and an event starts at a date and time.

To add those columns to the database, we'll need a new migration file. Now, when we generated the Movie model we also got a migration file for creating the movies database table. This time we'd like to only generate a new migration file. Not surprisingly, this is so common that Rails has a migration generator.

  1. Start by printing the usage information for the migration generator:

    rails generate migration

    At the top you'll see that the generator takes the migration name followed by a list of fields and types (and an optional index) separated by colons:

    rails generate migration NAME [field[:type][:index] field[:type][:index]] [options]

    Note that each field/type pair is specified using the format field:type without any spaces before or after the colon. If a type isn't specified, then the default type is string.

  2. Armed with this helpful information, now use the generator to generate a migration named AddFieldsToMovies with the two fields and types listed above.

    Show Answer

    Hide Answer

    rails g migration AddFieldsToMovies description:text released_on:date

    Running that command should generate a YYYYMMDDHHMMSS_add_fields_to_movies.rb file in the db/migrate directory. We now have our second migration file!

  3. Open the generated migration file and you should see the following:

    class AddFieldsToMovies < ActiveRecord::Migration[6.0]
      def change
        add_column :movies, :description, :text
        add_column :movies, :released_on, :date
      end
    end
    

    This time instead of using create_table the change method is using the add_column method to add both columns to the movies table. The add_column method takes three parameters: the name of the table, the name of the column, and the column type.

    How did the generator know that we wanted columns added to that specific table? That's another naming convention. If you name your migration using the format AddXXXToYYY, Rails assumes you want to add columns with the listed field names and types to the specified table (YYY). Pretty smart!

    It's worth pointing out that you could have generated the migration like this:

    rails g migration add_fields_to_movies

    In this case, because the field names and types weren't listed, you would end up with an empty change method, like so:

    class AddFieldsToMovies < ActiveRecord::Migration[6.0]
      def change
      end
    end
    

    You'd then need to put the add_column lines in the change method.

    So, if you follow the naming conventions, Rails is able to generate the complete migration for you. In cases where the naming convention doesn't make sense, you'll just need to edit the change method yourself.

  4. Now that you have a new migration file, check the migration status by running:

    rails db:migrate:status

    This time you should see two migrations:

    database: db/development.sqlite3
    
     Status   Migration ID    Migration Name
    --------------------------------------------------
       up     20190502122806  Create movies
      down    20190506213706  Add fields to movies

    The second (new) migration has a "down" status because we haven't yet run it. Rails knows about the migration because it's in the db/migrate directory.

  5. Go ahead and run the migration.

    You should get the following output:

    ==  AddFieldsToMovies: migrating ==============================================
    -- add_column(:movies, :released_on, :date)
       -> 0.0006s
    -- add_column(:movies, :description, :text)
       -> 0.0003s
    ==  AddFieldsToMovies: migrated (0.0010s) =====================================

    Great—the migration successfully added both columns! It's important to note that only the second migration was run. The first migration wasn't re-run. Remember, when you run the db:migrate task, Rails looks at all the migration files in the db/migrate directory and only runs the migrations that haven't already been run. In other words, it only runs the migrations that have a status of "down".

  6. Finally, re-check the migration status.

    Show Answer

    Hide Answer

    rails db:migrate:status

    You should see both migrations marked as "up":

    database: db/development.sqlite3
    
     Status   Migration ID    Migration Name
    --------------------------------------------------
       up     20190502122806  Create movies
       up     20190506213706  Add fields to movies

So now we've run two migrations. The first migration created the movies table and the second migration added new columns to that table.

2. Update the Movies

Next we need to update the movies in our database so that they have values for the new fields that we just added. To do that, we'll need to read each movie into the console and assign values to the new description and released_on attributes. And you already know how to do that!

  1. Over in your console session, make sure to load the latest version of your application code by using the reload! command:

    >> reload!
    => true
    
  2. Then find the "Iron Man" movie and assign it to a movie variable.

    Show Answer

    Hide Answer

    >> movie = Movie.find_by(title: "Iron Man")
    

    You should get the following output:

    => #<Movie id: 1, title: "Iron Man", rating: "PG-13", total_gross: 0.585366247e9, created_at: "2019-05-02 12:46:57", updated_at: "2019-05-02 12:54:35", released_on: nil, description: nil>
    

    Notice that the movie now has description and released_on attributes matching the new columns we added to the movies database table. We didn't have to change the Movie model. When the Movie class was declared in the console, ActiveRecord queried the movies table schema and automatically defined attributes for each column.

  3. You probably noticed that the movie's description and released_on attributes have nil values, but go ahead and print them out just for practice.

    Show Answer

    Hide Answer

    >> movie.description
    => nil
    >> movie.released_on
    => nil
    
  4. Now assign values to the description and released_on attributes, and save the movie. For the date, you can assign a string with the format "YYYY-MM-DD" and it will get converted to a date.

    Show Answer

    Hide Answer

    >> movie.description = "Tony Stark builds an armored suit to fight the throes of evil"
    >> movie.released_on = "2008-05-02"
    >> movie.save
    
  5. Next, follow suit by updating the "Superman" movie.

    Show Answer

    Hide Answer

    >> movie = Movie.find_by(title: "Superman")
    >> movie.description = "Clark Kent grows up to be the greatest super-hero"
    >> movie.released_on = "1978-12-15"
    >> movie.save
    
  6. Finally, update the "Spider-Man" movie. This time, update the attributes and save the movie in one fell swoop.

    Show Answer

    Hide Answer

    >> movie = Movie.find_by(title: "Spider-Man")
    >> movie.update(description: "Peter Parker gets bitten by a genetically modified spider", released_on: "2002-05-03")
    

And with that, our database is all set!

3. Update the Movie Listing

The last step is to update the movie listing page so that it displays the new movie fields. You already know how to do this, too!

  1. Refresh the index page and it should come as no surprise that the movies don't appear to have a description or release date.

  2. Fix that by updating the index.html.erb template. Just to keep things simple for now, put each new field in a paragraph tag.

    Show Answer

    Hide Answer

    <p>
      <%= movie.description %>
    </p>
    <p>
      <%= movie.released_on %>
    </p>
    

That completes this feature! We added missing columns to the database and now those changes are being reflected on the movie listing page. It's interesting to note that we didn't need to change the index action in the MoviesController. And that's exactly as it should be!

Remember, the controller is just a middleman between the model and the view. It doesn't concern itself with the details of the model or how the data is displayed in the view. So in this case, because we didn't have to change the controller, we're confident that we have the MVC responsibilities properly divided.

Solution

The full solution for this exercise is in the migrations directory of the code bundle.

Bonus Round

Convention Check

Rails is full of conventions that make apps easier to build, test, change, and ultimately, pass on to the next developer to test, change, and maintain. To practice using the Rails conventions you've learned so far, suppose you wanted your Rails app to be a listing of books for sale.

  1. By convention, the book listing route would map a _______ request for the URL ________ .

    Show Answer

    Hide Answer

    The book listing route would map a GET request for the URL /books.

  2. The name of the controller would be _____________ and the name of the action would be ________.

    Show Answer

    Hide Answer

    The controller would be BooksController and the action would be index.

  3. If we don't explicitly tell the index action the name of the view template to render, what will Rails do by convention?

    Show Answer

    Hide Answer

    Because the name of the action is index, Rails will assume it should look for a view template with a similar name: index.html.erb. Because the name of the controller is BooksController, Rails will look for the view template in the app/views/books directory.

  4. The model would be named _____________.

    Show Answer

    Hide Answer

    The name of the model would be Book (singular).

  5. The corresponding database table would be named _____________.

    Show Answer

    Hide Answer

    The database table would be named books. It is plural because it contains many records (or rows), each representing one book.

  6. If the model was named Person, the database table would be named _________.

    Show Answer

    Hide Answer

    The database table would be people. Yup, Rails is smart enough to figure that out!

  7. After a migration has been run (or applied), its status changes from _______ to _____.

    Show Answer

    Hide Answer

    Its status changes from down to up.

Wrap Up

Any time you have to make a change to the database, think "new migration." Missing a column? Time for a new migration. Need to delete a column? That's another migration. Want to rename a table or column? Yup, migrations can do that, too. In fact, we'll talk even more about migrations a little later in this course.

Indeed, creating new migration files is really common. A typical Rails app will end up with tens, if not hundreds, of migration files where each migration represents an incremental database change. On team projects, all those migration files get checked in to a version control system as part of the Rails project. Then, whenever someone on the project checks out a version of the application, they can get their database schema in sync with the code simply by running rails db:migrate. That's the beauty of migrations. You end up with an automated, repeatable way to make modifications to the database.

OK, so now that we're showing more information on the movie listing page, it looks like we could use a bit of formatting. We'll tackle that using view helpers in the next section.

Dive Deeper

We'll write a couple more migrations a bit later in the course so you see them used in different situations. To learn more about migrations, refer to the Rails Guides: Migrations.