-
Notifications
You must be signed in to change notification settings - Fork 0
07 Length Validation
Now that it's a little nicer to look at, let's revisit our method for creating posts.
We can add post records now, but we aren't doing anything to make sure that the data users submit makes any sense. They could create a post that's completely blank, which is useless. They could submit a link that isn't even a valid URL. They could also submit text that's longer than our database allows. Let's deal with that last case first.
You may remember that post titles are limited to 255 characters. What exactly happens when we attempt to exceed that limit? Try entering in a really long title—just copy and paste a long bit of text from anywhere—and then submit the form.
ActiveRecord::StatementInvalid in PostsController#create
PG::StringDataRightTruncation: ERROR: value too long for type character varying(255) : INSERT INTO "posts" ("body", "created_at", "link", "title", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id"
Extracted source (around line #13):
11 def create
12 post = Post.new(params.require(:post).permit(:title, :link, :body))
13 post.save
14 redirect_to posts_path
15 end
16
It looks like everything was going swimmingly until the line in PostsController
when we actually attempt to save the record. At that point, we get an error from PostgreSQL. Thankfully, it's a pretty clear error message: value too long for type character varying(255)
. In this case, we expected that to be the problem, but had someone else been using our app when this error popped up in our logs, we'd have needed this context to figure out what went wrong. Thankfully, we discovered the error on our own and can take steps to handle it before anyone else starts using the app.
Now that we know the problem exists, how do we deal with it?
In a sense, we are dealing with this one, because our database constraints prevent the record from being saved if the title is too long. While it's not a bad idea to define those limits at the database level, especially if the database will be shared by multiple apps, testing and maintaining our app is a whole lot easier if that's not the only mechanism we use to enforce it.
We could use JavaScript to prevent users from entering text that exceeds the limit. That's a great way to provide immediate feedback to users without submitting the form with invalid data. Such client-side validation can be bypassed, however, and is best used in conjunction with something on the server side.
We could throw some if
statements in our controller and handle it that way, but that would only address it in this one controller action. That approach becomes unwieldy pretty quickly, and it makes testing and maintenance more difficult.
Generally speaking, this sort of thing is best handled within a model.
Open up the Post
model.
app/models/post.rb
class Post < ActiveRecord::Base
end
At the moment, we don't have any post-specific code in our model. We are relying entirely on functionality inherited from ActiveRecord::Base.
We're going to add a validation to our model. The documentation for Active Record Validations makes the case for why "[m]odel-level validations are the best way to ensure that only valid data is saved into your database."
Model-level validations are database agnostic.
It doesn't matter whether you're storing your data in PostgreSQL, MySQL, Oracle, Microsoft SQL Server, or something else. Validations are enforced by Rails before it ever hits the database.
Model-level validations cannot be bypassed by end users.
There are a number of ways that users can mess around with form data in a browser to bypass client-side validations. They won't be able to avoid model-level validations as long as they're using your app.
Model-level validations are convenient to test and maintain.
You don't have to remember where the heck you validated the data: You did it in the model, of course. And...
Rails provides built-in helpers for common needs.
This is another reason validations are convenient to test and maintain.
Rails allows you to create your own validation methods as well.
The built-in helpers are perfect in many cases. You will find that you sometimes need something different. Thankfully, validations offer that flexibility.
In this case, there's a built-in helper that lets us do what we need.
From the documentation:
length
This helper validates the length of the attributes' values. It provides a variety of options, so you can specify length constraints in different ways:
class Person < ActiveRecord::Base
validates :name, length: { minimum: 2 }
validates :bio, length: { maximum: 500 }
validates :password, length: { in: 6..20 }
validates :registration_number, length: { is: 6 }
end
The possible length constraint options are:
- :minimum - The attribute cannot have less than the specified length.
- :maximum - The attribute cannot have more than the specified length.
- :in (or :within) - The attribute length must be included in a given interval.
- :is - The attribute length must be equal to the given value.
We need to specify a maximum length.
app/models/post.rb
class Post < ActiveRecord::Base
validates :title, length: { maximum: 255 }
end
Now that we have our validation, let's try submitting a really long title again.
What happened? We didn't get any error messages, but the new post isn't there. Let's look at posts_controller#create
line-by-line.
11 def create
12 post = Post.new(params.require(:post).permit(:title, :link, :body))
13 post.save
14 redirect_to posts_path
15 end
On line 12, Post.new
isn't going to concern itself with our validation. Validations are not enforced at that stage.
On line 13, however, post.save
shouldn't work. And yet, the redirect on line 14 is still happening. Everything is happening except that the new post isn't really saved.
If we try this out on a console, we'll find that post.save
doesn't throw an exception if a validation doesn't pass. It simply returns false. So we need to change our controller so that it only redirects to posts_path
if it actually saves.
11 def create
12 post = Post.new(params.require(:post).permit(:title, :link, :body))
13 if post.save
14 redirect_to posts_path
15 end
16 end
So what happens if post.save
returns false
? It will once again find itself without anything to render, and we'll get a big fat error. What should it display under these conditions? Let's send users back to the form.
11 def create
12 post = Post.new(params.require(:post).permit(:title, :link, :body))
13 if post.save
14 redirect_to posts_path
15 else
16 redirect_to new_post_path
17 end
18 end
Let's resubmit a too-long title again.
We no longer get sent to the index. All that appears to have happened is that the form got cleared out. In fact, it did hit posts#create
, but it redirected us to the page we were already on, without any fields filled in. The least it could do is tell us there's a problem!
Nonetheless, we've made progress. Let's commit our changes.
Don't forget to check your git status, and maybe even git diff, to make sure you're committing what you expect.
$ git add .
$ git commit -m "Validate that post#title is at most 255 characters long."