Relationships with optional foreign key values (DBIx::Class)

This entry demonstrates how to implement a relationship with optional foreign key values using DBIx::Class.

Imagine two related tables 'human' and 'cat': A cat belongs to a human but when the human dies the cat can continue to roam freely.

package MyApp::Schema::Result::Human;
use strict;
use warnings;
use base 'DBIx::Class';
__PACKAGE__->load_components(qw/ Core /);
__PACKAGE__->table('human');
__PACKAGE__->add_columns(
    id => {
        data_type         => 'integer',
        is_auto_increment => 1,
        is_numeric        => 1,
    },
);
__PACKAGE__->set_primary_key('id');
__PACKAGE__->has_many(
    'cats',
    'MyApp::Schema::Result::Cat',
    'human_id',
    { cascade_delete => 0 },
);
1;
package MyApp::Schema::Result::Cat;
use strict;
use warnings;
use base 'DBIx::Class';
__PACKAGE__->load_components(qw/ Core /);
__PACKAGE__->table('cat');
__PACKAGE__->add_columns(
    id => {
        data_type         => 'integer',
        is_auto_increment => 1,
        is_numeric        => 1,
    },
    human_id => {
        data_type      => 'int',
        is_numeric     => 1,
        is_foreign_key => 1,
        is_nullable    => 1,
    },
);
__PACKAGE__->set_primary_key('id');
__PACKAGE__->belongs_to(
    'human',
    'MyApp::Schema::Result::Human',
    'human_id',
    { join_type => 'left', on_delete => 'SET NULL' },
);
1;


Notes

  • the 'has_many' relationship in table 'human' has the attribute "cascade_delete => 0", this means that if $human has a $cat and $human is deleted, $cat will not be deleted.

  • the 'human_id' column in table 'cat' is nullable so a cat can exist without having a owner/human.

  • the 'human_id' column in table 'cat' is a foreign key, a foreign key constraint will be created.

  • the belongs_to' relationship in table 'cat' has the attribute "join_type => 'left'", this makes sure that relatonship traversal for a relationship with optional foreign key value works consistently in all situations.

  • the 'belongs_to' relationship in table 'cat' has the attribute "on_delete => 'SET NULL'", this causes the column 'human_id' in table 'cat' to be set to NULL if the related human row is deleted.

I was asked why the 'on_delete' attribute is on the Cat class and not on the Human.

Answer:
'SET NULL' causes the referencing column in the referencing row to be set to null when the referenced row is deleted.
(paraphrased from: http://www.postgresql.org/docs/9.3/static/ddl-constraints.html)

Here is the foreign key constraint DDL for a Postgres DBRMS:
ALTER TABLE "cat" ADD CONSTRAINT "cat_fk_human_id" FOREIGN KEY ("human_id") REFERENCES "human" ("id") ON DELETE SET NULL DEFERRABLE:

Testing

use Test::More;
use Test::Fatal qw/ exception /;
my $human = $schema->resultset('Human')->create({});
my $cat   = $schema->resultset('Cat')->create({ human_id => $human->id });
is(
    exception { $human->delete; },
    undef,
    'human can be deleted even though used in cat.'
);
ok(!$human->in_storage, "human not in storage");
$cat->discard_changes(); # refresh from database
is($cat->human_id, undef, 'human_id has been set to NULL');

Leave a comment

About davewood

user-pic I like Toast.