NAME

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

VERSION

version 0.900000

SYNOPSIS

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

table 'artists';

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

primary_key 'id';

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

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

__PACKAGE__->meta->make_immutable;
1;

DESCRIPTION

DBIO::Moose is a thin bridge that activates Moose and MooseX::NonMoose in a DBIO result class so that Moose attributes and DBIO columns coexist without conflict.

When you use DBIO::Moose:

  • Moose and MooseX::NonMoose are activated in the calling package.

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

  • A custom FOREIGNBUILDARGS method is installed that filters constructor arguments so only DBIO-known keys reach DBIO::Row::new. This replaces MooseX::NonMoose's default pass-through implementation.

Call __PACKAGE__->meta->make_immutable at the end of your class definition for full Moose optimization. It is safe to do so alongside DBIO.

The constructor problem

This section explains why plain use base or extends cannot work, and why both MooseX::NonMoose and a custom FOREIGNBUILDARGS are required.

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 Moose and extends 'DBIO::Core', Moose generates a new new() in your class. The problem: Moose's generated new() does not know how to call a non-Moose parent's new(). Moose's construction protocol (new_object, BUILD, attribute initializers) is entirely separate from the non-Moose parent's constructor. Without explicit bridging, the non-Moose parent constructor is never called, and the DBIO internals are never initialized.

What MooseX::NonMoose provides

MooseX::NonMoose is a Moose extension that adds non-Moose parent constructor support. It works by overriding new() in the generated metaclass to call the non-Moose parent's new() first, then Moose's own initialization. The mechanism it uses mirrors Moo's: a method called FOREIGNBUILDARGS.

Moose calls FOREIGNBUILDARGS with the same arguments as new() and passes the return list to the non-Moose parent's new. The default FOREIGNBUILDARGS installed by MooseX::NonMoose is a pass-through: it returns all args unchanged.

That default is wrong for DBIO. Passing { name => 'X', score => 42 } to DBIO::Row::new causes store_column('score', 42) to die because score is a Moose attribute, not a database column.

DBIO::Moose installs a replacement FOREIGNBUILDARGS that:

1. Normalizes args to a hashref.
2. Looks up the result source to find declared columns and relationships.
3. Forwards only DBIO-known keys (columns, relationships, - prefixed internals) to DBIO::Row::new.
4. Leaves pure Moose attributes out — Moose handles those itself via attribute initialization.

Note that DBIO::Moose always installs FOREIGNBUILDARGS unconditionally (unlike DBIO::Moo, which skips it if one already exists). This ensures the filtering version replaces MooseX::NonMoose's pass-through default, which has already been installed into the class by the time import runs.

Two construction paths

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

new() — programmatic construction

Used by $rs->create(...) and $rs->new_result(...). The MooseX::NonMoose-enhanced Moose constructor runs, calls FOREIGNBUILDARGS to get filtered args, calls DBIO::Row::new with those filtered args (setting up column data, result source, storage link), then initializes Moose attributes.

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 without going through new() at all:

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

Moose's constructor never runs. Moose attributes are never initialized by the constructor when a row is fetched from the database.

The lazy requirement

Because inflate_result bypasses new(), Moose attributes on DB-fetched rows have uninitialized internal slots. Non-lazy attributes with defaults are normally set during Moose'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', isa => 'Int', default => 0);

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

With lazy => 1, the default is evaluated the first time the accessor is called, regardless of how the object was created. This applies to default and builder alike. Attributes declared with builder should also be marked lazy => 1 unless the build method does not depend on column data.

make_immutable and DBIO

Calling __PACKAGE__->meta->make_immutable replaces the Moose-generated new() with a faster, inlined version. This is safe with DBIO because:

  • inflate_result never calls new() — make_immutable does not affect the database-fetch path at all.

  • The inlined new() preserves the FOREIGNBUILDARGS call added by MooseX::NonMoose, so DBIO initialization still happens correctly for create and new_result.

Always call make_immutable at the end of your class definition, after all has, with, and before/after/around declarations, and after any use DBIO::Cake column declarations.

Moose roles

Moose 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 Moose attributes.

with 'MyApp::Role::HasDisplayName';

# or multiple roles at once:
with 'MyApp::Role::HasDisplayName', 'MyApp::Role::Auditable';

Type constraints (isa) in roles are enforced at object construction time and on every mutation, as expected.

Manual setup without DBIO::Moose

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

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

# 1. Activate Moose and MooseX::NonMoose
use Moose;
use MooseX::NonMoose;

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

# 3. Override MooseX::NonMoose's pass-through FOREIGNBUILDARGS with a
#    filtering version that keeps only DBIO-known keys
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 Moose attributes as normal
use DBIO::Cake;

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

has score => (is => 'rw', isa => 'Int', lazy => 1, default => 0);

__PACKAGE__->meta->make_immutable;
1;

Important: If you use MooseX::NonMoose without defining your own FOREIGNBUILDARGS, MooseX::NonMoose's default pass-through is used, and DBIO::Row::new will die on any Moose attribute key it receives. You must override FOREIGNBUILDARGS as shown above.

Historical context

The combination of Moose with a non-Moose ORM has a long history in the Perl ecosystem. MooseX::NonMoose was written specifically to handle this class of problem. The DBIO::Moose design mirrors what DBIx::Class::Moo::ResultClass does for Moo (by ribasushi): install a filtering FOREIGNBUILDARGS to separate the ORM constructor arguments from the OO framework attributes.

The key insight: neither DBIO nor Moose is wrong. DBIO's store_column correctly rejects unknown keys — that is a feature, not a limitation. Moose correctly passes all constructor arguments to attribute initializers — that is also a feature. The role of FOREIGNBUILDARGS is to stand between the two and give each framework only the keys 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::Moose because DBIO::Core is already in the inheritance chain when the keywords run.

SEE ALSO

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

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.