Models

The model tier is declarative: a model class names its table and fields, the app names its databases, and every model on one database shares a single connection per worker.

A model class

package MyApp::Model::Book;
use Punk::Model;

table 'books';
field id      => { type => 'integer', primary => 1 };
field title   => { type => 'string', required => 1, minLength => 1 };
field author  => { type => 'string' };
field created => { type => 'string' };

1;

Field definitions double as validation - create and update check their input against the declared types and constraints.

Registration

database dsn => 'dbi:SQLite:dbname=myapp.db';
model    'Book';

model 'Book' resolves against MyApp::Model:: at boot. Several databases can be configured by name; a model picks its own with a database declaration of its own, defaulting to the unnamed one:

database dsn => 'dbi:SQLite:dbname=myapp.db';          # the default
database analytics => { dsn => 'dbi:Pg:dbname=warehouse' };

The contract

Every backend speaks the same six methods, with the same SQL and the same result shapes:

my $book  = $c->model('Book')->get(id => 42);
my $page  = $c->model('Book')->search({ author => 'Gibson' },
                                      { limit => 20, offset => 40 });
my $all   = $c->model('Book')->all;
my $made  = $c->model('Book')->create({ title => 'Neuromancer' });
my $fresh = $c->model('Book')->update(42, { author => 'W. Gibson' });
$c->model('Book')->delete(42);

Punk::Model::DBI, the default backend, memoises quote_identifier and the fixed-shape get/delete SQL on the pooled connection - a get is 2.06 microseconds against 3.61 rebuilt every call.

The async backend

Punk::Model::DBIx::Loop is the same contract, the same SQL and the same result shapes, but every method returns a Punk::Future and the statement runs on DBIx::Loop over the worker's own event loop:

database backend => 'DBIx::Loop', dsn => 'dbi:Pg:dbname=myapp';

get '/books/:id' => sub {
    my ($c) = @_;
    return $c->model('Book')->get(id => $c->param('id'))->then(sub {
        my ($book) = @_;
        $c->render('book/view', { book => $book });
    });
};

On a Hyperman worker the query does not block the loop - the worker serves other requests while the database round-trips. Swapping a model between backends is a configuration change, not a rewrite.