Delete by setting deleted column to not null

From ClassDBI

Jump to: navigation, search

(This is just a copy of something that Tony posted on the list a few months back, but I wanted to have it here for reference.)

It seems that various people use a trick for "deleting" data from their tables when they want to keep the data around for possible restoration or approval of the delete, etc. Instead of actually deleting a row, you have a column called "deleted" that is (say) a timestamp, and defaults to NULL. When you want to "delete" the row, you simply set "deleted" to a not-NULL value--in !MySQL, something like "UPDATE table SET deleted = NOW() WHERE id=$id". Then, all your SELECT queries should have appended to the end of the WHERE clause, "AND deleted IS NULL".

(Naturally this technique is not well suited to extremely large and delete-heavy tables.)

In CDBI, you can accomplish this by overriding a few methods, like so, and you'll get this functionality in a perfectly transparent fashion. And if you want to work with the "deleted" rows in any way, you can have a separate subclass that doesn't have these overrides. This has worked well for me in the past. Just add to a subclass:

__PACKAGE__->set_sql(Retrieve => <<);
       SELECT __ESSENTIAL__
       FROM   __TABLE__
       WHERE  DELETED IS NULL
       AND    %s
__PACKAGE__->set_sql(RetrieveAll => <<);
       SELECT __ESSENTIAL__
       FROM   __TABLE__
       WHERE  DELETED IS NULL
__PACKAGE__->set_sql(DeleteMe => <<"");
      UPDATE __TABLE__
      SET deleted = NOW()
      WHERE  __IDENTIFIER__

et voila!

(Tony warns that this may not be stable for the long-term, but some similar thing is likely to be easy to do in the future.)


If you need to *really* delete a record or retrieve records that have been marked deleted, I've had success creating a real_delete function in my class based on the delete subroutine from ClassDBI and then using the retrieve_from_sql() function to retrieve all rows. I hope others add their notes as the techniques below are hacks!

Here's an example of using retrieve_from_sql to get deleted records:

 my $user = DBI::Users->retrieve_from_sql(qq{id = $id OR (id = $id AND deleted is not null)})->first;

I'm not sure if it is possible to use placeholders in the SQL statement passed to retrieve_from_sql so be sure to check the values with what you pass in. The WHERE clause defined in the retrieve_from_sql method gets appended to the one created in the set_sql statement which is why it is necessary to add the OR clause to include records where deleted is not null.

This can be wrapped into a method called real_retrieve in you CDBI subclass as follows:

 # Usage:
 #   my $user = QT::DBI::Users->real_retrieve($id);
 sub real_retrieve {
   my $class = shift;
   my $id = shift;
 
   return $class->retrieve_from_sql(qq{id = $id OR (dtime IS NOT NULL AND id = $id)})->first;
 }


Here is a sample of a real_delete function (not tested with the set_sql statements above):

 sub real_delete {
   my $class = shift || die "Cannot be called as a class method";
 
   $class->call_trigger('before_delete');
 
   eval {
     my $dbh = $class->db_Main();
     my $sql = qq~
       DELETE FROM users
       WHERE id = ?
       ~;
     my $sth = $dbh->prepare_cached($sql);
     $sth->execute($class->id);
     $sth->finish;
   };
   if ($@) {
     return $class->_croak("Can't delete $class $@", err => $@);
   }
   $class->call_trigger('after_delete');
   undef %$class;
   bless $class, 'Class::DBI::Object::Has::Been::Deleted';
   return 1;
 }


If your class has any relationships (e.g., has_a, has_many, etc.), you will want to override the default delete() method to comment out the triggers in order to prevent it from deleting the related records. Warning: This could have a significant impact on your module! YMMV

Personal tools