In this post we integrate the DBIx::Class schema we created last time into our Mojolicious blog app.
Mojolicious Series
- Part 01 - Introduction
- Part 02 - Routing and Rendering
- Part 03 - Forms, Logins
- Part 04 - Database schemas with DBIx::Class
- Part 05 - DBIx::Class Integration in Mojolicious
- Part 06 - Schema Versioning with DBIx::Class::Migration
- Part 07 - Relationships with DBIx::Class
Accessing DBIC in Mojolicious
To integrate the schema in our app, we could use a helper method that
makes the schema method available throughout the app. Add the following
code to your startup
method in the main module:
1
2
3
4
5
6
7
8
9
10
11
use Moblo::Schema;
sub startup {
my $self = shift;
# ...
my $schema = Moblo::Schema->connect('dbi:SQLite:moblo.db');
$self->helper(db => sub { return $schema; });
}
Note: This approach shares one connection for all requests of a worker. There are several alternate ways to share schemas, e.g., through caching (CHI) or database pools.
With the helper, call $self->schema->resultset('Post')
to access the
post resultset from controllers, or simply
<% schema->resultset('Post') %>
from within templates.
Adding new posts
Let's first add a route in the restricted area to publish new posts on
the blog. Start with the routes to a new Post
controller to the main
module.
1
2
$authorized->get('/create')->name('create_post')->to(template => 'admin/create_post');
$authorized->post('/create')->name('publish_post')->to('Post#create');
Create the corresponding template within
templates/admin/create_post.html.ep
and create a basic form for input
of title and content like so:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<h2>Restricted area</h2>
<h3>Create new post</h3>
%= form_for 'publish_post' => (method => 'POST') => begin
<div>
%= label_for title => 'Title'
<br/>
%= text_field 'title'
</div>
<div>
%= label_for content => 'Post Content'
<br/>
%= text_area content, cols => 40, rows => 20
</div>
%= submit_button 'Publish', class => 'btn'
% end
In the controller, we accept the form and insert it into the database. Afterwards, we forward the user back to the restricted area main page.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package Moblo::Post;
use Mojo::Base 'Mojolicious::Controller';
use DateTime;
sub create {
my $self = shift;
# Grab the request parameters
my $title = $self->param('title');
my $content = $self->param('content');
# Persist the post
$self->db->resultset('Post')->create({
title => $title,
content => $content,
# Use the username as author name for now
author => $self->session('username'),
# Published now
date_published => DateTime->now->iso8601,
});
$self->flash(post_saved => 1);
$self->redirect_to('restricted_area');
}
1;
Note the $self->flash(post_saved => 1)
call. It provides a way to
send messages to the next request through the session cookie. We can
thus show a confirmation on the admin page when returned from saving a
post.
1
2
3
4
5
6
7
<h2>Restricted area</h2>
% if (flash->{post_saved}) {
<p>Your post was saved!</p>
% }
%= link_to "Create new post" => 'create_post'
Now that we can actually create posts, we can edit our main page to show all posts:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<h2>Moblo — Blog Index</h2>
<p>Welcome to Moblo!</p>
<h3>Post Index</h3>
% my $posts = db->resultset('Post');
% if ($posts->count == 0) {
<p>None :/<p>
% }
% while (my $post = $posts->next) {
<div class="post">
<h4>
<%= $post->title %>
<br/>
<small>(published: <%= $post->date_published %>)</small>
</h4>
<p><%= $post->content %></p>
</div>
% }
<hr>
<p>
%= link_to Login => 'login_form'
</p>
Deleting Posts
To delete a post, we create a route that takes the post id as a placeholder.
1
2
3
4
5
# on GET, we display a template asking to confirm the deletion.
$authorized->get('/delete/:id', [id => qr/\d+/])->name('delete_post')->to(template => 'admin/delete_post_confirm');
# on POST, we delete the post.
$authorized->post('/delete/:id', [id => qr/\d+/])->name('delete_post_confirmed')->to('Post#delete');
The placeholder is denoted by :id
and is restricted using the regex
pattern qr/\d+/ to accept numeric values only. Note: Do not use
start/end markers ^$
or capturing groups, as the regex is actually
combined in a larger route regex pattern.
In the templates and controllers, placeholder are available through the stash. Thus the template might look like this:
1
2
3
4
5
6
<h2>Confirm Deletion</h2>
<p>Do you really want to delete the post <%= stash('id') %> ? </p>
%= form_for 'delete_post_confirmed' => { id => stash('id') } => (method => 'POST') => begin
%= submit_button 'Delete', class => 'btn'
% end
The form_for
looks a bit complicated, but the first part is simply the
named route with a parameter (that is used to fill the placeholder):
delete_post_confirmed' => { id => stash('id') }
and the rest is no
different from the login form in the second post of this series.
Lastly, add the delete action to the Moblo::Post
controller:
1
2
3
4
5
6
7
8
9
10
sub delete {
my $self = shift;
my $posts = $self->db->resultset('Post');
$self->app->log->info($self->stash('id'));
$posts->search({ id => $self->stash('id') })->delete;
$self->flash(post_deleted => '1');
$self->redirect_to('restricted_area');
}
Note that we don't have to check the id to be numeric (the placeholder does that already). However, you may want to check that the id actually exists. If it does not, the search will return nothing, and thus the call to delete will be meaningless.
In the next post, we will introduce versioning to the database schema
Additional Documentation
- For more examples on placeholders, check the routing guide.
Code at GitHub
You can browse the app at the current state as well as its history on Github.