Using DBIx::Class::Schema::Loader to find design flaws

If you avoid DBIx::Class because of performance issues, you should probably check out version 0.08250 or better. In an interesting note from the Changes file:

Rewrite from scratch the result constructor codepath - many bugfixes and performance improvements (the current codebase is now capable of outperforming both DBIx::DataModel and Rose::DB::Object on some workloads).

Previously I used it because with DBIx::Class::Schema::Loader, I could pregenerate my classes from a database schema and it's fairly robust. When the database changes, I can regenerate my classes on the fly and and run the tests to verify that nothing's broken. However, I hit upon a case where it generated some bad code. As it turns out, DBIx::Class::Schema::Loader was correct and it revealed a bug in my database design.

Let's say I have the following two tables:

CREATE TABLE people (
    person_id INTEGER PRIMARY KEY AUTOINCREMENT,
    name      VARCHAR(255) NOT NULL,
    email     VARCHAR(255)     NULL UNIQUE,
    birthday  DATETIME     NOT NULL
);

CREATE TABLE customers (
    customer_id    INTEGER PRIMARY KEY AUTOINCREMENT,
    person_id      INTEGER  NOT NULL,
    first_purchase DATETIME NOT NULL,
    FOREIGN KEY(person_id) REFERENCES people(person_id)
);

In this case, we have people and each person might be a customer. I used the following code to generate my classes:

use strict; 
use warnings;
use Config::Any;
use DBIx::Class::Schema::Loader 'make_schema_at';

my $file = 'config/my.conf';

my $config
  = Config::Any->load_files( { files => [$file], use_ext => 1 } );
my $connect_info = $config->[0]{$file}{db_connect}
  or croak("Could not load connect info for {$file}{db_connect}");

make_schema_at(
    "My::Schema",
    {   dump_directory => './lib',
        components     => ["InflateColumn::DateTime"],
    },
    [ @{$connect_info}{qw/dsn user pass/} ],
);

However, in My::Schema::Result::Person (note that the schema loader intelligently determines that the singular of "People" is "Person"), I had the following relation created:

__PACKAGE__->has_many(
  "customers",
  "My::Schema::Result::Customer",
  { "foreign.person_id" => "self.person_id" },
  { cascade_copy => 0, cascade_delete => 0 },
);

What? That should be a might_have relationship (a Person object might_have a Customer object associated with it). After a little bit of digging, I found the problem.

CREATE TABLE customers (
    customer_id    INTEGER PRIMARY KEY AUTOINCREMENT,
    person_id      INTEGER  NOT NULL UNIQUE,
    first_purchase DATETIME NOT NULL,
    FOREIGN KEY(person_id) REFERENCES people(person_id)
);

Notice anything different? I now have a unique constraint on the person_id column, something I should have had before.

One of the nice things about the schema loader is this:

# Created by DBIx::Class::Schema::Loader v0.07039 @ 2014-02-10 14:19:32
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:dDDvuJIAuI+TebFR+WB7fA

If you need custom code added to your classes, just add it after that line. For my person class, I can add this:

sub is_customer {
    my $self = shift;
    return defined $self->customer;  
}

When I regenerate my classes, my custom code is preserved.

I used to use DBIx::Class::Schema::Loader with DBIx::Class because it saved me development time at the cost of performance. With the new DBIx::Class work, I now get the best of both worlds.

1 Comment

That's pretty slick! Thanks for posting this.

About Ovid

user-pic Freelance Perl/Testing/Agile consultant and trainer. See http://www.allaroundtheworld.fr/ for our services. If you have a problem with Perl, we will solve it for you. And don't forget to buy my book! http://www.amazon.com/Beginning-Perl-Curtis-Poe/dp/1118013840/