Ovid has asked for the wisdom of the Perl Monks concerning the following question:

I've gotten stuck on a problem with code that works perfectly, but is an ugly mess. Here's the general issue: we have products with three types of monetary values: cost, sale price and list price. More price types will be added in the future. However, due to some complications with our price model, we've moved these out of the product table into their own table. We do this because we have various "price models" that alter the price depending upon the product and the customer. Thus, we have three tables: a product table, a price_type table and a product_prices table with an aggregate key of the product id and the price type id. It's getting the price type id which is tricky.

In the price types table, we currently have the following data:

price_type_id price_type_label 1 Sale Price 2 List Price 3 Cost

The problem is, how do we figure out which type is which? As a temporary hack, I've done the following in our price type object:

sub SALE_PRICE_ID {1} sub LIST_PRICE_ID {2} sub COST_ID {3}

In the product object, I have the following:

sub cost { return $_[0]->get_price(PriceType->COST_ID); } sub list_price { return $_[0]->get_price(PriceType->LIST_PRICE_ID); } sub sale_price { return $_[0]->get_price(PriceType->SALE_PRICE_ID); } sub get_price { my ($self, $id) = @_; return unless $id and $id =~ /^[[:digit:]]+$/; my $price_type = PriceType->retrieve($id); $self->_croak "PriceType ID ($id) unknown" unless $price_type; my $sth = $self->sql_find_price; $sth->execute($self->id, $id); my ($price) = $sth->fetchrow_array; $sth->finish(); return $price; }

That is not terribly maintainable. I don't want to hardcode this data into the price types object, nor do I want code that requires that I write new object methods when I add another row to a database. If I can't figure out a better way of dealing with this, I'm going to have a multi-tiered system where my presentation layer is going to be coupled with my database layer!

Eagerly awaiting any and all advice :)

Cheers,
Ovid

New address of my CGI Course.
Silence is Evil (feel free to copy and distribute widely - note copyright text)

Replies are listed 'Best First'.
Re: Database design and Class::DBI
by jwest (Friar) on Mar 05, 2003 at 20:18 UTC
    Instead of declaring those subs statically, you could generate the subs dynamically based on the content of the table.

    BEGIN { my $dbh = DBI->connect(...) or die DBI->errstr; my $sth = $dbh->prepare( qq( SELECT price_type_id, price_type_label FROM price_type ) ) or die $dbh->errstr; $sth->execute or die $sth->errstr; my $data = $sth->fetchall_arrayref or die $sth->errstr; for my $row (@$data) { my ($id, $label) = @{$row}; my $name = lc($label); $name =~ s/\s/_/; eval qq( sub $name { return shift->get_price($id); } ); } }

    Though, I'm sure there's a cleaner way. One could, for example, make the price_type_label the argument to get_price, and resolve the price_type_label to the price_type_id within get_price. This solution provides for the three distinct subroutines for the different price types.

    I'd say this only provides a solution to the part of the problem where the data is hardcoded. The presentation layer still needs to discover which methods will be available to it. Arguably, this could be done using the same table. That doesn't feel particularly clean to me, however.

    minor update: s/tr/s/
    minor update2: s/$_\[0\]/shift/

    --jwest


    -><- -><- -><- -><- -><-
    All things are Perfect
        To every last Flaw
        And bound in accord
             With Eris's Law
     - HBT; The Book of Advice, 1:7
    
