ANNOUNCE: act_as_soft_deletable - new rails plugin for soft deleting / disabling ActiveRecord models

Acts_as_soft_deletable is a rails plugin that provides the ability to
soft delete or disable models.

When models are destroyed, they will be archived so that they can
later be restored easily. Its similar to acts_as_paranoid but uses a
different approach that should make it a little more foolproof.

See the README at the following url for a better description.

github url: http://github.com/ajh/acts_as_soft_deletable/tree/master

Feedback is always appreciated.

Thanks,

Andy Hartford
Substantial
hartforda@gmail.com

Andy,

This looks awesome! Thanks for putting it together. I really like the
idea of keeping deleted items in separate tables so all the rest of
the AR stuff works as expected. Nice!

One thing I thought of while reading your README:

If you define your "Deleted" tables in a migration, why not build it
so that any future migrations would automatically adjust the deleted
tables? This may be more code on your end as you'd probably have to
either monkeypatch or just hook into the migration process, but it
couldn't be too hard. Pseudocode wise it would probably look like:

in "self.up", if a modification to table X occurs, check if there is
an X::Deleted table. If so, make the same modifications to X::Deleted

The beauty of doing this would be that you define your "deletable"
tables at the beginning, then just forget about 'em... So future
migrations are just normal migrations.

Just a thought, anyway. I'll be adding your plugin to some projects
I'm working on... and ditching my "acts_as_paranoid"-like hacks.

Thanks!

-Danimal

One other thought:

What if you extended this, either as a second plugin, or just
additional features, to add "acts_as_archivable"? Which is effectively
the same thing, but instead of thinking in terms of "delete" and
"undelete", it's "archived" or not.

And, I could see some cool hooks you could do. Like adding some cool
find mods. Off the top of my head:

Assuming I have a Post model with lots of posts, I might want to do
something like:

Post.find_and_archive(:all, :conditions => ["created_at < ?",
1.month.ago])

This would return an array of post objects that are older than 1
month, but it would also "move" them from the posts table to the
archived_posts table.

The other cool thing is that you could then make wicked simple rake
tasks to do archival maintenance.

Maybe even some hooks so that you could do "global" finds... i.e.
finds that would aggregate the X and archived_X tables.

Post.find_with_archive_by_author("John Do") # => would find all posts
from the "posts" table AND the "archived_posts" table

Just some thoughts.

Keep it up!

-Danimal

If you define your "Deleted" tables in a migration, why not build it
so that any future migrations would automatically adjust the deleted
tables?

Yeah, that's a good idea. I did something similar with the
update_columns class method, but your way is definitely slicker. One
thing makes me a little uncomfortable though. If the migration drops a
column, and it automatically gets dropped from the archive table then
data is irretrievably lost. I can't decide if that's a bad thing, I'd
love to get your thoughts on that.

What if you extended this, either as a second plugin, or just
additional features, to add "acts_as_archivable"?Which is effectively
the same thing, but instead of thinking in terms of "delete" and
"undelete", it's "archived" or not.

I originally intended to make an acts_as_paranoid replacement that
worked with rails 2.0 and I got stuck in the 'deleted' paradigm. Now
that you point it out I agree 'archive' is a better metaphor. I'll
have to think about whether I want to go through the trouble to rename
everything :slight_smile:

Post.find_with_archive_by_author("John Do") # => would find all posts
from the "posts" table AND the "archived_posts" table

This is a cool idea too! Its not obvious to me how I'd implement this.
My first thought is method_missing so I could handle methods that
start like 'find_with_archive...'. But I would need a way to decline
to handle stuff like 'find_by_author' so that ActiveRecords
method_missing can do its thing. I'm not sure how to do that but I'll
play around with it.

Keep it up!

-Danimal

Thanks for the great ideas and I love to hear any more feedback and
whether you were able to use it successfully!

Andy

Andy,

Some thoughts to keep this conversation going...

1) Migrations:

A couple of thoughts. First, you could just figure that if someone is
using your plugin and has set the "Archived" tables in their initial
migrations, then they are aware of what deleting columns would mean.
So you could go that route.

Another thought would be to offer some sort of warning... i.e. running
a migration that removes columns would give a warning... which could
be overridden by a flag or even some environment setting.

Another option, although very complex, would be to monkeypatch AR so
that deleting columns doesn't actually delete them but "hides" them.
But the more I think about that, the less I like it.

Ultimately, I'd just go with the first option. If you think about it,
the value here is storing record data beyond the "delete" point (I
like "archive" better). So if the "structure" of the data is changed,
that change should be allowed (in this case, removing columns as part
of a migration). It's the rows of data, if you will, that you want to
maintain. I think if the programmer knows enough to use your plugin
and define the extra tables, then they should be aware and willing to
accept that removing columns means removing them from the
"Deleted" (or "Archived") tables.

2) Archived vs. Deleted

Totally up to you. I like the idea "archived" more because it
syntactically describes what's happening better, IMO. I know, I
know... there is the idea of "undelete", but really... Deleting, to
me, means GONE. Archiving, though, means, "effectively gone... but
still _somewhere_ if I need it".

