DBIx and inheritance in Perl
Asked Answered
H

3

7

I'm currently trying to implement the following scenario with DBIx:

The table products contains "general products" and "bundle products" (bundle products are collections of general products):

package Product;
use base 'DBIx::Class::Core';
__PACKAGE__->table("products");
__PACKAGE__->add_columns(
  "productId",
  { data_type => "varchar", is_nullable => 0, size => 10},
  "name",
  { data_type => "varchar", is_nullable => 1, size => 150},
  "type",
  {
     data_type => "enum",
     default_value => "general",
     extra => {
       list => ["general", "bundle"],
     },
     is_nullable => 0,
  });

As you can see, wether the product is a general product or a bundle product is saved in the column type.

Now I would like to encapsulate this information in the class identity: I would like to have following classes:

  • Product (type does not matter)
  • BundleProduct (type = 'bundle')
  • GeneralProduct (type = 'general')

I wrote:

package BundleProduct;
use base 'Product';

__PACKAGE__->resultset_attributes({ where => { 'type' => 'bundle' } });
1;

and

package GeneralProduct;
use base 'Product';

__PACKAGE__->resultset_attributes({ where => { 'type' => 'general' } });
1;

But when executing

my @allProducts = $schema->resultset('BundleProduct')->all;

all general products are fetched. Although the resulting objects are of instance BundleProduct, the generated SQL contains the WHERE-condition of the class GeneralProduct (type = 'general'). Even worse: If I try to fetch a Product (base class of BundleProduct and GeneralProduct) the condition type = 'general' is applied, too! It seems that the definition within GeneralProduct overwrites all other definitions.

What is wrong with my design?

Hubey answered 11/10, 2012 at 16:51 Comment(0)
T
4

The usage of resultset_attributes is not recommended. You should implement a result set class for Product with methods bundle_products and general_products:

package My::Schema::ResultSet::Product;
use base 'DBIx::Class::ResultSet';

sub bundle_products  { shift->search({ type => 'bundle' }); }
sub general_products { shift->search({ type => 'general' }); }

Then you can search specific products like this:

$schema->resultset('Product')->bundle_products->all;
$schema->resultset('Product')->general_products->all;

See the documentation of resultset_attributes.

Also have a look at DBIx::Class::DynamicSubclass. It adds some useful features when subclassing results.

Toting answered 7/3, 2013 at 12:53 Comment(2)
++ for DBIx::Class::DynamicSubclass in this scenarioAphoristic
... or do it "by hand" as described in the cookbook: search.cpan.org/~ribasushi/DBIx-Class-0.08250/lib/DBIx/Class/…Aphoristic
S
0

It may always be defaulting to general because the data is not being inflated to an Enum object?

Surprised that it doesn't give a clear error but perhaps adding below (to Product package) will fix your problem:

__PACKAGE__->load_components(qw/InflateColumn::Object::Enum/);

Additional to above also try adding is_enum => 1 to your type column definition:

type => {
  data_type     => "enum",
  is_enum       => 1,  
  default_value => "general",
  is_nullable   => 0,
  extra => {
    list => ["general", "bundle"],
  },
},

NB. This should enforce the use of the inflation object (Object::Enum) whereas without it I believe it tries to use the enum native to the RDBMS you're using (if its present).

Fingers crossed this works. If not try removing the default_value to see how this affects things.

Slut answered 11/10, 2012 at 18:39 Comment(2)
@dreagtun Thank you, but I think that InflateColumn::Object::Enum is only designated for converting varchar fields into enum fields within the application. linkHubey
Looking at the tests provided by DBIx::Class::InflateColumn::Object::Enum module the author always loads the component and also sets is_enum => 1. Here's the test that matches what you're trying todo in your example: metacpan.org/source/JMMILLS/… The author maybe being over zealous but it maybe worth a try. Also author provides no default_value tests so there could be an issue there? Anyway I've updated my answer accordingly.Slut
G
0

This might be slightly OT, but I seem to always run into some insidious application implementation problem when dealing with enum data types in my schemas.

So I don't use them any more. I use foreign key relationships and either individual tables or a combined table holding all codes:

<id, code_type, code_name> 
< 1, 'product_type', 'bundle'>
< 2, 'product_type', 'general'>

and then I join from product to product_type on product.product_type_id = code_table.id

This technique has made application implementation much, much easier at the cost of some additional database management in the beginning of the project.

Gunplay answered 11/10, 2012 at 20:3 Comment(2)
You can avoid the join by making a product_types table with a single field, the type name, which has a unique constraint and is a foreign key into the products table. The foreign key will enforce the limited set of values for the field, and since the key is the name rather than a number, there's no need to do extra joins.Dunaville
True. but I no longer use data for keys. No matter how hard I try, somebody eventually wants to change the key data and then you have all the extra work getting those updates done. If the code is used often, it will be in memory so the join won't take much time. Why on earth is everybody afraid of joins?Gunplay

© 2022 - 2024 — McMap. All rights reserved.