Re: Database design and Class::DBI
by adrianh (Chancellor) on Mar 05, 2003 at 22:10 UTC

    I'd use a different object model.

    With the system you've described you've tightly coupled products with changes to the price type. Instead, I'd have a subclass of PriceType for each different type. These would be auto-generated from the price types table.

    That way you can isolate the price type dependant information into the PriceType subclasses.

    Pseudo-code:

    package PriceType; use base qw(Class::Singleton); sub id { croak "this should return the price_type_id" }; my $dbh = DBI->connect(...); my $price_type = $dbh->prepare('select price_type_id, price_type_label + from price_type'); $price_type->execute; $price_type->bind_columns(my \($id, $label) ); while (my $type = $price_type->fetch) { $label =~ = s/[^a-z]//gs; my $method = join('::', __PACKAGE__, $label, "id"); my $id = $id; { no strict 'refs'; *{$method} = sub { $id } }; };

    and get_price in the product class becomes:

    sub get_price { my ($self, $price_type) = @_; croak "need PriceType" unless UNIVERSAL::isa($price_type, 'PriceTyp +e'); my $sth = $self->sql_find_price; $sth->execute($self->id, $price_type->id); my ($price) = $sth->fetchrow_array; return $price; }

    So rather than:

    my $cost = $product->cost; my $list = $product->list_price; my $sale = $product->sale_price;

    you would have

    my $cost = $product->price(PriceType::Cost->instance); my $list = $product->price(PriceType::ListPrice->instance); my $sale = $product->price(PriceType::SalePrice->instance);

    A tad more verbose, but you can now change price types to your hearts content without touching your product class.

    As an alternative, you could argue that the price is more intrinsic to the PriceType than the Product, so you might want to shift get_price into PriceType giving you:

    my $cost = PriceType::Cost->instance->get_price($product); my $list = PriceType::ListPrice->instance->get_price($product); my $sale = PriceType::SalePrice->instance->get_price($product);

    Hope these vague ramblings make some sort of sense :-)

      adrianh, your idea is very insightful and makes perfect sense from a pure Object Design view.

      But I am thinking it would also make sense simply to keep the classes matching the tables, and view those classes as a middle layer between your perl code and the database tables.

      The most straight forward view to look at this is, to take each table as an class, and the table's columns as properties of the class representing it. This is the basic route to move from traditional RD (relational database) towards modern OD (object database).

      Assume the database design is alright, then there would be no need for a subclass of the price type class. If you need a subclass for price type class, so you can store extra info of a particular price type, then where does those extra info being stored in the database? There must be a need for extra columns and different table(s) (which disagrees with the assumption that the database is perfect.)

      So if price type is a table to hold all price types, generally speaking, it indicates that there is no need for subclasses of the price type class.

      Of course, those are just "generally speaking"... but would be usually alright.

      Your thoughts?
        The most straight forward view to look at this is, to take each table as an class, and the table's columns as properties of the class representing it. This is the basic route to move from traditional RD (relational database) towards modern OD (object database).

        The most straightforward view is not necessarily the correct one ;-)

        The one class per table assumption when implementing a relational model in an object oriented world can be an inaccurate oversimplification.

        In most non-trivial database schema you will have tables representing relationships that are better implemented in an object oriented environment by inheritance, object composition, etc. - rather than by separate classes.

        You will also find single tables representing multiple classes. For example, a single table representing users with different roles (admin, normal, publisher, etc.) might be best implemented in an object oriented environment by multiple subclasses of a user class - since the methods applicable to an "admin" user would be different from those applicable to a "normal" user.

        Assume the database design is alright, then there would be no need for a subclass of the price type class. If you need a subclass for price type class, so you can store extra info of a particular price type, then where does those extra info being stored in the database? There must be a need for extra columns and different table(s) (which disagrees with the assumption that the database is perfect.)

        I think the database design is just fine. We have products. We have prices of three different types (cost, list & sale). There are many different ways you could represent this in a relation database. Let's consider three:

        1. We could have a single product table, and three separate price tables cost_price, sale_price and list_price.
        2. We could have a single product table and a single price table, with the different price types indicated by an enumerated type column.
        3. We could have a single product table and a single price table, with the different price types indicated by a relationship with a price type table (i.e. what Ovid originally described).

        The first is obviously lousy since it's not fully normalised.

        The second is fine as far as it goes, but we know from Ovid's original question that it is likely that there are going to be more price types added.

        We don't want to have to change our database schema every time marketing come up with a new pricing scheme. We also don't want to have to change our code every time there is a new pricing scheme - and it's hard to find out what an enumerated type column can represent in DBI.

        We also know that we need to associate labels with each price type for presentational purposes - surely this would be better in the database than in the code.

        Which, of course, leads us to the third option listed above. We have the different types represented explicitly as a separate table that we can query and alter without having to change the database schema or code. We can associate labels with each type easily so we don't have to recode our presentation layer every time we change our underlying price types.

        There are, in turn, many possible OO implementations of the above schema. Each with their own advantages and disadvantages. Since we know that price types are likely to change it would seem best to ensure that these changes have as little impact as possible on the rest of the system. To me, isolating the functionality relating to prices and price types into a single PriceType class hierarchy seems a good way of doing this.

        So if price type is a table to hold all price types, generally speaking, it indicates that there is no need for subclasses of the price type class.

        No it doesn't. It may or may not be true - but you cannot argue the number of classes from the number of tables. Relation models and object models are just different.

        Of course, those are just "generally speaking"... but would be usually alright.

        As you can probably guess I would disagree with "usually" :-)

Re: Database design and Class::DBI
by perrin (Chancellor) on Mar 05, 2003 at 20:54 UTC
    The approach jwest suggested is the best you can do for avoiding hardcoding, but honestly I think you are worrying about this way too much. As long as these constants are declared in an obvious place, it seems fine to me. After all, this class where you're putting them is the class that encapsulates all knowledge about how to get this data from the database, right? Of course it's going to change if you change the properties that your objects have.
