Scope_out feature: default_scope

Posted by Andreas on Monday, August 13, 2007 at 07:08 (CEST)

I suppose, you know the great scope_out plugin for rails. If not, go check it out, since it’s really great to define DRY scoped associations. Basically, you can use scope_out to define the scoped associations within the model the scope is applied to. However, what I’m missing is a feature like a default scope that can be applied to the model and scopes the default finder.

Consider the following example where a user belongs to a group and a group has many users:

1 class Group < ActiveRecord::Base
2   has_many :users
3 end
4 class User < ActiveRecord::Base
5   belongs_to :group
6 end

I want to distinguish between active and inactive users, so I added a “status” attribute that either contains ‘active’ or ‘inactive’. Using scope_out, you can easily access all active and all inactive users via all othe models that have a has_many :users association:

1 class User < ActiveRecord::Base
2   scope_out :active, :conditions => { :status => 'active' }
3   scope_out :inactive, :conditions => { :status => 'inactive' }
4 end

So far, that’s great… You get group.users.active and group.users.inactive. However, next I’d like to track an additional status of a user: ‘deleted’. If a user is deleted, his database record should not really be destroyed, but marked as ‘deleted’ in the status attribute. This would make it possible to keep a working reference to an existing record of a deleted user, e.g. in a post that this user wrote before he was deleted.

To get scoped finders for deleted and non-deleted users, you however need to add the following scope_out statements to the user model:

1 class User < ActiveRecord::Base
2   scope_out :deleted, :conditions => { :status => 'deleted' }
3   scope_out :non_deleted, :conditions => "status != 'deleted'"
4 end

Now, this works nice, however the drawback is, that you now have to watch out to use group.users.non_deleted at any place you used group.users before. E.g. the controller that takes care of showing a page with all users of a group probably uses group.users to fetch the users of a group. But actually, deleted users should not be displayed by default.

Idea: default_scope

It would be great, to be able to define a default scope that applies to all finder methods that are not otherwise scoped out. E.g. like this:

1 class User < ActiveRecord::Base
2   scope_out :deleted, :conditions => { :status => 'deleted' }
3   default_scope :conditions => "status != 'deleted'"
4 end

This should result in:

1 group.users            # get a list of users where status is not 'deleted' (default scope applied)
2 group.users.active     # get a list of users where status is 'active' (default scope NOT applied)
3 group.users.find(5)    # get user with id 5 (default scope NOT applied)
4 group.users.find(:all, :conditions => '...') # get users which match the condition (default scope NOT applied)

I’m not sure yet, if it would make sense to apply the default scope to automatic finders like find_by_name.

I don’t know how others think about this idea (comments welcome), however I’d probably like it, since it would make things easier in an application I am currently developing (which has a lot of models that have those status attribute).

Unfortunately this idea is just plain theory yet and I probably won’t find time to test it until next week.

Update (2007-08-21): Actually there’s a rails plugin from dvisionfactory that seems to do exactly what I was proposing here.

4 comments

Gravatar
Adam Cohen wrote 15 days later:

Interesting article, although it seems that the idea of overriding the default find method is losing favour in the rails community. Rick Olson no longer seems to support his acts_as_paranoid plugin, which basically overrides the find method to only return records whose ‘deleted_at’ attribute is null. He recommends using scope-out-rails and being explicit with your finders instead (easier to debug when something goes wrong, and you can’t figure out why your find method is returning unexpected values). There’s more information on the subject here:

http://www.railsweenie.com/forums/3/topics/1593
-


Gravatar
Andreas wrote 17 days later:

Indeed. Last weekend, I tried to override the find method in a similar way like acts_as_paranoid does and must admit that it got pretty confusing to debug. Being explicit with finders definitely results in code that is easier to understand and easier to debug. Maybe the saving of modifying a lot of views (like in my case above) is not worth the trouble you get with a modified default finder.

Gravatar
Foliosus wrote 6 months later:

There is one argument in favor of overriding the default finder (like dvisionfactory’s global_scope): legacy data. I’ve got an app running against a legacy DB where people’s identity information (name, email etc.) is stored in a table. The problem is, whenever there’s a change to the identity info, the row has a flag set and a new row is inserted with the all of the person’s identity data. In my app, I don’t care about the historical data, and I don’t have the option of removing it. So in this case, a “set it and forget it” approach to ignoring those records is ideal.

I agree with the core team — overriding the default finders should be avoided — but in this case, it’s the only way to stay DRY.

Gravatar
Andreas wrote over 1 year later:

Looks like a default_scope similar to the idea above was just added to Edge Rails a few days ago: commit

Comments are closed