Yancy: The Next Model
Tags: perl, mojolicious, webdev
Published: 2021-09-06 18:15:36

[Yancy](http://preaction.me/yancy) is a [content management system](https://en.wikipedia.org/wiki/Content_management_system) and application framework for the [Mojolicious](https://mojolicious.org) web framework. For the last year, I've been using it to develop [Zapp, my workflow automation webapp](https://metacpan.org/pod/Zapp). Over that time, it became harder and harder to organize all the code needed to manipulate Zapp's data. To solve this problem, I wrote [Yancy::Model](http://preaction.me/yancy/perldoc/Yancy/Guides/Model).

Mojolicious is a [Model-View-Controller (MVC) web framework](https://docs.mojolicious.org/Mojolicious/Guides/Growing#Model-View-Controller). The model layer is where the data manipulation happens: Reading and writing records in the database. It is also where business logic happens: Sending e-mail for transactions, or periodic data cleanup. The goal of a model layer is to provide an API on to the application's data so that it can be used not only by the web application, but by other tools as well.

Because [Mojolicious does not provide its own model layer](https://docs.mojolicious.org/Mojolicious/Guides/Growing#Model), most people usually turn to the [DBIx::Class ORM](https://metacpan.org/pod/DBIx::Class). But, this has always felt too complex and heavy for my needs: By the time I _know_ my project needs something like DBIx::Class, it's usually too late to migrate easily. I wanted something lighter and more agile that could grow from a rapidly-developed proof-of-concept app to the final, maintainable production version.

Yancy provides a generic API on to multiple database systems, which it calls [Backends](http://preaction.me/yancy/perldoc/Yancy/Backend). Backends handle basic database operations with a common API. Using this common API, I built a lightweight class system for accessing data:

* [Yancy::Model](http://preaction.me/yancy/perldoc/Yancy/Model) wraps the backend object and manages the classes.
* [Yancy::Model::Schema](http://preaction.me/yancy/perldoc/Yancy/Model/Schema) provides methods to create and search a database table.
* [Yancy::Model::Item](http://preaction.me/yancy/perldoc/Yancy/Model/Item) represents a single row in a table and provides methods to update and delete it.

Using these basic classes provides a more fluent interface to the database than using the backend API directly. But, the power of a model layer is in writing custom code to make managing the data easy and safe. For this, Yancy::Model allows you to add your own classes for schemas (tables) and items (rows).

Writing custom model classes makes organizing your data management code easy. For example, Zapp has a table for workflows (called "plans") with a related table containing tasks ("plan_tasks") and another related table containing workflow input ("plan_inputs"). Whenever a user looks at a plan, they need to also see the plan's tasks and inputs. So, I can create a schema class that fetches this information automatically.

```perl
package Zapp::Schema::Plans;
use Mojo::Base 'Yancy::Model::Schema', -signatures;
sub get( $self, $id, %opt ) {
    # I could use two JOINs here instead, but joining 
    # two 1:* relationships could result in a lot
    # of data to fetch and discard...
    my $plan = $self->SUPER::get( $id, %opt );
    my $inputs_schema = $self->model->schema( "plan_inputs" );
    $plan->{inputs} = $inputs_schema->list({ plan_id => $id }, { order_by => 'rank' })->{items};
    my $tasks_schema = $self->model->schema( "plan_tasks" );
    $plan->{tasks} = $tasks_schema->list({ plan_id => $id }, { order_by => 'task_id' })->{items};
    return $plan;
}
```

Zapp also has a table for recording every time a plan is run, called "runs". This table also records the tasks and inputs the plan had when it was run in "run_tasks" and "run_inputs" respectively (along with some additional fields to record the status of the tasks and the user's actual input). In a way, a run _is_ a plan. As above, when a user looks at a run, they need to also see the run's tasks and inputs. Since Yancy::Model uses plain Perl objects, I can refactor the plans class to also handle runs.

```perl
package Zapp::Schema::Plans;
use Mojo::Base 'Yancy::Model::Schema', -signatures;
has tasks_table  => 'plan_tasks';
has inputs_table => 'plan_inputs';
sub get( $self, $id, %opt ) {
    my $plan = $self->SUPER::get( $id, %opt );
    my $inputs_schema = $self->model->schema( $self->inputs_schema );
    $plan->{inputs} = $inputs_schema->list({ plan_id => $id }, { order_by => 'rank' })->{items};
    my $tasks_schema = $self->model->schema( $self->tasks_schema );
    $plan->{tasks} = $tasks_schema->list({ plan_id => $id }, { order_by => 'task_id' })->{items};
    return $plan;
}

package Zapp::Schema::Runs;
use Mojo::Base 'Zapp::Schema::Plans', -signatures;
has tasks_table  => 'run_tasks';
has inputs_table => 'run_inputs';
```

Now plans and runs both fetch their related data automatically. I can add similar functionality to `create`, `set`, `list`, and `delete` to make dealing with the related data seamless. Further, I can create completely custom methods like `enqueue` which will add the plan (or the run) to the [Minion job queue](https://docs.mojolicious.org/Minion) to be executed.

With [Yancy::Model](http://preaction.me/yancy/perldoc/Yancy/Guides/Model/) I can quickly build an API for my application's data. As my application develops, I can keep my data logic separate from my web frontend logic. Since they're separate, I can use my data logic in other applications and tools. This is the biggest benefit of using an MVC pattern with a framework like [Mojolicious](http://mojolicious.org).