Using DBIx::Class within a Dancer application

DBIx::Class, also known as DBIC, is one of the many Perl ORM (Object Relational Mapper), but it's commonly recognised as the best and most widely used.

This is a nice presentation from Leo : http://www.slideshare.net/ranguard/dbixclass-beginners-presentation

Basically, DBIC allows you to interact with your SQL Database without writing any SQL.

To do that, you need a set of Schema classes that describes your database structure. Then you can use DBIC to create, update, delete, search, and do many more things on the data that are in your database.

From a Dancer web application, it is very easy to use DBIC, thanks to Dancer::Plugin::DBIC. This article will implement a simple web application to demonstrate the use of Dancer::Plugin::DBIC.

Note : Although this article only skims the surface of DBIX::Class, it won't explain how to use it. We recommend you to have a look at DBIx::Class::Manual::Intro or DBIx::Class::Manual::Example if needed.

The bookstore example

Let's consider a simple Dancer application that allows to search for authors or books. The application is connected to a database, that contains authors, and their books. The website will have one single page with a form, that allows to query books or authors, and display the results.

To keep this article short, the HTML will be simplistic, and the implementation as well. However, we'll try to explain how to properly use Dancer::Plugin::DBIC.

The application will be structured as follow:

  • a Dancer route /search to handle the request, and decide if there is any search to perform, and send the results to the view

  • a view, that will display the search form, and the results if any.

  • a set of models, linked to a database, that will contain the books and authors. These models will be created using DBIC

The basics

Create the application

Okay, that's easy enough:

$> dancer -a bookstore

Change template type

We'll want to loop on results and display authors and books, and it's easier to use Template Toolkit to do that, rather than the default Dancer::Template::Simple.

So let's specify in the configuration that we'll use Template Toolkit as template engine:

# add in bookstore/config.yml
template: template_toolkit

Create a view

We need a view to display the search form, and below, the results, if any. The results will be fed by the route to the view as an arrayref of results. Each result is a hashref, with a author key containing the name of the author, and a books key containing an arrayref of strings : the books names.

That explanation is probably hard to follow, so here is an example, much easier:

# example of a list of results
[ { author => 'author 1',
    books => [ 'book 1', 'book 2' ],
  },
  { author => 'author 2',
    books => [ 'book 3', 'book 4' ],
  }
]

So, what will the view look like? Here is a simple example, displaying the search form, and the results, if any. It's written in Template Toolkit, but Dancer changes the default [‰ %] format to be <% %> instead.

# bookstore/views/search.tt
<p>
<form action="/search">
Search query: <input type="text" name="query" />
</form>
</p>
<br>  
  
<% IF query.length %>
  <p>Search query was : <% query %>.</p>
  <% IF results.size %>
    Results:
    <ul>
    <% FOREACH result IN results %>
      <li>Author: <% result.author.replace("((?i)$query)", '<b>$1</b>') %>
      <ul>
      <% FOREACH book IN result.books %>
        <li><% book.replace("((?i)$query)", '<b>$1</b>') %>
      <% END %>
      </ul>
    <% END %>
  <% ELSE %>
    No result
  <% END %>
<% END %>

Create a route

Let's create a simple Dancer route, to be added in the bookstore.pm module:

# add in bookstore/lib/bookstore.pm
get '/search' => sub {
    my $query = params->{query};
    my @results = ();
    if (length $query) {
        @results = _perform_search($query);
    }
    template 'search', { query => $query,
                         results => \@results,
                       };
};

It's rather simple: get the parameter called query, if it exists perform the search, and in any case, call the search view.

So, as you can see, we need to write the _perform_search() method. But before we do that, let's create the database.

Create a database

We'll go with SQLite, as it fits well with the aim of simplicity of this example. Let's create the SQLite file database:

$> sqlite3 bookstore.db
CREATE TABLE author( 
  id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 
  firstname text default '' not null, 
  lastname text not null);

CREATE TABLE book(
  id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 
  author INTEGER REFERENCES author (id), 
  title text default '' not null );

Simple stuff: we have 2 tables, one for authors, and one for books, that points to the author table.

Populate with some data

Let's write a script to populate the database with some data. We'll use DBIX::Class, and let it discover our simple database schema.

# populate_database.pl
package My::Bookstore::Schema;
use base qw(DBIx::Class::Schema::Loader);
package main;
my $schema = My::Bookstore::Schema->connect('dbi:SQLite:dbname=bookstore.db');
$schema->populate('Author', [
  [ 'firstname', 'lastname'],
  [ 'Ian M.',    'Banks'   ],
  [ 'Richard',   'Matheson'],
  [ 'Frank',     'Herbert' ],
]);
my @books_list = (
  [ 'Consider Phlebas',    'Banks'    ],
  [ 'The Player of Games', 'Banks'    ],
  [ 'Use of Weapons',      'Banks'    ],
  [ 'Dune',                'Herbert'  ],
  [ 'Dune Messiah',        'Herbert'  ],
  [ 'Children of Dune',    'Herbert'  ],
  [ 'The Night Stalker',   'Matheson' ],
  [ 'The Night Strangler', 'Matheson' ],
);
# transform author names into ids
$_->[1] = $schema->resultset('Author')->find({ lastname => $_->[1] })->id
  foreach (@books_list);
