Models

Exercises

Objective

In the previous exercise, we brought our web app to life in the browser using a controller and a view. In this exercise, we'll turn our attention to the model with the goal being to store movies in the database.

The objectives of this exercise are to:

  • generate a Movie model

  • use a migration to create a movies database table

  • create three movies and save them in the database

  • read movies from the database

  • update a movie's attributes in the database

  • delete a movie from the database

In the end we'll have a Movie model that we can use to create, read, update, and delete (CRUD) movies in the database. Here's visually what we want:

In the next section we'll actually connect the model to the controller, but that's getting ahead of ourselves. First things first.

1. Generate the Movie Model & Migration

In Rails, a model is simply a Ruby class that lives in the app/models directory. The role of a model is to provide access to application-level data and encapsulate the application's business logic. Although it's not a requirement, most models in a Rails app are connected to an underlying database since that's where application data is typically stored.

Up to this point we've been representing a movie simply as a title string. Now we want a movie to have three attributes: a title, a rating such as PG, and the total gross (in dollars) the movie has earned. So we'll represent the movies in the database with the following fields and types:

name type
title string
rating string
total_gross decimal

To do that, we'll need to create a database table to store the movies and also define a Movie model that accesses that table. Sounds like a lot of tedious configuration work, right? Thankfully, Rails takes all the grunt work off our hands.

In the same way that we used a generator to quickly create a controller in the previous exercise, we'll use another generator to generate the Movie model and the instructions for creating the corresponding database table. And by following a simple set of conventions, Rails lets us avoid any configuration. It's another example of "convention over configuration."

  1. First, just to get a feel for how to use the model generator, print the usage information by typing the following while inside of your flix project directory:

    rails generate model

    At the top you'll see that the generator takes the model name followed by a list of fields and types separated by colons:

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

    For future reference, here's a list of supported database column types:

    • string
    • text
    • integer
    • decimal
    • float
    • boolean
    • binary
    • date
    • time
    • datetime
    • primary_key
    • timestamp
  2. Now use the generator to generate a model named Movie (singular) with the fields and types listed above.

    Show Answer

    Hide Answer

    rails g model movie title:string rating:string total_gross:decimal

    Running that command should generate several files.

  3. Open the generated Movie model in the app/models/movie.rb file and it should look like this:

    class Movie < ApplicationRecord
    end
    

    That's all we got!? Just an (empty) Ruby class! It doesn't have any attributes or methods. Not even a gratuitous comment. The only intriguing part is that it inherits from the ApplicationRecord class. Believe it or not, that's all we need for a model to connect to its underlying database. It's not magic; it's just an example of the power of Rails conventions (and Ruby meta-programming). We'll dig into this a bit more later.

  4. The generator also generated a timestamped database migration file in the db/migrate/YYYYMMDDHHMMSS_create_movies.rb file. The YYYYMMDDHHMMSS timestamp is embedded in the filename so migrations can be kept in chronological order. Open that file and you should see the following:

    class CreateMovies < ActiveRecord::Migration[6.0]
      def change
        create_table :movies do |t|
          t.string  :title
          t.string  :rating
          t.decimal :total_gross
    
          t.timestamps
        end
      end
    end
    

    Think of a migration file as instructions for modifying your database. In this case, when we told the generator we wanted a Movie model that had specific fields (attributes), the generator was smart enough to generate the instructions for creating the corresponding database table. And the instructions are written in Ruby. You have to admit, that's pretty handy!

    All the action happens in the change method. This is where we "change" the database. Since this migration needs to create the movies table, the generated code calls the create_table method and passes it the name of the table as a symbol (:movies). It's important to note that the name of the model is singular (movie) and the name of the database table is plural (movies). Rails uses this simple naming convention to automatically connect the model to the database table.

    The create_table method also has an attached block. Inside of the block, table columns are created by calling methods on the t object which references the table being created. In this case, the generated code calls the appropriate methods to create the three columns we asked for when we ran the generator. The generator also added the t.timestamps line which is a shortcut that ends up creating two additional columns: created_at and updated_at. Finally, although there's no line for it here, an id column will be automatically created. We'll see a bit later how Rails takes care of populating these additional columns.

    Think about that: You can express your migrations in generic Ruby code, and Rails automatically translates the code into a language the database understands. So no matter which database you use—SQLite, MySQL, PostgreSQL, Oracle, or the like—the same migration files will work on any of them. And that's pretty darn convenient!

