If you need to handle a unique constraint in a database table field when writing an add or edit process in a Play Framework application, I hope this example will be helpful. In the end I’ll show:
- How to write a Play Anorm query that performs a SQL INSERT on a database field that has a unique constraint
- How to write a Play controller method (Action) that handles that SQL exception
- How to create a new
Form
after you catch that exception - How to add custom error messages to that form
- How to redirect control back to your Play template
If you need to see any of those things, I hope the following example is helpful.
The database method
In my case I’m writing a URL-shortening application and I have a unique constraint on a table column named short_uri
, so I wrote my database-insert method with Anorm to look like this:
// uses Try b/c `short_uri` has a unique constraint, which can cause an exception
def insertNewUrl(u: Url): Try[Option[Long]] = db.withConnection { implicit c =>
Try {
val q = SQL"""
insert into urls (user_id,long_url,short_uri,notes)
values (1,${u.longUrl},${u.shortUrl},${u.notes})
"""
q.executeInsert() //returns the primary key as an Option
}
}
I catch the possible unique constraint exception with Try
. Because executeInsert
returns the primary key inside an Option
, this method returns the type Try[Option[Long]]
. The key here for the purpose of this example is that the SQL INSERT can fail because of the unique constraint, so I handle the possible exception with Try
.
The controller method/action
For me, the harder part in this case is to know what to do in your controller action code. I’m not going to try to explain it all, I’m just going to show my code, and then offer a little explanation:
def handleAddFormSubmission = authenticatedUserAction { implicit request =>
val formDidNotValidateFunction = { formWithErrors: Form[Url] =>
BadRequest(views.html.editUrl(0, formWithErrors, addFormSubmitUrl))
}
val formValidatedFunction = { data: Url =>
// form data is valid, try saving to the database
val url = Url(
data.id,
data.longUrl,
data.shortUrl,
data.notes
)
// there’s a Try here b/c a SQL INSERT can fail due to database constraints
val pkInOptionInTry: Try[Option[Long]] = urlDao.insertNewUrl(url)
pkInOptionInTry match {
case Success(maybePk) => {
// no exception was throw, so `maybePk` probably has the primary key
Redirect(routes.UrlAdminController.list())
.flashing("info" -> s"URL '${data.shortUrl}' was added.")
}
case Failure(e) => {
// an exception was thrown, so the SQL INSERT failed, probably a duplicate key error
val formBuiltFromRequest: Form[Url] = form.bindFromRequest()
val newForm = formBuiltFromRequest.copy(
errors = formBuiltFromRequest.errors ++ Seq(
FormError("short_uri", "Probably have a duplicate URI in the database."),
FormError("short_uri", e.getMessage)
)
)
BadRequest(views.html.editUrl(0, newForm, addFormSubmitUrl))
}
}
}
val formValidationResult: Form[Url] = form.bindFromRequest
formValidationResult.fold(
formDidNotValidateFunction,
formValidatedFunction
)
}
One big key in this method is knowing that you can use Try
to easily handle the exception that can be returned by the database access method.
Creating a new Play Form after the error
A second big key is knowing that you can rebuild a Form
with either of these two approaches:
val newForm: Form[Url] = form.bindFromRequest()
val newForm: Form[Url] = form.fill(data)
I haven’t looked into the differences between those two possible approaches, but it took me a while to figure out those approaches, so hopefully that knowledge will save someone else time in the future. The key here is that you need to build a new form after the exception that you can pass to your template, and this is how you do that.
Adding custom error messages to the form
Another big key was find out that methods like form.withError(...)
and form.withGlobalError(...)
don’t work as expected, so I had to write code like this to get my custom error messages to show up in the Play template:
val newForm = formBuiltFromRequest.copy(
errors = formBuiltFromRequest.errors ++ Seq(
FormError("short_uri", "Probably have a duplicate URI in the database."),
FormError("short_uri", e.getMessage)
)
)
You probably only need one error message there, but since I’m writing this application for myself I don’t mind seeing the output from e.getMessage
. Note that this approach binds those error messages to the short_uri
field in the template.
The Play template
You might not need to see the Play Framework template to understand what I just showed, but it may help to see the template code, so here it is:
@(
urlId: Long,
form: Form[Url],
postUrl: Call
)(implicit request: RequestHeader, messagesProvider: MessagesProvider)
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/main.css")">
<link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/admin.css")">
</head>
<body id="edit-url">
<div id="content">
<div id="edit-url-form">
<h1>URL Shortener</h1>
@* Flash shows updates to a page *@
@request.flash.data.map{ case (name, value) =>
<div>@name: @value</div>
}
@* Global errors are not tied to any particular form field *@
@if(form.hasGlobalErrors) {
@form.globalErrors.map { error: FormError =>
<div>
Error: @error.key: @error.message
</div>
}
}
@helper.form(postUrl, 'id -> "blog-edit-form") {
@helper.CSRF.formField
@* id is 0 for 'add', and is set by the 'edit' process *@
<input type="hidden" name="id" value='@urlId'>
@helper.inputText(
form("long_url"),
'_label -> "Long URL",
'placeholder -> "the original, long url",
'id -> "long_url",
'size -> 68
)
@helper.inputText(
form("short_uri"),
'_label -> "Short URI",
'placeholder -> "the short uri you want",
'id -> "short_uri",
'size -> 68
)
@helper.textarea(
form("notes"),
'_label -> "Notes",
'id -> "notes",
'rows -> 5,
'cols -> 60
)
<button>Save</button>
}
</div>
</div>
</body>
</html>
I’m not going to bother to explain that code at all, other than to say that the short_uri
field in the template is where my custom error message will show up.
As a final note, here’s what those custom form error messages look like in the template (a data-entry form):
Summary
In summary, if you wanted to see how to handle Play Framework things like how to handle a SQL exception related to a unique constraint violation; how to create a new form after an exception like that; and how to add custom error messages to a Play form, I hope this example is helpful.