DBIx::C...atalyst(3)
π NAME π
okay so this page is DBIx::Class::Migration::Tutorial::Catalyst β full name, no cuts β and it lives in the User Contributed Perl Documentation section (section 3, library calls, iykyk). Your shell header is gonna abbreviate it to something like DBIx::C...atalyst(3) because terminals are haters, but that's the same page, bestie, don't panic. What's it about? Using a web framework. π
π― GOAL π―
By the end of this section you'll have learned some strategies for using migrations while you're doing web development, plus some strategies for testing. That's the vibe.
Real talk though: this is not a Catalyst tutorial. You should already be familiar with the Catalyst web development framework and you should have read the Catalyst::Manual. We are gonna build a minimal Catalyst application, but our focus is database integration and exploring some testing strategies β please do not look at this app and think "ah yes, peak best practices." It is giving learning lab, not production icon. π
Lowkey it would be valuable to review the docs for Catalyst::Test, and also Catalyst::Plugin::ConfigLoader if you want a refresher on the whole localized-configs-and-environment-specific-configs situation. π
π Bootstrap a basic Catalyst application
First things first, update your 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 up: bootstrap a Catalyst application. I know you can use the catalyst.pl commandline tool, but for our simple little app let's just create a few files manually, it's quicker to vibe with:
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'd rather 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 it's 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 straightforward Catalyst application class. The only thing I've done differently from the default is I've put our configuration files in share/etc, rather than in the application root (that's the directory containing your dist.ini file). I think it's a bit more forward looking, and like β we already have the share directory, so why not use it? β¨
For your Model, we'll use Catalyst::Model::DBIC::Schema to provide a bit of thin glue between your 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 a lot of Catalyst examples use Config::General or YAML for configuration info. 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 at 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 above would use the SQLite sandbox. But 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're missing. π§
NOTE: If you use the FromMigration trait, we will automatically start and stop the database if needed (and you're using a database like MySQL or Postgresql that needs starting and stopping). This startup and teardown can impact the startup time of your application. fr fr, it's a real thing.
NOTE: If you already had a database set up, and you're not using the database sandbox feature (as you won't when you're on a production server, or if you're using some shared hosting setups, for example), you should set up your connect_info as you normally would in a Catalyst configuration.
Let's set up a trivial controller that pulls a few rows out of the database and just outputs them 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 just want to grab all the Artists and send them to our View. Since in MVC it's 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 flattens your results into an array of hashrefs, rather than returning 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's worth the trouble to create the base class. Plus, 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
Okay bestie, so there's literally two main places where Catalyst and DBIx::Class::Migration can cooperate: running migrations and running tests. That's the whole vibe check π
π Running Migrations
You can totally just use dbic-migration directly with your Catalyst application, but 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 you're way less likely to easily make a mistake and upgrade the wrong database :) no cap.
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 Catalyst's 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 environment specific configuration (such as if you have a file share/etc/musicbase_web_qa.pl that points to your QA database), you can leverage your 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 environment 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 subclassing 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-Ilibso that we can findMusicBase::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
Bestie, 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 an environment 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 do 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, although 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. Periodt. π
π 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
And that's the whole page, bestie β it came from perl v5.42.0, dated 2020-06-03, and the page itself is DBIx::C...atalyst(3). Slay. π
β¨
π¦ where this page comes from
- opensuse-leap 16.0: perl-DBIx-Class-Migration 0.075-bp160.1.10 Β· Artistic-1.0 OR GPL-1.0-or-later
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash-1017 on : a language model's rewrite of the original, under its terms