fREWdiculous!
17 Feb
In the most well designed databases that I’ve used we never really deleted anything from the database. We would just mark a field as deleted and then just make sure to filter out the deleted data when we searched and it was all groovy. You could easily readd the item and you never truly lost much data.
Well, now that I am using an ORM I’d like a similar feature in my current database and I’d like it to be as automatic as possible. The first thing I did was, in the Model class, override the delete method. Easy peasy:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | package ACD::Model::CustomerBillingAddress; use base DBIx::Class; __PACKAGE__->load_components(qw/PK::Auto Core InflateColumn::DateTime/); __PACKAGE__->table('CustomerBillingAddresses'); __PACKAGE__->add_columns(qw/ customer_id id # ... creation_date deletion_date phone fax /); __PACKAGE__->set_primary_key(qw/customer_id id/); __PACKAGE__->belongs_to('customer' => 'ACD::Model::Customer', 'customer_id'); sub delete { my $self = shift; $self->update({ deletion_date => \"GETDATE()" }); } 1; |
And then to filter out the deleted rows I just did this in my Contoller’s search function:
1 | $search->{deletion_date} = \"IS NULL"; |
(I have a Controller based function because it also turns all the data into json, paginates it, etc.)
But setting that in every single model class is Bad Design. What if I decided to switch to a boolean instead of a date? So I did some research and found out that with Components I could change the delete method. So here is a component that does what I want:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
That’s pretty cool! And then to use it I just add this line to my model classes:
1 2 3 4 |
Now there is really only one thing left that bugs me about this current interface. When searching I have to remember to filter out the deleted items. Again: bad design! So after more research and help from people in #dbix-class I came up with this solution. First we make a new ResultSet class:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
That override’s the search method and add’s deletion_date IS NULL to the sql query. The next::method call is what would be super in java, except not quite the same because it allows for multiple inheritance.
And then, this is the best part, to have the models automatically use this resultset we add the following method to our ParanoidDeletion Component:
1 2 3 4 5 | sub table { my $class = shift; $class->next::method( @_ ); $class->resultset_class('ACD::ParanoidResultSet'); } |
and that’s basically it! The only real thing left to do is allow the user of the class to specify which column will be set on deletion and then package it up and send it to CPAN!
Enjoy!
4 Responses for "Paranoid Deletion in DBIx::Class"
Hi fREW,
A few things to point out about this method:
deleted_date => \”IS NULL” is spelt deleted_date => undef, in DBIx::Class (and SQL::Abstract, which is the bit that actually turns the syntax into SQL).
Overriding search is a bit odd. It won’t be called when you fetch items of this class via a relation, so those will get deleted items as well. If you never want to fetch deleted items, you can add a permanent extra where-clause to all resultsets of this class:
__PACKAGE__->resultset_attributes({ where => { deleted_date => undef } });
If you ever need to access the deleted ones for any purpose, you’ll need to add a second result class without this line.
Lastly, why override the table() sub when you can just do:
__PACKAGE__->resultset_class(‘ACD::ParanoidResultSet’);
in your Result class.
Jess
@Jess: Yeah, I have learned a lot of the things you mention since I wrote that article. The attributes thing you mention is also very important as that applies to joins from other models as well, vs my search which doesn’t.
I tried your last part initially and it didn’t work. I recommend looking at the cookbook for why: you have to instantiate the model before you set the resultset_class, so you have to do it in table. I could be wrong about that, but a co-worker and I tried it just yesterday outside of table and we had no luck until we put it in the table method.
Yeah, erm, Jess is the DBIC docs lead, she wrote a lot of the cookbook.
If you put that line -under- the ->table call it’d work fine. Equally, given __PACKAGE__->load_namespaces in your schema instead of load_classes you could have a default resultset class without any typing at all.
I’d suggest you look at DBIx-Class-Journal in svn for a better approach than this though – your current system scales kinda horribly over time and I’d suggest you’ll find having separate audit tables much more effective.
If you’re going to be paranoid about deletes, you need to be paranoid about updates too, as updates can easily be just as destructive. Hence change auditing records are good.
Leave a reply