Rose-DB-Object
view release on metacpan or search on metacpan
lib/Rose/DB/Object/Tutorial.pod view on Meta::CPAN
=item * A list or reference to an array of primary key values (but only if the foreign table has a single-column primary key).
=back
Setting a new list of prices will delete all the old prices. As with foreign keys, any actual database modification happens when the parent object is saved. Here are some examples.
$p = Product->new(name => 'Kite');
$p->prices({ price => 1.23, region => 'US' },
{ price => 4.56, region => 'UK' });
$p->save; # database is modified here
# US: 1.23, UK: 4.56
print join(', ', map { $_->region . ': ' . $_->price } $p->prices);
New prices can be added without deleting and resetting the entire list:
# Add two prices to the existing list
$p->add_prices({ price => 7.89, region => 'DE' },
{ price => 1.11, region => 'JP' });
$p->save; # database is modified here
Passing a reference to an empty array will cause all the prices to be deleted:
$p->prices([]); # delete all prices associated with this product
$p->save; # database is modified here
=head3 Cascading delete
Deleting a product now becomes slightly more interesting. The naive approach fails.
$p->delete; # Fatal error!
# DBD::Pg::st execute failed: ERROR: update or delete on "products"
# violates foreign key constraint "prices_product_id_fkey" on
# "prices"
# DETAIL: Key (id)=(12345) is still referenced from table "prices".
Since rows in the C<prices> table now link to rows in the C<products> table, a product cannot be deleted until all of the prices that refer to it are also deleted. There are a few ways to deal with this.
The best solution is to add a trigger to the C<products> table itself in the database that makes sure to delete any associated prices before deleting a product. This change will allow the naive approach shown above to work correctly.
A less robust solution is necessary if your database does not support triggers. One such solution is to manually delete the prices before deleting the product. This can be done in several ways. The prices can be deleted directly, like this.
foreach my $price ($p->prices)
{
$price->delete; # Delete all associated prices
}
$p->delete; # Now it's safe to delete the product
The list of prices for the product can also be set to an empty list, which will have the effect of deleting all associated prices when the product is saved.
$p->prices([]);
$p->save; # All associated prices deleted here
$p->delete; # Now it's safe to delete the product
Finally, the L<delete()|Rose::DB::Object/delete> method can actually automate this process, and do it all inside a transaction as well.
$p->delete(cascade => 1); # Delete all associated rows too
Again, the recommended approach is to use triggers inside the database itself. But if necessary, these other approaches will work too.
=head3 Many-to-many relationships
The final relationship type is the most complex. In a "many to many" relationship, a single row in table A may be related to multiple rows in table B, while a single row in table B may also be related to multiple rows in table A. (Confused? A conc...
This kind of relationship involves three tables instead of just two. The "local" and "foreign" tables, familiar from the other relationship types described above, still exist, but now there's a third table that connects rows from those two tables. ...
Let's add such a relationship to our growing family of classes. Imagine that each product may come in several colors. Right away, both the "one to one" and "many to one" relationship types are eliminated since they can only provide a single color...
But wait, isn't a "one to many" relationship suitable? After all, one product may have many colors. Unfortunately, such a relationship is wasteful in this case. Let's see why. Imagine a C<colors> table like this.
CREATE TABLE colors
(
id SERIAL NOT NULL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
product_id INT NOT NULL REFERENCES products (id)
);
Here's a simple C<Color> class to front it.
package Color;
use base 'My::DB::Object';
__PACKAGE__->meta->setup
(
table => 'colors',
columns =>
[
id => { type => 'serial', primary_key => 1, not_null => 1 },
name => { type => 'varchar', length => 255, not_null => 1 },
product_id => { type => 'int', not_null => 1 },
],
foreign_keys =>
[
product =>
{
class => 'Product',
key_columns => { product_id => 'id' },
},
],
);
Finally, let's add the "one to many" relationship to the C<Product> class.
package Product;
use base 'My::DB::Object';
__PACKAGE__->meta->setup
(
...
relationships =>
[
colors =>
{
type => 'one to many',
lib/Rose/DB/Object/Tutorial.pod view on Meta::CPAN
product_id | color_id
------------+----------
10 | 1
10 | 2
20 | 3
20 | 2
20 | 1
Each color appears only once, and the mapping table handles all the connections between the C<colors> and C<products> tables.
The "many to many" C<colors> method works much like the "one to many" C<prices> method described earlier. The valid argument formats are the same.
=over 4
=item * A list or reference to an array of objects of the appropriate class.
=item * A list or reference to an array of hash references containing name/value pairs used to construct such objects.
=item * A list or reference to an array of primary key values (but only if the foreign table has a single-column primary key).
=back
The database modification behavior is also the same, with changes happening when the "parent" object is saved.
$p = Product->new(id => 123)->load;
$p->colors({ name => 'green' },
{ name => 'blue' });
$p->save; # database is modified here
Setting the list of colors replaces the old list, but in the case of a "many to many" relationship, only the map records are deleted.
$p = Product->new(id => 123)->load;
$p->colors({ name => 'pink' },
{ name => 'orange' });
# Delete old rows in the mapping table and create new ones
$p->save;
New colors can be added without deleting and resetting the entire list:
# Add two colors to the existing list
$p->add_colors({ name => 'gray' },
{ name => 'red' });
$p->save; # database is modified here
Passing a reference to an empty array will remove all colors associated with a particular product by deleting all the mapping table entries.
$p->colors([]);
$p->save; # all mapping table entries for this product deleted here
Finally, the same caveats L<described earlier|/"Cascading delete"> about deleting products that have associated prices apply to colors as well. Again, I recommend using a trigger in the database to handle this, but L<Rose::DB::Object>'s cascading de...
# Delete all associated rows in the prices table, plus any
# rows in the product_color_map table, before deleting the
# row in the products table.
$p->delete(cascade => 1);
=head3 Relationship code summary
To summarize this exploration of inter-table relationships, here's a terse summary of the current state of our Perl classes, and the associated database tables.
For the sake of brevity, I've chosen to use the shorter versions of the foreign key and relationship definitions in the Perl classes shown below. Just remember that this only works when your tables, columns, and classes are named according to the ex...
First, the database schema.
CREATE TABLE vendors
(
id SERIAL NOT NULL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
UNIQUE(name)
);
CREATE TABLE products
(
id SERIAL NOT NULL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
vendor_id INT REFERENCES vendors (id),
status VARCHAR(128) NOT NULL DEFAULT 'inactive'
CHECK(status IN ('inactive', 'active', 'defunct')),
date_created TIMESTAMP NOT NULL DEFAULT NOW(),
release_date TIMESTAMP,
UNIQUE(name)
);
CREATE TABLE prices
(
id SERIAL NOT NULL PRIMARY KEY,
product_id INT NOT NULL REFERENCES products (id),
region CHAR(2) NOT NULL DEFAULT 'US',
price DECIMAL(10,2) NOT NULL DEFAULT 0.00,
UNIQUE(product_id, region)
);
CREATE TABLE colors
(
id SERIAL NOT NULL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
UNIQUE(name)
);
CREATE TABLE product_color_map
(
product_id INT NOT NULL REFERENCES products (id),
color_id INT NOT NULL REFERENCES colors (id),
PRIMARY KEY(product_id, color_id)
);
Now the Perl classes. Remember that these must each be in their own ".pm" files, despite appearing in one contiguous code snippet below.
( run in 0.577 second using v1.01-cache-2.11-cpan-39bf76dae61 )