Model Validations

Exercises

Objective

While creating and updating movies using the web forms, you may have noticed that you can create or update a movie with a blank title. In fact, the app will happily accept other not-so-desirable values, such as "Lots of Money" for the total gross amount. That's a big problem! Without validating what gets entered in the web forms, our database can quickly become corrupted with bad (invalid) data which can also trigger nasty little bugs.

To prevent bad data from getting into the database, we'll declare validations. Validations are a type of business rule: they ensure the integrity of your application data and prevent it from being saved in an invalid state. Since models are the gatekeeper to application data, the movie-related validations belong in our Movie model.

Over the next two exercises, we'll need to make changes in the model, view, and controller:

  • In the Movie model, we'll declare reasonable data validations.

  • Then we'll change the create and update controller actions to handle the cases where the movie wasn't saved in the database because it's not valid.

  • Finally in the view (the form) we'll display any validation errors to give the customer specific feedback so they can make the necessary corrections.

1. Declare Movie Validations

Let's start in the Movie model and work our way back out to the web interface. We'll use the built-in validations that are included with Rails. As you work through this section you might find it helpful to peek at the documentation for the validates method.

Use built-in model validations to enforce the following rules about a movie:

  1. Values for the fields title, released_on, and duration must be present.

    Show Answer

    Hide Answer

    validates :title, :released_on, :duration, presence: true
    
  2. The description field must have a minimum of 25 characters.

    Show Answer

    Hide Answer

    validates :description, length: { minimum: 25 }
    
  3. The total_gross field must be a number greater than or equal to 0.

    Show Answer

    Hide Answer

    validates :total_gross, numericality: { greater_than_or_equal_to: 0 }
    
  4. The image_file_name field must be formatted so that the file name has at least one word character and a "jpg" or "png" extension. The regular expression syntax for this one is kinda tricky, so go ahead and copy in the following:

    validates :image_file_name, format: {
      with: /\w+\.(jpg|png)\z/i,
      message: "must be a JPG or PNG image"
    }
    
  5. Finally, the rating field must have one of the following values: "G", "PG", "PG-13", "R", or "NC-17". Go ahead and add a Ruby constant named RATINGS that contains an array of strings like so:

    RATINGS = %w(G PG PG-13 R NC-17)
    

    Can you find a built-in validation that validates whether a value is included in (hint!) a specified array?

    Show Answer

    Hide Answer

    validates :rating, inclusion: { in: RATINGS }
    
  6. To make selecting a rating easier in the web interface, change the rating form field in app/views/movies/_form.html.erb to use a drop-down to select the rating from the list of possible ratings, like so:

    <%= f.select :rating, Movie::RATINGS, prompt: "Pick one" %>
    

    Then browse to either the create or edit form and you should now see a drop-down of options for selecting a movie rating. We've got our belt and suspenders on now! The form only allows you to select specific ratings and the model validates the rating, too.

2. Experiment with Validations

Now that we have the validations declared in the Movie model, let's try to save an invalid movie using the console and see what happens...

  1. Fire up a Rails console to experiment in.

  2. In the console, instantiate a new Movie object without a title.

    Show Answer

    Hide Answer

    >> movie = Movie.new
    
  3. Now try to save the invalid Movie object to the database.

    The result should be false.

    It's important to understand what happened here. When you call the save method, it automatically runs all the validations. If a validation fails, a corresponding message is added to the model object's errors collection. And if the errors collection contains any messages, then the save is abandoned and false is returned.

    In short, the failed validations are preventing the model from being saved, which is exactly what we want!

  4. We know our movie is invalid because the save method returned false, so there must be some errors waiting for us.

    Inspect the errors by accessing the errors collection.

    There's quite a bit of information in the output. To dig down into the actual error messages, tack on a call to full_messages to get an array of error messages.

    Show Answer

    Hide Answer

    >> movie.errors.full_messages
    

    You can even turn that into a fairly readable English sentence by also tacking on the to_sentence method.

    Show Answer

    Hide Answer

    >> movie.errors.full_messages.to_sentence
    
  5. How would you get only the error messages that are associated with the :title attribute?

    Show Answer

    Hide Answer

    >> movie.errors[:title]
    
  6. Finally, create a valid movie object and save it away.

    Show Answer

    Hide Answer

    >> movie.title = "Hulk"
    >> movie.description = "Bruce Banner transforms into a raging green monster when he gets angry."
    >> movie.released_on = "2003-06-20"
    >> movie.duration = "138 min"
    >> movie.total_gross = 113_107_712
    >> movie.image_file_name = "hulk.png"
    >> movie.rating = "PG-13"
    
    >> movie.save
    

    This time the result should be true. The validations ran again, but this time they all passed so no errors were found in the errors collection. Consequently, the movie was inserted in the database and the call to save returned true.

  7. Since the save was successful, the movie's errors collection must be empty. Just for practice, confirm that by asking the movie if it has any errors.

    Show Answer

    Hide Answer

    >> movie.errors.any?
    

Solution

The full solution for this exercise is in the model-validations directory of the code bundle.

Wrap Up

Perfect! Now whenever you try to create or save a Movie, Rails will run all the validations. The movie only gets saved if the errors collection is empty. So our Movie model now dutifully prevents bad data from getting into our database.

As you build your own app, keep model validations in mind every time you write a migration and add new fields to your database. Rails includes a bunch of common validations, but you can also create your own custom validations when necessary.

Now that we have that foundation, we'll move up into the web interface to handle cases where users enter invalid data.

Dive Deeper

To learn more about validations, and how to write custom validations, refer to Rails Guides: Active Record Validations.