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.
That's pretty slick! Thanks for posting this.