Result Class - Components

4 min read
← Back to Blogs
Result Class - Components

DISCLAIMER: Image is generated using ChatGPT.



For my upcoming book on DBIx::Class, I have been working on examples to support the thoeretical chapters. This exercise helped me fill several gaps in my narration. I started with DBIx::Class - Result Class & ResultSet Class followed by a three-part series on Schema ClassPart 1,   Part 2  and Part 3.

In this post, I am going to explore the power of Components within Result class. This will likely be another multi-part series, as components make it easy to get the job done with minimal effort.

Take a look at this Result class, from the post: DBIx::Class - Result Class & ResultSet Class

package MyApp::Schema::Result::User;

use strict;
use warnings;
use base 'DBIx::Class::Core';

__PACKAGE__->table('users');

__PACKAGE__->add_columns(
    id       => { data_type => 'integer', is_auto_increment => 1   },
    username => { data_type => 'varchar', size              => 100 },
    status   => { data_type => 'varchar', size              => 20  },
    deleted  => { data_type => 'integer', default_value     => 0   },
);

__PACKAGE__->set_primary_key('id');

1;

The line: use base 'DBIx::Class::Core'; is critical here.

You would say, we are just making the result class inherit from base class DBIx::Class::Core, right?

Yes, technically, you are spot on but what happens under the hood is even more fun.

The DBIx::Class::Core, brings this to your result class.

__PACKAGE__->load_components(qw/
    PK::Auto
    InflateColumn
    Relationship
    ResultSourceProxy
    Table
    Row
/);

Okay, but what if we want to load additional components?

We can do that easily, for example: __PACKAGE__->load_components(qw/InflateColumn::DateTime/);.

We have used the term components many times already, but what exactly are they?

From a DBIx::Class (DBIC) perspective, Components are modular “mix-ins” used to inject extra functionality into your Result classes.

They allow you to compose your class behaviour dynamically at runtime rather than relying on a rigid, deep inheritance tree.

In short, Inheritance (use base) defines what the class is, while Components (load_components) define what the class can do.

Let’s demonstrate the use of two most common components: InflateColumn::DateTime and TimeStamp

Here is the folder structure:

.
├── lib
│   └── MyApp
│       ├── Schema
│       │   └── Result
│       │       └── Member.pm
│       └── Schema.pm
└── t
    └── date-timestamp.t

Schema Class: MyApp::Schema

package MyApp::Schema;

use strict;
use warnings;

use base 'DBIx::Class::Schema';

__PACKAGE__->load_namespaces;

1;

Result Class: MyApp::Schema::Result::Member

package MyApp::Schema::Result::Member;

use strict;
use warnings;

use base 'DBIx::Class::Core';

__PACKAGE__->load_components(qw/ InflateColumn::DateTime TimeStamp /);

__PACKAGE__->table('members');

__PACKAGE__->add_columns(
    id          => { data_type => 'integer', is_auto_increment => 1 },
    name        => { data_type => 'varchar', size => 100 },
    status      => { data_type => 'varchar', size => 50, default_value => 'Active' },

    created_at  => { data_type => 'datetime', set_on_create => 1 },
    modified_at => { data_type => 'datetime', set_on_create => 1, set_on_update => 1 },
);

__PACKAGE__->set_primary_key('id');

1;

Let’s get to the specifics of component: InflateColumn::DateTime.

If the Result class is having one or more columns with data types: date, datetime or timestamp then the component InflateColumn::DateTime let us treat the column as DateTime object. Then adding component TimeStamp as well, we can set update (set_on_update) or create (set_on_create) date and time automatically.

Let’s create the unit test now: t/date-timestamp.t

First we build the clean schema.

#!/usr/bin/env perl

use strict;
use warnings;
use lib 'lib';

use Test2::V0;
use File::Temp qw( tempfile );
use MyApp::Schema;

my ( $fh, $filename ) = tempfile( SUFFIX => '.db', UNLINK => 1 );
my $schema = MyApp::Schema->connect("dbi:SQLite:dbname=$filename");
$schema->deploy;

Explicit: Row Creation Behaviour

subtest 'Explicit: Row Creation Behaviour' => sub {
    my $row = $schema->resultset('Member')
                     ->create({ name => 'Alice Smith' });

    ok($row->created_at,  'created_at was filled automatically');
    ok($row->modified_at, 'modified_at was filled automatically');

    isa_ok($row->created_at, ['DateTime'], 'created_at is inflated into a DateTime object');
    is($row->created_at->epoch, $row->modified_at->epoch, 'Timestamps match exactly on creation');
};

Explicit: Individual Object Update Behaviour

subtest 'Explicit: Individual Object Update Behaviour' => sub {
    my $row = $schema->resultset('Member')
                     ->create({ name => 'Bob Jones' });

    my $original_created  = $row->created_at;
    my $original_modified = $row->modified_at;

    sleep 1;

    $row->update({ name => 'Bob Jones Jr.' });

    is($row->created_at->epoch, $original_created->epoch,
        'created_at remains completely static on modifications');

    ok($row->modified_at->epoch > $original_modified->epoch,
        'modified_at automatically stepped forward on object ->update()');
};

Implicit Restriction vs Row Behaviour: Update Mechanics

The test below demonstrate the subtle change in bahaviour. The component hooks only fire when we operate on the row object.

subtest 'Implicit Restriction vs Row Behaviour: Update Mechanics' => sub {
    $schema->resultset('Member')
           ->create({ name => 'Member A', status => 'Active' });
    $schema->resultset('Member')
           ->create({ name => 'Member B', status => 'Active' });

    my $row_a = $schema->resultset('Member')->find({ name => 'Member A' });
    my $timestamp_initial = $row_a->modified_at->clone;

    sleep 1;

    # BEHAVIOUR 1: resultset->search()->update() bypasses the hooks
    $schema->resultset('Member')
           ->search({ status => 'Active' })
           ->update({ status => 'Suspended' });

    my $row_a_after = $schema->resultset('Member')
                             ->find({ name => 'Member A' });

    is($row_a_after->status, 'Suspended',
        '[ResultSet Update] Column data successfully changed via ResultSet');

    is($row_a_after->modified_at->epoch, $timestamp_initial->epoch,
        '[ResultSet Update] CRITICAL: modified_at DID NOT change (bypassed hooks)');

    sleep 1;

    # BEHAVIOUR 2: resultset->find()->update() FIRES the hooks
    $schema->resultset('Member')
           ->find({ name => 'Member A' })
           ->update({ status => 'Active' });

    my $row_a_final = $schema->resultset('Member')
                             ->find({ name => 'Member A' });

    is($row_a_final->status, 'Active',
        '[Row Object Update] Column data successfully changed back via find()->update()');

    ok($row_a_final->modified_at->epoch > $timestamp_initial->epoch,
        '[Row Object Update] SUCCESS: modified_at stepped forward automatically!');
};

Final test, run the test suite.

$ yath
PERL_HASH_SEED not set, setting to '20260529' for more reproducible results.

** Defaulting to the 'test' command **

( PASSED )  job  1    t/date-timestamp.t

                                Yath Result Summary
-----------------------------------------------------------------------------------
     File Count: 1
Assertion Count: 13
      Wall Time: 4.07 seconds
       CPU Time: 3.88 seconds (usr: 0.11s | sys: 0.03s | cusr: 3.07s | csys: 0.67s)
      CPU Usage: 95%
    -->  Result: PASSED  <--


Happy Hacking !!!