$schema->populate('Book', [
  [ 'title', 'author' ],
  @books_list,
]);

Then run it in the directory where bookstore.db sits:

perl populate_database.db

And that's our database populated !

Use Dancer::Plugin::DBIC

Let's go back to our Dancer application now. Instead of interacting with the database using SQL, let's configure DBIX::Class. DBIC needs to understand how your data is organised in your database. There are two ways of letting DBIC know:

  • either by writing a set of Perl modules, called schema modules: they will describe the database schema, each module describing one entity,

  • or by letting DBIC connect to the database, explore it, and generate the schema itself.

We'll demonstrate the use of the two solutions. The author of this article (dams) is not a big fan of the detection method: on complex database, it doesn't get everything right, so one needs to help DBIC. Describing the schema manually in proper Perl classes seems a cleaner option. But hey, TIMTOWTDI.

Use auto-detection

Let's add some configuration in our Dancer application. We want to indicate that we want to use the Dancer::Plugin::DBIC plugin, and how we want to use it. We also want to define a new DBIC schema, that we will call bookstore. And we need to indicate that this schema is connected to the SQLite database we created.

# add in bookstore/config.yml
plugins:
  DBIC:
    bookstore:
      dsn:  "dbi:SQLite:dbname=bookstore.db"

We could potentially define more schemas, by adding more fields under the DBIC: entry.

Note : you've noticed that we have only described which database to link the schema to. That way, we let Dancer::Plugin::DBIC connect to the database and discover its schema, and make it available for us

Now that the configuration is done, let's see what needs to be done in the code.

First of all, we need to indicate to Dancer that we want to use Dancer::Plugin::DBIC. That's easily done:

# add in bookstore/lib/bookstore.pm
use Dancer::Plugin::DBIC;

And now we can implement _perform_search using Dancer::Plugin::DBIC. The plugin gives you access to an additional keyword called schema, which you give the name of schema you want to retrieve. It returns a DBIx::Class::Schema::Loader (because we let the plugin discover the schema for us). This returned object can then be used to get a resultset and perform searches, as per standard usage of DBIX::Class.

# add in bookstore/lib/bookstore.pm
sub _perform_search {
    my ($query) = @_;
    my $bookstore_schema = schema 'bookstore';
    my @results;
    # search in authors
    my @authors = $bookstore_schema->resultset('Author')->search({
      -or => [
        firstname => { like => "%$query%" },
        lastname  => { like => "%$query%" },
      ]
    });
    push @results, map {
        { author => join(' ', $_->firstname, $_->lastname),
          books => [],
        }
    } @authors;
    my %book_results;
    # search in books
    my @books = $bookstore_schema->resultset('Book')->search({
        title => { like => "%$query%" },
    });
    foreach my $book (@books) {
        my $author_name = join(' ', $book->author->firstname, $book->author->lastname);
        push @{$book_results{$author_name}}, $book->title;
    }
    push @results, map {
        { author => $_,
          books => $book_results{$_},
        }
    } keys %book_results;
    return @results;
}

We needed to do some data fiddling so that the books results are gathered by authors.

Use home-made schema classes

Writing your own DBIC schema classes goes a bit beyond this article, but here are the basics. You can either have the .pm files be generated from you using dbicdump (see DBIx::Class::Schema::Loader), and then you can modify them to fit your needs. Or you can write them yourself from scratch, as explained in the DBIC documentation.

A third option is to use the nice DBIx::Class::MooseColumns that let's you write the DBIC schema classes using Moose. This way of doing make it look more like ActiveRecord declarations.

You should put your schema classes in a place that Dancer will find. A good place is in bookstore/lib/.

Once your schema classes are in place, all you need to do is modify config.yml to specify that you want to use them, instead of the default auto-detection method:

# change in bookstore/config.yml
plugins:
  DBIC:
    bookstore:
      schema_class: My::Bookstore::Schema
      dsn: "dbi:SQLite:dbname=bookstore.db"

The rest will work exactly the same.

Start the application

Our bookstore lookup application can now be started using the built-in server:

# start the web application
bookstore/bin/app.pl

Now if we search for The, here is what we get :

As you can see, the page presents 4 results. The first one is an author's match, Richard Matheson. The next 2 ones are 2 of his books. The last one is The Player of Games (a great book by the way...).

Conclusion

Well that was a rather long article, but we wanted to show a real example of using DBIC in Dancer. Most of the Dancer Plugins have the same spirit in common: be as simple as possible, and don't get in the way of the user. Dancer::Plugin::DBIC is exactly that, and we hope we demonstrated it to you.

AUTHOR

dams ( Damien Krotkine <dams@cpan.org> )