3) Archival Finds

I think it shouldn't be too hard to do this. I haven't done any
method_missing coding (yet), but the code I have seen looks fairly
straightforward. You might even do some plugin research and find some
others that do some custom Find calls and use that as a starting
point.

Another thought... maybe don't override the method name, but rather
add in a hash parameter... like:

Post.find_all_by_author("John Doe", :order =>
updated_at, :include_archive => true)

Or something like that. That might be easier as you don't need to do
any method_missing hacking... but just intercept that parameter and do
some table-joining.

4) File-based Archival

I could even imagine adding some hook whereby the archival takes
records from the DB and dumps them to some flat-file, or XML, or CSV
or some such format. This may not be necessary... but I can imagine
some possible use. I.e. where you "archive" (to the archive tables)
posts older than 3 months, but you "flat-file-archive" posts older
than 1 year. Just an idea, though... maybe it's not that useful.

As for me, I'll definitely be using this in at least one of my apps.
I've got one that's a very simple contact management and job posting
app. The jobs can be flagged as "filled". I'm thinking that it's the
perfect case to try this out... since I can make the "filled" jobs
just be the "archived" jobs. Then, the normal Job AR stuff only works
on active jobs unless the deleted (er... "filled") jobs are requested.

The biggest win of your approach over acts_as_paranoid is the speedup
in normal queries because the "deleted" data simply isn't in those
tables anymore. And most normal usage won't need the deleted data
anyway. So two big thumbs up on this work!

-Danimal

1) Migrations:
Another thought would be to offer some sort of warning... i.e. running
a migration that removes columns would give a warning... which could
be overridden by a flag or even some environment setting.

I like the warning approach that can be disabled. With that in place,
I don't mind automatically syncing tables rather than the manual
#update_columns call. I'll try to get these changes implemented soon.

You might even do some plugin research and find some
others that do some custom Find calls and use that as a starting
point.

Thats a good idea. I'll look around for that. Maybe someone reading
this thread can suggest something?

4) File-based Archival

I could even imagine adding some hook whereby the archival takes
records from the DB and dumps them to some flat-file, or XML, or CSV
or some such format. This may not be necessary... but I can imagine
some possible use. I.e. where you "archive" (to the archive tables)
posts older than 3 months, but you "flat-file-archive" posts older
than 1 year. Just an idea, though... maybe it's not that useful.

It would also be possible to write a rake task to dump the archival
tables with mysqldump or the equivalent.

The biggest win of your approach over acts_as_paranoid is the speedup
in normal queries because the "deleted" data simply isn't in those
tables anymore. And most normal usage won't need the deleted data
anyway.

Oh yeah, I'll have to add this advantage to my readme!

Take care,
Andy

Andy,

This is good stuff. Keep at it!

I probably won't be able to fiddle with it for a week or so, but it's
definitely a plugin that will be useful in at least one of my projects
(as I mentioned before).

Thanks again for doing this and offering it to the world.

-Danimal

Danimal, I added some of the features you suggested.

I added a find method like ActiveRecord#find that will return deleted
records as well, it's called find_with_deleted. I also added support
for dynamic finders like find_all_with_deleted_by_name_and_date(name,
date).

I added a warning to the update_column migration method that warns
when columns are deleted. The warning can be disabled. I'm still
looking into automatically syncing the tables when the live table
changes.

Andy

Andy, this is awesome! Thanks for the hard work.

-Danimal

Andy,

So you are sticking with the acts_as_soft_deleteable name for the plugin? I’ve got a piece of work coming up that will be using it and I just want to be sure I get synced up with the correct code.

Cheers,

Walter

http://kete.net.nz/

Walter,
Yeah, I'm going to keep the name. I was considering acts_as_archivable
but that's already taken by another plugin which does something
completely different :wink:

I'd love to hear any ideas or feedback you have once you start using
it!

Great. Good to know. I plan to use it on a join model, actually. A related links type thing. Will let you know.

Cheers,

Walter

Sounds neat!

On the archival angle, something I have been wishing for for a while
now is an archival plugin/system that does the following:

1) Tiered time-based (or, really, on any column, I just can't think
for a use for anything other than created/updated_at columns)
backups. For example, move everything over 6 months old to another
table, move everything over a year old to a secondary database.
2) Multiple data stores (multiple DBs, file stores, ???) for
flexibility.
3) The ability to daemonize and run it external to the running web
framework itself (like a cron job) so its transparent to the users of
the system), and the ability to do on-demand archiving in-framework
4) Adds in archival find methods to AR objects marked as archivable,
adds in individual archive/unarchive commands, etc.
5) Updating records in the archives should move them back to the main
table automagically (this could be configurable)

I have programmed multiple order processing systems, and every time I
have yearned for such a thing. Unfortunately, I am really not good
enough with the guts of RoR to pull this off :(.

Shngn wrote:

I'd love to hear any ideas or feedback you have once you start using
it!

I'll try to start using it this week... I'd been tinkering with the
idea of a soft delete via a 'visible' flag or something, but didn't
really like that idea.

Your work looks like just the ticket!