Re: Database design and Class::DBI
by benn (Vicar) on Mar 05, 2003 at 20:31 UTC
    If I think I understand you right (no guarantee..it's been a long day :-) ), and if I was using a dbms that supports it, I'd plump for price_type being an enumerated set of some kind ('enum' in MySQL), thus avoiding the need for a 3rd table. The enum type can be accessed either with id_number or string.

    So your prices table has a
    price_type : enum('Sale Price',List Price','Cost')
    Then your select simply becomes
    select amount from prices where id=$id and price_type ='$price_type';
    ...where $price_type can either be a string or an id number (1..3)
    There are various ways to enumerate the values in the 'enum' field, allowing you to check for valid types before performing the select - I suspect many of them may DBMS-dependent though.
    Hope this helps.
Re: Database design and Class::DBI
by enoch (Chaplain) on Mar 05, 2003 at 20:09 UTC
    I would have the product object initialize three member vars when it is constructed: $sale_price, $list_price, and $cost_price. Each having the obvious accessors get_sale_price(), get_list_price(), and get_cost_price(). And, I would leave it up to whatever is using the product object to choose which price is appropriate to it's needs.

    Is there a specific reason you cannot do it that way? Are applications alrady using the get_price() call and expecting the product object to return the appropriate price?

    enoch

Re: Database design and Class::DBI
by particle (Vicar) on Mar 05, 2003 at 20:38 UTC

    without addressing the main issue (because i don't have any good ideas,) i will mention that when i see code like

    sub SALE_PRICE_ID {1} sub LIST_PRICE_ID {2} sub COST_ID {3}

    i find people usually mean

    sub SALE_PRICE_ID() {1} sub LIST_PRICE_ID() {2} sub COST_ID() {3}

    which will inline these subs. for a sub to be inlined into a constant, it must have a prototype of "()". you (and others) can find more on this in perlsub, under the "Constant Functions" heading.

    ~Particle *accelerates*

      First, these are method calls and prototypes on method calls are ignored. I realize this is a special case that allows for constant folding -- I don't want to use prototypes for the only and only case where they actually work with methods :) I might encourage others to do Bad Things.

      Second, the discussion of void prototype issues and the resultant "mysterious" behavior is enough for me to avoid using them unless I really, really want them. Also, subclassing them doesn't always work if they've been optimized away. Later versions of Perl (IIRC >= 5.8?) will optimize away the subroutine itself, thus ensuring that calls to SUPER from a subclass will have disappointing results.

      Cheers,
      Ovid

      New address of my CGI Course.
      Silence is Evil (feel free to copy and distribute widely - note copyright text)

        Is there a reason you didn't use the "constant" pragma? Personally I find it annoying and use package variables instead, but I believe it does exactly what you just did.
Re: Database design and Class::DBI
by pg (Canon) on Mar 05, 2003 at 21:01 UTC
    I don’t think the database is well designed, but I know the database design is not from you. ;-)

    I can understand why there are three tables, and I actually agree that’s a must, if you want to make the price-product info fully normalized.

    But the data in price-type table should be only used for two purposes:
    1. As a data integration reference, we can use it to check and make sure all price-id’s in all other tables are valid, i.e. there is row in this table for that price-id.
    2. Store the visual presentation for each price-id, for example, if you want to print some report, instead of showing the less meaningful price-id, you may want to show the price type.
    But to use price-type as part of unique keys elsewhere is a bad idea. Only price-id should be used for that purpose.

    In the product-price table, instead of storing price-type, should storing price-id, and form unique index together with product-id.
      I am Ovid's coworker and this part of the database was designed by me.

      The product_prices table does store the price_id and uses (product_id, price_id) as the primary key. It wouldn't be normalized otherwise. The name in price_types is only used for display purposes.

        Hey, slacker! Get back to work! Sheesh. How are we going to get anything done if you're playing on Perlmonks all day? ;)

        Cheers,
        Ovid

        New address of my CGI Course.
        Silence is Evil (feel free to copy and distribute widely - note copyright text)

        Okay, then I don’t have a single problem with the database design. (Scary, the guy did the database design is also here, good I didn’t say any thing too strong ;-).

        To be serious, I don’t have problem with your database design, I said those all because of Ovid’s typos …Well, he is a good guy made couple small typos ;-)

        Good to talk to you. Perl rules.
Re: Database design and Class::DBI
by PodMaster (Abbot) on Mar 05, 2003 at 23:48 UTC
    It looks to me like price_type_id should be a 'SET' or 'ENUM' type, and since those don't/shouldn't change, just hardcode them in your class.


    MJD says you can't just make shit up and expect the computer to know what you mean, retardo!
    I run a Win32 PPM repository for perl 5.6x+5.8x. I take requests.
    ** The Third rule of perl club is a statement of fact: pod is sexy.

Re: Database design and Class::DBI
by toma (Vicar) on Mar 06, 2003 at 09:12 UTC
    One challenge is that you are going to have more pricing models in the future, and there is no guarantee that they will conform to whatever schema or algorithm that you create today.

    To ensure that tomorrow's pricing scheme fits into the schema you design today, you might need to limit the creativity of your marketing department. This is a 'bad idea', because you 'need to eat'.

    Maybe what you need is a nice way to keep your code maintainable in the face of potentially wildly different requirements. Perhaps you should write new modules for the different pricing schemes, using inheritance or calling common subroutines where it makes sense.

    As my friend Steve says, "Make different things different and what is the same the same." If your job is to take other people's money, the answer should be 'yes' whether the implied coding requirements violate design goals or not.

    Some of the research on this topic refers to the buzz-phrase "business rules". I think the idea of encapsulating business rules into a fixed relational schema and associated algorithms is known to be problematic.

    It should work perfectly the first time! - toma