Contact Me
The Passionate Programmer: Creating a Remarkable Career in Software Development
Rails Recipes

Yesterday I asked on Twitter whether anyone was successfully using is_paranoid in a Rails application, because I had confused myself into thinking it couldn’t possibly work.

The problem I was having wasn’t is_paranoid’s fault, but it turns out it actually can’t do what I wanted it to do in its native state. The explanation of why is something I thought a number of Rubyists might benefit from hearing, so here it is.

Briefly, an explanation of is_paranoid: if you declare an ActiveRecord model to be paranoid, whenever you attempt to delete that model, is_paranoid will instead set a flag which indicates that the record is deleted. is_paranoid uses default_scope to filter out soft-deleted records in your queries. So you can act as if records are deleted without actually removing the rows. If is_paranoid is new to you but sounds familiar, you might be thinking of acts_as_paranoid, which is Rick Olson’s original implementation of this idea.

What I wanted to do for the specific application I’m working on is to declare that every model should inherit the is_paranoid behavior. Easy enough, I thought, given the way these things typically work in Rails’ inheritable accessor setup:

But when I tried to destroy an instance of (for example) Person, the regular ActiveRecord destroy code was invoked and the records were being actually destroyed. Bummer.

So I cracked open the code to is_paranoid and found this perfectly reasonable idiom:

At this point, after pretending I was an idiot for a few minutes, I realized that this code was indeed incapable of doing what I wanted it to do.

Some of you already know why. For the rest of you, let’s talk about how Ruby’s mixins fit into its inheritance mechanism.

Maybe it’s just me, but when I think of something getting “mixed in” to something else, I imagine the two things becoming intertwined. So, the natural assumption when mixing a module into a Ruby class would be that the methods of the module get intertwined with the Ruby class. And for the HelloWorld of mixins, that indeed appears to be the case:

But if you start mixing modules that implement methods the class also implements in, things don’t go quite as smoothly:

Instead of “Overridden do_something” as some might expect, this code prints “Doing something in Thing”.

Why?

Because when we mix a module into a Ruby class, we’re not actually intermingling the methods of the module and the class. Instead we’re inserting the module into the class’s inheritance hierarchy. A good way to see how this works is by using the “ancestors” method:

When a method is called on an instance of SubThing, you can see that it is looked up first in SubThing’s class, then Thing’s class, then in IneffectiveOverride, and so on.

(I used yuml to generate this. Cool site!)

To further demonstrate that mixins don’t really get “mixed in”, notice what happens when you try to include a module at multiple points in the inheritance hierarchy:

If a module is already present at a higher point in the hierarchy, it won’t be mixed in again.

So is_paranoid was apparently built without the goal of being able to mix it into ActiveRecord::Base. Sounds reasonable to me.

12 Comments

  1. Jeremy Says:

    You could just toss a call to “is_paranoid” into #inherited couldn’t you?

  2. Chad Fowler Says:

    Jeremy, yea I ended up getting it working. This was just an explanation of something that I think is easy to misunderstand.

  3. Jeffrey Chupp Says:

    This was an enjoyable read. I hope is_paranoid works out well for you, but let me know if you have any issues. There’s always room for improvement.

  4. Chad Fowler Says:

    Jeff, I like its small size and simplicity. Well done and thanks!

  5. Jack Dempsey Says:

    Nice article Chad. This ties in nicely with some of the posts wycats has made over the last few months about using inheritance and mixins, one of which is here for those interested: http://yehudakatz.com/2009/03/06/alias_method_chain-in-models/

    thx! Jack

  6. Chad Fowler Says:

    Thanks for the link, Jack!

  7. Joe Martinez Says:

    +1 I learned this the hard way yesterday when trying to override ActiveResource’s save implementation…

  8. Tobin Harris Says:

    Thanks for using http://yUML.me, and mentioning it in your blog. Rails recipes is an inspirational book, both in content and format, you da man :)

  9. andhapp Says:

    Although I have read a good explanation about the hierarchy that gets formed when Modules are mixed into ruby classes in “Programming Ruby 1.9”...this was much better.

  10. george Says:

    great reading and visualizin .. alway get stuck on such ancestory. You cannot read such often enough :-)

  11. der_flo Says:

    Am I the only one who misses a solution? How would you e.g. modify “is_paranoid” to circumvent this “problem”? Sorry, I don’t get it yet.

    Thanks, der Flo

  12. Chad Fowler Says:

    der Flo, I didn’t actually provide a solution :) I just admitted that is_paranoid wasn’t intended to work the way I was trying to use it. Jeremy’s idea from the first comment would work.

Sorry, comments are closed for this article.