NAME

DBIO::Moo - Enable Moo attributes in DBIO result classes

VERSION

version 0.900000

SYNOPSIS

package MyApp::Schema::Result::Artist;
use DBIO::Moo;
use DBIO::Cake;

table 'artists';

col id   => serial;
col name => varchar(100);

primary_key 'id';

# Moo attribute — lazy, computed from column data on first access
has display_name => (is => 'lazy');
sub _build_display_name { 'Artist: ' . $_[0]->name }

# Moo attribute with a default — MUST be lazy (see L</The lazy requirement>)
has score => (is => 'rw', lazy => 1, default => sub { 0 });

1;

DESCRIPTION

DBIO::Moo is a thin bridge that activates Moo in a DBIO result class and wires up the constructor so that Moo attributes and DBIO columns coexist without conflict.

When you use DBIO::Moo:

  • Moo is activated (use Moo) in the calling package.

  • DBIO::Core is set as the base class via Moo's extends.

  • A FOREIGNBUILDARGS method is installed that filters constructor arguments so only DBIO-known keys reach DBIO::Row::new.

After use DBIO::Moo, use DBIO::Cake for DDL-style column declarations or use the plain __PACKAGE__->add_columns(...) API. Either way, Moo's has, with, before/after/around are all available.

The constructor problem

This section explains why a naive use base or extends cannot work, and why FOREIGNBUILDARGS is necessary.

A DBIO result class's constructor is DBIO::Row::new. It expects a single hashref and calls store_column for every key it receives. If it sees a key that is not a declared column, relationship, or - prefixed internal key, it dies: No such column 'score' in table 'artists'.

When you use Moo and then extends 'DBIO::Core', Moo generates a new new() in your class that wraps the Moo constructor machinery. The fundamental problem is: Moo's generated new() does not automatically call the non-Moo parent's new(). By default, the non-Moo parent constructor is simply skipped. Without explicit plumbing, you get a Moo object that has none of the DBIO internals set up.

The plumbing Moo provides for this is FOREIGNBUILDARGS. When Moo detects that it is subclassing a non-Moo class, it calls FOREIGNBUILDARGS with the same arguments as new() and passes the return list directly to the non-Moo parent's new. If FOREIGNBUILDARGS is not defined, Moo does not call the parent constructor at all.

DBIO::Moo installs a FOREIGNBUILDARGS that:

1. Normalizes args to a hashref.
2. Looks up the result source to find declared columns and relationships.
3. Passes only DBIO-known keys (columns, relationships, - prefixed internals) to DBIO::Row::new.
4. Leaves pure Moo attributes out of the forwarded args — Moo handles those itself via its own BUILD/accessor initialization.

Without this filtering, passing { name => 'X', score => 42 } to new would cause DBIO::Row::new to call store_column('score', 42) and die because score is not a database column.

Two construction paths

Understanding the distinction between new() and inflate_result is critical for using Moo attributes correctly.

new() — programmatic construction

Used by $rs->create(...) and $rs->new_result(...). Moo's generated constructor runs, initializes Moo attributes, calls FOREIGNBUILDARGS to get filtered args, then calls DBIO::Row::new with those filtered args to set up the DBIO internals (column data, result source, storage link).

inflate_result() — construction from database rows

Used by every query: find, search, all, etc. DBIO::Row::inflate_result blesses a pre-built hash directly into your class and sets up the DBIO internals without going through new() at all:

bless { _column_data => \%row, _result_source => $rsrc, ... }, $class;

Moo's constructor never runs. This means: Moo attributes are never initialized by the constructor when a row is fetched from the database.

The lazy requirement

Because inflate_result bypasses new(), Moo attributes on DB-fetched rows have uninitialized internal slots. Non-lazy attributes with defaults are normally set during Moo's new() — but since new() does not run, those slots remain unset and reading them returns undef instead of the default.

The solution is always declare defaults with lazy => 1:

# WRONG — default never applied to inflate_result rows
has score => (is => 'rw', default => sub { 0 });

# CORRECT — default computed on first access, works for both paths
has score => (is => 'rw', lazy => 1, default => sub { 0 });

With lazy => 1, the default is evaluated the first time the accessor is called, regardless of how the object was created. Both new()-created and inflate_result-created rows behave identically.

Attributes without defaults (is => 'rw' with no default or builder) do not need lazy: they simply start as unset regardless of construction path, which is expected.

Attributes with builder (is => 'lazy') are inherently lazy by Moo's definition and work correctly on both construction paths without any additional configuration.

Manual setup without DBIO::Moo

If you prefer to wire things up yourself instead of using DBIO::Moo, here is exactly what use DBIO::Moo does, spelled out explicitly:

package MyApp::Schema::Result::Artist;

# 1. Activate Moo
use Moo;

# 2. Set DBIO::Core as the base class
use DBIO::Core ();
extends 'DBIO::Core';

# 3. Define FOREIGNBUILDARGS to bridge Moo and DBIO constructors
sub FOREIGNBUILDARGS {
  my ($class, @args) = @_;

  my $attrs = ref $args[0] eq 'HASH' ? $args[0]
            : @args                   ? { @args }
            :                           {};

  my $rsrc = do { local $@; eval { $class->result_source_instance } };
  return ($attrs) unless $rsrc;

  my %dbio_args;
  for my $key (keys %$attrs) {
    if ($key =~ /^-/ || $rsrc->has_column($key) || $rsrc->has_relationship($key)) {
      $dbio_args{$key} = $attrs->{$key};
    }
  }
  return (\%dbio_args);
}

# 4. Now declare your columns and Moo attributes as normal
use DBIO::Cake;

table 'artists';
col id   => serial;
col name => varchar(100);
primary_key 'id';

has score => (is => 'rw', lazy => 1, default => sub { 0 });

1;

Historical context

DBIx::Class::Moo::ResultClass (by ribasushi) was the original solution for combining Moo with DBIx::Class. It used the same FOREIGNBUILDARGS approach and was the reference implementation that informed DBIO::Moo's design.

The key insight from that work: the FOREIGNBUILDARGS filter is essential. Without it, every call to $rs->create({ name => 'X', score => 5 }) would die because DBIO's store_column rejects unknown keys. With it, Moo handles score and DBIO handles name — each layer sees only what it owns.

Interaction with DBIO::Cake

DBIO::Cake keywords (table, col, primary_key, etc.) call __PACKAGE__->add_columns, __PACKAGE__->set_primary_key, etc. on the result class at compile time. This works correctly after use DBIO::Moo because DBIO::Core is already in the inheritance chain when the keywords run.

Interaction with Moo roles

Moo roles applied with with work normally. The role's requires are satisfied by either DBIO column accessors (they are plain subs installed in the package) or other Moo attributes.

with 'MyApp::Role::HasDisplayName';

SEE ALSO

DBIO::Core, DBIO::Cake, DBIO::Moose, Moo, Moo::Role

AUTHOR

DBIO & DBIx::Class Authors

COPYRIGHT AND LICENSE

Copyright (C) 2026 DBIO Authors Portions Copyright (C) 2005-2025 DBIx::Class Authors Based on DBIx::Class, heavily modified.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.