Lesson Monday

Databases are only as useful as the data they contain. For this reason, it's very important to prevent bad data from getting into our databases. For instance, imagine if we have a user sign up for our application. We want to make sure that the user enters a first name, last name, birthdate and valid email address. If the email address is missing, we can't do much with this data and this row in the database is pretty much useless.

One way to ensure that we have good data is to use validations. A validation checks to see if data is valid before it gets committed to the database. A common example is a validation to check the presence of a field. For instance, if a user doesn't enter an email address, the validation would fail and the user would be notified of the error.

Active Record makes it easy to add validations to a Rails application. In fact, our application already has a built-in validation: a song must belong to an album.

Let's add validations to ensure that an album has a name. We'll include tests that take advantage of shoulda-matchers and additional code in our form views to display validation errors to users.

Using shoulda-matchers to Test Validations


Let's start with a spec. Fortunately, shoulda-matchers provides ready-made specs for validations:

spec/models/album_spec.rb
describe Album do
  ...
  it { should validate_presence_of :name }
end

As expected, this test will fail:

Album should validate that :name cannot be empty/falsy
     Failure/Error: it { should validate_presence_of :name }

       Expected Album to validate that :name cannot be empty/falsy, but this
       could not be proved.
         After setting :name to ‹""›, the matcher expected the Album to be
         invalid, but it was valid instead.
     # ./spec/models/album_spec.rb:5:in `block (2 levels) in <top (required)>'

Adding a Validation


Adding a validation with ActiveRecord is simple:

app/models/album.rb
class Album < ApplicationRecord
  ...
  validates :name, presence: true
end

Now our test will pass.

Here's another example of a validation. This time, we'll create a validation to ensure that the length of an album isn't more than 100 characters. We'll start with a spec:

spec/models/album_spec.rb
describe(Album) do
  ...
  it { should validate_length_of(:name).is_at_most(100) }
end

Note that the syntax is a little different because we chain a second method to our validation. Fortunately, the shoulda-matchers documentation gives clear examples on how to use each matcher.

And the method to make this pass:

app/models/album.rb
class Album < ApplicationRecord
  ...
  validates_length_of :name, maximum: 100
end

There are many different Active Record validations available. We might use a length validation to make sure a password is sufficiently strong. We might use a numericality validation to ensure that a number is passed into the database. To learn more about validations, including how they work and how to create custom validations, check out the Rails documentation on validations.

Returning an Error Message to the User


If a user tries to add a new album without a name, the form for creating a new album will be rendered again. Currently, there's no message that lets the user know the album wasn't saved to the database. In addition, the user won't know why the album wasn't saved. This can be a very frustrating experience for a user, especially with more complex forms.

Let's add an error message to fix this problem. We'll add the code to the top of the view where the form for creating an album resides:

views/albums/new.html.erb
<% if @album.errors.any? %>
  <h3>Please fix these errors:</h3>
  <ul>
    <% @album.errors.full_messages.each do |message| %>
      <li><%= message %></li>
    <% end %>
  </ul>
<% end %>

...

So how exactly does this code work? Doesn't an instance of @album only have a name and genre property? Let's add a <% binding.pry %> to the top of this code and then navigate to the form for adding a new album. Now let's check pry in the terminal:

=> 1: <% binding.pry %>
   2:
   3: <% if @album.errors.any? %>
   4:   <h3>Please fix these errors:</h3>
   5:   <ul>
   6:     <% @album.errors.full_messages.each do |message| %>

[1] pry(#<#<Class:0x007fb9d5673040>>)> @album.errors
=> #<ActiveModel::Errors:0x007fb9d7550878
@base=#<Album:0x007fb9d76a2d70 id: nil, name: nil, year: nil, created_at: nil, updated_at: nil, genre: nil>,
@details={},
@messages={}>
[2] pry(#<#<Class:0x007fb9d5673040>>)> @album.errors.full_messages
=> []

If we check the value of @album.errors, we'll see that ActiveModel has nicely wrapped this up in an Errors object which includes various properties, including @base, which is our instantiated Album object. Currently, @details and @messages are empty hashes. There is also a @full_messages option as well. These are all instance variables so they can be passed into the view.

Let's exit pry and try to add an album without a name. After we submit the form, our create method will be activated:

controllers/albums_controller.rb
def create
  @album = Album.new(album_params)
  if @album.save
    redirect_to albums_path
  else
    render :new
  end
end

The name validation in our model will prevent the album from being saved, which will trigger the else in our conditional. Now let's check out pry again:

> @album.errors
=> #<ActiveModel::Errors:0x007fb9d25c05b0
 @base=#<Album:0x007fb9d25c3cb0 id: nil, name: "", year: nil, created_at: nil, updated_at: nil, genre: "rock">,
 @details={:name=>[{:error=>:blank}]},
 @messages={:name=>["can't be blank"]}>
> @album.errors.full_messages
=> ["Name can't be blank"]

The @album instance variable was updated in the create action, which we can see in the @base property of the Errors object. The @details and @messages fields are also populated with key-value pairs. Note that there can be multiple key-value pairs if users have more than one error in their form submission. We use @album.errors.full_messages in our view because it returns an array of strings. Each string clearly states an error with the form submission.

Let's exit pry and take a look at the form now. The following message is now at the top of the page:

Please fix these errors:
Name can't be blank

Once again, Rails has made our coding lives easier by providing errors which we can display for the user.

These error messages should be included for all new and edit views. Our validations will be triggered whenever an Album object is created or updated. We could specify that the validation should only happen on one type of action (only new or edit), but we never want an album without a name in the database.

Validations, testing for validations and error messages will all be expected for this section's independent project.

Lesson 17 of 34
Last updated July 14, 2022