Applications with Mojolicious – Part Five: DBIC Integration in Mojolicious

In this post we integrate the DBIx::Class schema we created last time into our Mojolicious blog app.

Mojolicious Series

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 &mdash; 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

Code at GitHub

You can browse the app at the current state as well as its history on Github.