NAME
DBIx::Class::Migration::Tutorial::Catalyst - Using a web framework
GOAL
By the end of this section, you will learn some strategies for using migrations with web development and for testing.
This is not a Catalyst tutorial. You should be familar with the Catalyst web development framework, and have read the Catalyst::Manual. Although we will build a minimal Catalyst application we are focused on database integration as well as exploring some strategies for testing and would not consider this application to represent overall best practices.
Reviewing the documentation for Catalyst::Test would be valuable, as well as Catalyst::Plugin::ConfigLoader for a refresher on the idea of localized and enviroment specific configurations.
Bootstrap a basic Catalyst application
Update you dist.ini
file:
name = DBIx-Class-Migration
author = John Napiorkowski <jjnapiork@cpan.org>
license = Perl_5
copyright_holder = John Napiorkowski
copyright_year = 2012
abstract = Tutorial Application for DBIx-Class-Migration
version = 0.001
[@Basic]
[Prereqs]
Moose = 0
MooseX::MethodAttributes = 0
DBIx::Class = 0
DBIx::Class::Migration = 0
Catalyst = 0
Catalyst::Devel = 0
Catalyst::Plugin::ConfigLoader = 0
Catalyst::Model::DBIC::Schema = 0
Catalyst::Action::RenderView = 0
Catalyst::View::TT = 0
Plack = 0
[Prereqs / TestRequires]
Test::DBIx::Class = 0
Test::Most = 0
You should see we've added two dependencies related to Catalyst. Now install them with cpanm:
dzil listdeps | cpanm
Next, bootstrap a Catalyst application. I know you can use the catalyst.pl commandline tool, but for our simple application let's just create a few files manually:
touch app.psgi
touch lib/MusicBase/Web.pm
mkdir lib/MusicBase/Web
mkdir lib/MusicBase/Web/Controller
mkdir lib/MusicBase/Web/Model
mkdir lib/MusicBase/Web/View
touch lib/MusicBase/Web/Controller/Root.pm
touch lib/MusicBase/Web/Model/Schema.pm
touch lib/MusicBase/Web/View/HTML.pm
mkdir share/etc
touch share/etc/musicbase_web.pl
mkdir share/html
touch share/html/index.tt
If you prefer to use a helper, you can also try https://metacpan.org/pod/Dist::Zilla::Plugin::Catalyst::New it generates part of the files catalyst.pl creates but is closer to the normal Dist::Zilla stuff.
Now open lib/MusicBase/Web.pm
and change it to look like this:
package MusicBase::Web;
use Moose;
use Catalyst qw/
ConfigLoader
/;
extends 'Catalyst';
our $VERSION = '0.01';
__PACKAGE__->config(
'Plugin::ConfigLoader' => {
file => __PACKAGE__->path_to('share', 'etc'),
},
);
__PACKAGE__->setup;
__PACKAGE__->meta->make_immutable;
This is a pretty plain and straight Catalyst application class. The only thing I've done differently from default is I've placed our configuration files in share/etc
, rather than in the application root (the directory that contains your dist.ini
file). I think this is a bit more forward looking, and since we already have the share
directory, why not use it?
For your Model, we'll use Catalyst::Model::DBIC::Schema
to provide a bit of thin glue between you Catalyst web application and your MusicBase::Schema:
lib/MusicBase/Web/Model/Schema.pm
package MusicBase::Web::Model::Schema;
use Moose;
extends 'Catalyst::Model::DBIC::Schema';
__PACKAGE__->meta->make_immutable;
As is typical for Catalyst models, there's not a lot going on here, just the minimum useful glue to make Catalyst aware of your Schema.
Let's add the View now. Open lib/MusicBase/Web/View/HTML.pm
and enter:
package MusicBase::Web::View::HTML;
use base 'Catalyst::View::TT';
1;
We'll need a bit of configuration to finish the job of hooking the two together so open share/etc/musicbase_web.pl
in your text editor and make it look like this:
{
name => 'MusicBase::Web',
default_view => 'HTML',
disable_component_resolution_regex_fallback => 1,
'Controller::Root' => {
namespace => '',
},
'Model::Schema' => {
traits => ['FromMigration'],
schema_class => 'MusicBase::Schema',
install_if_needed => {
default_fixture_sets => ['all_tables']},
},
'View::HTML' => {
INCLUDE_PATH => [ '__path_to(share,html)__' ],
TEMPLATE_EXTENSION => '.tt',
},
};
I know many of the Catalyst
examples use Config::General
or YAML for configuration information. I tend to use Perl configuration files because of the extra flexibility.
I'm setting a few things here, but for our discussion the most important one is the Model::Schema
section, where I point our model to the SQLite database we've been using all along. Since we just want Catalyst to use our database sandbox, the easiest way to do this is to use the FromMigration
trait, which you get for free when you install DBIx::Class::Migration. You can review the documentation at Catalyst::TraitFor::Model::DBIC::Schema::FromMigration.
The configuration given would use the SQLite sandbox. What if you wanted to switch to using the MySQL sandbox instead? The Model::Schema
parameters would look like so:
'Model::Schema' => {
traits => ['FromMigration'],
schema_class => 'MusicBase::Schema',
extra_migration_args => {
db_sandbox_class => 'DBIx::Class::Migration::MySQLSandbox'},
install_if_needed => {
default_fixture_sets => ['all_tables']},
},
In any case, you hopefully noticed that we also run some setup code to install the database and populate some fixtures, if they are missing.
NOTE: If you use the FromMigration
trait, we will automatically start and stop the database if needed (and you are using a database like MySQL or Postgresql that needs starting and stopping). This startup and teardown can impact the startup time of you application.
NOTE: If you already had a database setup, and are not using the database sandbox feature (as you won't when in a production server, or if you are using some shared hosting setups, for example) you should setup your connect_info
as you normally would in a Catalyst configuration.
Let's setup a trivial controller that pulls a few rows out of the database and just outputs this to a web page.
package MusicBase::Web::Controller::Root;
use Moose;
use MooseX::MethodAttributes;
extends 'Catalyst::Controller';
sub index :Path :Args(0) {
my ($self, $ctx) = @_;
my @artists = $ctx->model('Schema::Artist')
->search({},{ result_class =>
'DBIx::Class::ResultClass::HashRefInflator' })
->all;
$ctx->stash(artists => \@artists);
}
sub end : ActionClass('RenderView') {}
__PACKAGE__->meta->make_immutable;
Here we want to just get all the Artists and send them to our View. Since in MVC it is considered correct to inform a View of a Model in a Read Only manner (in other words, your View should not be able to modify the Model) I generally use the DBIx::Class::ResultClass::HashRefInflator result class which will flatten your results to an array of hashrefs, rather than return a list of result objects. Besides making it impossible for your template authors to accidentally modify the model, you get a nice speed bump since inflating an array of hashrefs is much faster than creating all those result objects.
Usually I create a resultset method in my base resultset class, and have all my custom resultsets inherit from that. Something like:
package MusicBase::Schema::ResultSet;
use base 'DBIx::Class::ResultSet;
sub all_as_array {
shift->search({},{ result_class =>
'DBIx::Class::ResultClass::HashRefInflator' })
->all;
}
sub all_as_arrayref { [shift->all_as_array] }
I would have MusicBase::Schema::ResultSet::Artist
inherit from MusicBase::Schema::ResultSet
instead of DBIx::Class::ResultSet as it does now. Then I could have written:
sub index :Path :Args(0) {
my ($self, $ctx) = @_;
$ctx->stash( artists =>
$ctx->model('Schema::Artist')->all_as_arrayref );
}
This is a common enough pattern for me that it is worth the trouble to create the base class. Additionally, if you have lots of DBIx::Class components to load it helps to create a central base class, since that speeds things up at load time.
And here's the template for the webpage: share/html/index.tt
<html>
<head>
<title>Artists</title>
<link rel="stylesheet"
href="http://twitter.github.com/bootstrap/1.4.0/bootstrap.min.css"
type="text/css" rel="Stylesheet">
</head>
<body class="container">
<h1>Artist List</h1>
[% FOR artist IN artists %]
<p>[% artist.name %]</p>
[% END %]
</body>
</html>
We'll use Twitter's Bootstrap CSS to make things look neat. Lastly you need to edit app.psgi
as follows:
use MusicBase::Web;
MusicBase::Web->psgi_app;
So now we can start our Catalyst application!
plackup -Ilib
Integrating DBIx::Class::Migration and Catalyst.
There's two main places to where Catalyst and DBIx::Class::Migration can cooperate: Running migrations and Running Tests.
Running Migrations
Although you can just use dbic-migration directly with you Catalyst application, since Catalyst already does a great job of managing configuration, let's learn how to subclass DBIx::Class::Migration::Script and customize it for your application. That way you don't need to set ENV
or pass option flags to the dbic-migration
commandline tool (and easily make a mistake and upgrade the wrong database :) ).
touch lib/MusicBase/Schema/MigrationScript.pm
And then open lib/MusicBase/Schema/MigrationScript.pm
in your editor and change it to look like this:
package MusicBase::Schema::MigrationScript;
use Moose;
use MusicBase::Web;
extends 'DBIx::Class::Migration::Script';
sub defaults {
schema => MusicBase::Web->model('Schema')->schema,
}
__PACKAGE__->meta->make_immutable;
__PACKAGE__->run_if_script;
Basically you've made a subclass of DBIx::Class::Migration::Script but you are setting the schema
to always be whatever Catalyst thinks it is. Now you can use Catalysts built in configuration management to decide what database you are running migrations on. For example you can run this straight out (remember to remove the ENV var DBIC_MIGRATION_SCHEMA_CLASS, if you have it set now for running the tutorial)
$ perl -Ilib lib/MusicBase/Schema/MigrationScript.pm status
Schema is 3
Deployed database is 3
And you can reset the data from fixtures, dump new ones, etc. Plus, if you created an enviroment specific configuration (such as if you have a file share/etc/musicbase_web_qa.pl
that points to your QA datase) you can leverage you Catalyst based configuration to make your life a bit easier. For example:
CATALYST_CONFIG_LOCAL_SUFFIX=qa perl -Ilib \
lib/MusicBase/Schema/MigrationScript.pm status
Would grab the connected schema for your qa enviroment specific configuration and give you the status on that (assuming you can ping it from your logged in terminal). This integration is very useful since you can use whatever your Catalyst application thinks is the current database as the target of the migration. You can use other bits of configuration info as well, such as a custom target_dir
etc.
Alternative: If subclasing DBIx::Class::Migration::Script seems like an overly heavy handed solution, or running the *.pm file like a script just weirds you out, you can simply create a script like the following, which would work identically:
For example, something in script/my-dbic-migration
#!/usr/bin/env perl
use MusicBase::Web;
use DBIx::Class::Migration::Script;
DBIx::Class::Migration::Script
->run_with_options(
schema => MusicBase::Web->model('Schema')->schema);
And then use it like:
perl -Ilib script/my-dbic-migration status
NOTE: In this case you need to invoke the perl interp with -Ilib
so that we can find MusicBase::Web
If you have a bunch of files in /script
or if you need to be able to run your custom migration tool after installation, this could be a good option.
Running Tests
We've seen how using tools like Test::DBIx::Class together with DBIx::Class::Migration can really simplify your unit level testing effort. Using them you don't need to spend a lot of time setting up dedicated testing databases and managing configuration sets (that need to change over time). However if you want to write tests that check your actual web pages (for example you want to test things like if a page shows the correct results and if web forms work) you need to manage that a bit differently. Here's what I do:
First, create a enviroment specific configuration for testing:
touch share/etc/musicbase_web_test.pl
Then open share/etc/musicbase_web_test.pl
and add the following:
{
'Model::Schema' => {
traits => ['FromMigration'],
schema_class => 'MusicBase::Schema',
extra_migration_args => {
db_sandbox_builder_class => 'DBIx::Class::Migration::TempDirSandboxBuilder',
db_sandbox_class => 'DBIx::Class::Migration::MySQLSandbox'},
install_if_needed => {
default_fixture_sets => ['all_tables']},
},
};
So what is going to happen here is if you start the application pointing to this configuration (with CATALYST_CONFIG_LOCAL_SUFFIX
=test) when the application runs it will automatically create a clean new database and populate it with the all_tables
fixture set. Just for fun, we will create a test instance of Mysql. Please note this will be a temporary sandbox, and will be deleted when your Catalyst application exits. It is not the same as the MySQL sandbox we created in share/musicbase-schema/*
.
This looks similar to the first configuration file we did, where we use the FromMigration
trait to hookup your migration deployments. However, instead of running our tests on the database sandbox in /share
(which you don't want to so since that's not going to be a reliable and consistent database for testing) we use the following bit:
db_sandbox_builder_class => 'DBIx::Class::Migration::TempDirSandboxBuilder',
To instruct the sandbox builder to put the sandbox into a temporary directory instead of /share
. What will happen here is that (similar to the way that we saw with Test::DBIx::Class) we build up a database from scratch, populate it with known fixtures, run tests, and then tear it down at the end. This way you get clean and repeatable tests. The downside is that the buildup / teardown can add time to the tests, athough you should be able to run your test cases in parallel (using prove -j9 ...
, to run up to nine tests at once) to offset this issue.
Let's write a test case:
touch t/web.t
And open t/web.t
in your editor:
#!/usr/bin/env perl
use Test::Most;
use Catalyst::Test 'MusicBase::Web';
ok my $content = get('/'),
'got some content';
like $content, qr/Michael Jackson/,
'Found Michael Jackson';
done_testing;
Finally run your test:
CATALYST_CONFIG_LOCAL_SUFFIX=test prove -lvr t/web.t
You know from when we did the original demo data script that "Michael Jackson" was one of the artist, so we'd expect to find him in the <$content> from the Root controller (since that's just a list of all the Artist names). So you'd probably want a bit more testing on this page, but this should give you the idea.
Since the above test builds and breaks down a full MySQL sandbox, it might not run instantly, just FYI.
SUMMARY
That's it for some ideas on using migrations with a web development framework like Catalyst. If you are using Catalyst you can take advantage of its great configuration management tools to make it even easier to manage your migrations. You also now have some strategies for making it easy to test.
NEXT STEPS
Proceed to DBIx::Class::Migration::Tutorial::Conclusion
AUTHOR
See DBIx::Class::Migration for author information
COPYRIGHT & LICENSE
See DBIx::Class::Migration for copyright and license information