2. Run the Migration

OK, so we've generated the instructions (migration) for creating the database table, but the generator doesn't actually carry out the instructions. To do that, we need to run the migration.

  1. Before we pull the lever, you might be wondering which database will get changed when the migration is run. As usual, Rails is one step ahead of us. Peek inside the config/database.yml file and you should see something like the following (we've removed any comments):

    default: &default
      adapter: sqlite3
      pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
      timeout: 5000
    
    development:
      <<: *default
      database: db/development.sqlite3
    
    test:
      <<: *default
      database: db/test.sqlite3
    
    production:
      <<: *default
      database: db/production.sqlite3

    This YAML file tells Rails which database to use depending on which environment it's running in: development, test, or production. By default, your Rails app runs in the development environment. We'll talk more about the other environments a bit later in the course.

    The default stanza is special: it specifies the configuration information that's common across all environments. Then each environment stanza references the default configuration to pull in those bits of configuration. In other words, using default avoids having to duplicate all the common lines in each environment stanza.

    Notice that SQLite is configured as the default development database. SQLite is a lightweight, single-user database that works great for development. SQLite databases are stored in files, and the default development database will live in the db/development.sqlite3 file. That file doesn't exist (yet).

    You generally don't need to mess around with database.yml until you're ready to deploy your app to a production environment. When that time comes, you'll need to change the production settings to use another database such as MySQL, PostgreSQL, Oracle, or the like. But we'll talk more about that when we get to deployment.

    The takeaway is we don't have to worry about configuring a database!

  2. Now on to running the migration. This is a fairly common task that Rails automates for you. To see all the tasks at your disposal, inside the flix application directory type:

    rails -T

    The resulting list of tasks is fairly long, so here's a tip: You can filter it down to only show the database-specific tasks by adding db after the -T option, like so

    rails -T db

    From that list of tasks, identify the task that migrates the database and run it.

    You should see the following output:

    == 20190502122806 CreateMovies: migrating =============
    -- create_table(:movies)
       -> 0.0010s
    == 20190502122806 CreateMovies: migrated (0.0011s) ====

    Cool! Running the task executed the instructions in our migration file. It did that by looking at all the migration files in the db/migrate directory and running any migrations that had not already been run. In this case, we only have one migration file to run. To "run" the migration, the change method in that file was automatically called which created the movies database table.

  3. Next, check the status of the migration by running the db:migrate:status task:

    rails db:migrate:status

    You should get the following output, though your migration id will be different:

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

    Notice the word "up" in the Status column. That means that the migration has been run (applied). So far, so good!

  4. Now try running the migration again using:

    rails db:migrate

    This time you shouldn't see any output. That's because all the migrations in the db/migrate directory have already been run. Rails is clever about keeping track of which migrations have already run, and only running migrations that haven't already been applied to the database. At this point we only have one migration, but in future exercises we'll create more migrations.

3. Create a Few Movies

Now that we have the movies database table and a Movie model, we can use the model to create, read, update, and delete movies in the database. We don't yet have a web interface for these operations, so we'll use the Rails console in the meantime. The console lets you interact with your app from the command line, and you get instant feedback without having to launch a browser.

Let's start by putting a few movies in the database...

  1. While still inside of your flix directory, fire up a Rails console session by typing:

    rails console

    Yup, there's a shortcut for that:

    rails c

    Once it starts, you'll see a prompt. (We'll show the prompt as >>, but don't worry if your prompt looks different.) You're now in an irb session, so you can type in any Ruby expression. For example, type the following to get a random number:

    >> rand(100)
    

    Notice when you hit Return, the Ruby expression you typed in is evaluated and the result is printed out on the subsequent line (=>) like this

    => 82
    
  2. What's unique (and very powerful) about this irb session is that it automatically loads the Rails environment. That means you can access components of your application, such as your models. For example, go ahead and try this:

    >> Movie.all
    

    You should get the following output:

    Movie Load (0.2ms)  SELECT "movies".* FROM "movies" LIMIT ?  [["LIMIT", 11]]
    => #<ActiveRecord::Relation []>
    

    Notice that the console prints out the SQL that was executed. In this case, we called the all method and behind the scenes Rails generated a SQL SELECT statement to query all the movies in the movies database table. We don't have any movies in our database yet, so an empty array was returned.

    So right off the bat it's looking like the Movie model already has some smarts about the database. Let's drill down a little deeper...

Create the First Movie

  1. Start by instantiating a new Movie object and assigning it to a variable named movie so we can use it later:

    >> movie = Movie.new
    

    You should get the following output:

    => #<Movie id: nil, title: nil, rating: nil, total_gross: nil, created_at: nil, updated_at: nil>
    

    Hey, this is interesting! Notice that this new object has attributes for every column in the movies table: id, title, rating, total_gross, created_at, and updated_at. The values are all nil, but the attributes exist.

    Wait just a doggone minute! Let's look back at the definition of the Movie class:

    class Movie < ApplicationRecord
    end
    

    There's nothing in this code that would indicate that the Movie class knows anything about the movies database table. So, how did it "magically" get all those attributes? The answer isn't as impressive as magic, but it's still very cool.

    The real key lies in the Movie class being a subclass of the ApplicationRecord class (defined in the app/models/application_record.rb file) which in turn is a subclass of the ActiveRecord::Base class. ActiveRecord is a library (gem) that's included with Rails. It does all the heavy lifting when it comes to interacting with the database. So because Movie is a subclass of ApplicationRecord, it inherits special database powers from the ActiveRecord::Base class.

    Here's what happens: When the Movie class is declared (like we did in the console), Rails looks at the class name (Movie) and assumes it should be mapped to a database table with the plural form of the name (movies). Then it goes off and queries the movies table schema to see which columns exist. It figures we probably want to be able to read and write values for each of those columns. So, through a nifty bit of meta-programming, ActiveRecord dynamically defines attributes for each column. And that's why in the output above we see those attributes on the new Movie object.

    So, without any configuration, the Movie model has attributes that reflect the columns in the movies database table. Now we can use those attributes to finish creating our movie.

  2. Returning to the movie object in the console, all of its attributes currently have nil values. Let's fix that. Start by assigning a value to the title attribute, like so:

    >> movie.title = "Iron Man"
    

    Then use the attribute to print the movie's title:

    >> movie.title
    

    What's neat about this is you read and write a model's attributes exactly as you would any attributes of a standard Ruby class. The only difference here is that you didn't have to define the attributes yourself. ActiveRecord takes care of that for you.

  3. Now it's your turn. Assign values for the movie's rating (a string) and total_gross (a number). Then turn around and print each of their values.

    The type of the total_gross attribute is a BigDecimal, so to print it in a friendly format you'll need to use movie.total_gross.to_s.

    >> movie.rating = "PG-13"
    >> movie.total_gross = 585366247
    
    >> movie.rating
    >> movie.total_gross.to_s
    
  4. With those attributes set, we're ready to put the movie in the database. Remember, we instantiated the Movie object by calling the new method, but that doesn't create a new movie in the database. To save the movie to the database, you have to explicitly call save at the end. Go ahead and do that now, and make sure to check out the generated SQL.

  5. What about those other columns: id, created_at, and updated_at?

    Print the value of the id attribute:

    When the movie was created, Rails automatically assigned a unique id as the primary key for the row in the database where this movie is stored. (Don't worry if the id of your movie is different.)

    Then print the values of the created_at and updated_at attributes:

    Show Answer

    Hide Answer

    >> movie.created_at
    
    >> movie.updated_at
    

    When the movie was created, Rails automatically put a timestamp in the created_at and updated_at columns.

  6. In addition to having dynamic attributes, the Movie class also inherits a number of methods for conveniently accessing the database. For example, we've already used the all method to fetch all the movies from the database. Use the all method again to verify that the movie we just created exists.

    You should get an array that contains the "Iron Man" movie.

  7. You can also count all the movies in the database. Any guess as to which method does that for you?

    Show Answer

    Hide Answer

    >> Movie.count
    => 1
    

Create A Second Movie

OK, now let's take what we learned and apply it toward putting a second movie in the database. We'll do it slightly differently this time.

  1. To create our first movie, we used the new method and assigned attributes individually. This time create a second movie by calling the new method and passing it a hash with the following attribute names and values:

    name value
    title "Superman"
    rating "PG"
    total_gross 300451603

    Make sure to save the movie to the database.

    Show Answer

    Hide Answer

    >> movie = Movie.new(title: "Superman", rating: "PG", total_gross: 300451603)
    >> movie.save
    
  2. Now you should have two movies in the database. Use a method to verify the count.

    Show Answer

    Hide Answer

    >> Movie.count
    => 2
    
  3. Then use another method to fetch all the movies in the database.

    The returned array should have the "Iron Man" and "Superman" movies.

Create A Third Movie

Let's try that again, this time doing it all in one fell swoop...

  1. Create a third movie by calling the create method and passing it a hash of the following attribute names and values:

    name value
    title "Spider-Man"
    rating "PG-13"
    total_gross 825025036

    The create method instantiates an object in memory and, if it's valid, automatically inserts it in the database. In other words, you don't have to call save if you use create.

    >> Movie.create(title: "Spider-Man", rating: "PG-13", total_gross: 825025036)
    
  2. Check that you now have three movies in the database.

    Show Answer

    Hide Answer

    >> Movie.count
    => 3
    
  3. Finish off by fetching all the movies.

    The returned array should have the "Iron Man", "Superman", and "Spider-Man" movies.

4. Update a Movie

Now suppose we need to update a movie's information in the database. We can use the Movie model to do that, too.

  1. First we need to read the movie we want to update into the console. The Movie class inherits a number of methods for finding a specific movie. For example, the find method takes an id as the parameter and returns the movie with that primary key.

    Still in the Rails console, find the "Iron Man" movie by its primary key (id) and assign the resulting object to a variable named iron_man.

    Show Answer

    Hide Answer

    >> iron_man = Movie.find(1)
    
  2. Now change the movie's title to "Iron Man 2" and double its total gross. Make sure to save the changes to the database.

    Show Answer

    Hide Answer

    >> iron_man.title = "Iron Man 2"
    >> iron_man.total_gross *= 2.0
    >> iron_man.save
    
  3. Since we updated the record, we'd expect the updated_at column to have a new timestamp. Check that it was updated.

    Show Answer

    Hide Answer

    >> iron_man.updated_at
    
  4. Now let's suppose we want to change the movie's title and total gross in one operation. Use the update method to change the title back to "Iron Man" and the total gross to 585366247.

    Call update with a hash of attributes to update the movie and save it in one fell swoop.

    >> iron_man.update(title: "Iron Man", total_gross: 585366247)
    

    The update method is a handy convenience, but it exists for a far more important reason. In most cases you'll create and update records from data submitted in an HTML form. (We'll do that later.) When the form is submitted, the form data is captured in a hash of attribute names and values. It's no coincidence then that the update method takes a hash of attribute names and values.

  5. To check your work, print the values of the title and total_gross attributes to make sure they were updated.

    Show Answer

    Hide Answer

    >> iron_man.title
    >> iron_man.total_gross.to_s
    

5. Delete a Movie

Finally, we might want to delete a movie from the database. Again, the Movie model inherits methods that make that really easy.

  1. As before, the first step is to read the movie we want to delete into the console. Let's suppose we want to delete the "Spider-Man" movie, but we don't know its id. Thankfully, ActiveRecord automatically creates a set of finder methods based on the names of database columns.

    Use the find_by method to find the "Spider-Man" movie and assign the resulting object to a variable named spider_man.

    Show Answer

    Hide Answer

    >> spider_man = Movie.find_by(title: "Spider-Man")
    
  2. Then use the destroy method to delete the "Spider-Man" movie from the database.

    Show Answer

    Hide Answer

    >> spider_man.destroy
    
  3. To check your work, call the find_by method again to verify that the movie no longer exists in the database. You should get a response of nil.

    Show Answer

    Hide Answer

    >> Movie.find_by(title: "Spider-Man")
    
  4. Finally, just to practice what you've learned, recreate the "Spider-Man" movie using any of the creation methods you used previously.

  5. When you're done, exit the console session by typing, wait for it... exit (or Ctrl-D):

    >> exit
    

Solution

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

Bonus Round

Poke Around in the Database

Running the first migration automatically created the database in the db/development.sqlite3 file. Bear in mind, you generally won't need to interact directly with the database. Instead, you'll use a model to access the database as we've done in this exercise. But just to better understand what the migration did for us, we'll grovel around in the database for a moment.

  1. To see what's inside the db/development.sqlite3 file, change into your flix application directory and start an interactive console for the database by typing:

    rails dbconsole

    This command figures out which database you're using and opens a command-line interface to that database. We're using SQLite, so once it starts you'll see the following prompt where you can type in commands:

    sqlite>
  2. First let's list the names of tables in the database. SQLite commands begin with a dot ("."), so at the prompt type .tables like so and hit Return:

    sqlite> .tables

    You should see the names of two tables: movies and schema_migrations.

  3. Now, to see the schema for the movies table, type:

    sqlite> .schema movies

    You should see the following output:

    CREATE TABLE "movies" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "title" varchar, "rating" varchar, "total_gross" decimal, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL);

    This is the SQL command that was executed to actually create the movies table in the SQLite database. What's interesting about this is that our migration file was written in Ruby, but then when the migration ran it created the table with a schema appropriate for the configured database (SQLite in this case). For example, the string type we specified in the migration got translated into a varchar(255) type in SQLite. In other words, Rails uses the database adapter to figure out the appropriate column type depending on the configured database.

  4. What about that schema_migrations table that showed up earlier? It's a special table that Rails uses to figure out which migrations have already been run. To show its schema, type:

    sqlite> .schema schema_migrations

    You should see the following output:

    CREATE TABLE "schema_migrations" ("version" varchar NOT NULL PRIMARY KEY);

    It's a table with a single column named version. To see what's in the table, type the following SQL command with a trailing semi-colon:

    sqlite> select * from schema_migrations;

    You should see a fairly big number. That's actually a timestamp. In fact, it's the same timestamp that's embedded in our migration filename. That's how Rails keeps track of which migrations in the db/migrate directory have already been run!

  5. Finally, to quit the SQLite console session, use the .quit command:

    sqlite> .quit

Wrap Up

Now you can easily create, read, update, and delete (CRUD) movies in the database! That's pretty exciting. It may seem like we took a lot of steps to get here, but look back and you'll notice we didn't do much typing and we were able to:

  • generate a model and migration
  • run a migration
  • create database records three different ways
  • fetch records from the database
  • update records in the database
  • delete records from the database

That's the power of Rails conventions. Initially, all the conventions can feel quite magical, and even confusing. But once you get comfortable with them, the conventions make things go a lot smoother.

In the next section we'll connect our Movie model to the MoviesController so that the list of movies in the browser reflects the movie information in the database.