Default Scopes and Inheritance to the rescue 11

Posted by pratik
on Tuesday, March 24

On my one of the current projects, there are two primary models each with a flag called approved. 99% of the front end part deals with only approved items. Unapproved items are usually only in the admin panel side of the story. So I started with using a named_scope called approved:

1
2
3
4
5
6
class Item < ActiveRecord::Base
  has_many :tags

  default_scope :order => 'items.name ASC'
  named_scope :approved, :conditions => { :published => true }
end

And now I’d have to use Item.approved. everywhere in my application. But that became a bit too cumbersome sooner than later. Playing around with this a bit, I came up with the solution using default_scope and the good ol’ inheritance:

1
2
3
4
5
6
7
8
9
10
11
12
class Item < ActiveRecord::Base
  has_many :tags

  default_scope :order => 'items.name ASC'
end

class PublishedItem < Item
  set_table_name 'items'
  set_inheritance_column nil # hax?

  default_scope :conditions => { :published => true }, :order => 'items.name ASC'
end

Checking this on console :

>> p = PublishedItem.first
  SELECT * FROM `items` WHERE (`items`.`published` = 1) ORDER BY items.name ASC LIMIT 1

>> i = Item.first
  SELECT * FROM `items` ORDER BY items.name ASC LIMIT 1

Seems to work just fine.

You could do it the other way around too:

1
2
3
4
5
6
7
8
9
10
11
12
13
class RawItem < ActiveRecord::Base
  set_table_name 'items'
  has_many :tags

  default_scope :order => 'items.name ASC'
end

class Item < RawItem
  set_table_name 'items'
  set_inheritance_column nil # hax?

  default_scope :conditions => { :published => true }, :order => 'items.name ASC'
end

Whichever one works for you.

Please note that the above code is NOT using STI. It’s using set_inheritance_column nil workaround to bypass the Active Record STI stuff and rely just on the ruby inheritance.

Comments

Leave a response

  1. grosserMarch 24, 2009 @ 02:00 PM

    great idea, ill try adding it to my app :)

    And i would use default scope on item and then use Item.unapproved in admin pages, seems easier to me…

  2. Eric AndersonMarch 24, 2009 @ 04:21 PM

    I dig this. It allows you to dry up your named scopes a bit but still allowing very easy access to the objects without the scope applied (no with_exclusive_scope hacks).

  3. Rodrigo KochenburgerMarch 24, 2009 @ 05:20 PM

    Hey Patrik,

    Any reason why you didn’t use ActiveRecord::Base.abstract_class attribute instead of re-seting the table name and setting inheritance column to nil?

    
    class Item < ActiveRecord::Base
      self.abstract_class = true
      self.table_name 'items'
    
      has_many :tags
      default_scope :order => 'items.name ASC'
    end
    
    class Item < ItemBase
      default_scope :conditions => { :published => true }, :order => 'items.name ASC'
    end
    

    This would still look at the items table, as set in the abstract class, but eliminates the inheritance column hack ;)

    Great tip, anyway.

    Cheers

  4. PratikMarch 24, 2009 @ 05:30 PM

    Hey Rodrigo,

    The inheritance column hack isn’t actually needed if the “type” column is missing. But it’s good to be explicit in cases like this. Using abstract_class would work, but you’re not really supposed to create objects of abstract classes, so you never know when that stops working.

  5. alecoMarch 25, 2009 @ 04:00 PM

    Wouldn’t it be possible to set default_scope to only published items and then in the admin panel use Item.with_exclusive_scope { find(:all) } instead of creating an inheriting class?

  6. Ben HughesMarch 27, 2009 @ 05:46 AM

    Yeah @aleco’s idea could work. It would be nice if you could just chain scopes and .find right off of with_exclusive_scope rather than having to pass it a block as a convenient way to bypass default_scope.

    I find myself in this kind of situation all the time.

  7. Craig BuchekMarch 27, 2009 @ 07:23 PM

    @Ben — I think you’re right. It should be really simple to add named_exclusive_scope.

    @Pratik — It took me some time to figure out what you’re trying to do. If you’re only looking for unpublished items, it doesn’t seem like a whole separate class makes sense. I think aleco’s idea (with Ben and my extensions) would be the best way to handle this situation. (Although I do think your solution is pretty cool, and might have some other uses.)

  8. jorenMarch 30, 2009 @ 09:54 AM

    I really like this solution. I tried to ignore the default scope first with with_exclusive_scope, but it ignore all scope. So user.comments_with_deleted gave me all the comments, and not scoped on ths user.

    I do have problems here with a has_many_and_belongs_to

    I have a certificate and a rawcertificate, on the certificate I have a default scope so that I also have the ‘deleted’ certificates with the rawcertifcate. This model is in a has_many_and_belongs_to relation with a odel called trade.
    But I don’t seem to get the Rawcertificate working with the certificate_trades table where the many to many relation is defined.
    Any solutions on that?

  9. Mark WildenMarch 31, 2009 @ 12:56 AM

    The problem with this is that it models state using types. The state can change, but the type doesn’t: you could get a PublishedItem and set published to false (which could easily happen if the user had the ability to unpublish an item).

    Item and PublishedItem are not really types. They express not qualities of the objects, but qualities of the way those objects are found and created.

    Using inheritance to save keystrokes rather than model data will be surprising to many people.

  10. JETMediaApril 04, 2009 @ 05:23 PM

    While I can see the different directions here I’d like to point out that what led me in pursuit of something like this is that I have a superclass Topic which I am happy to handle in its plural form.

    However I have different subclass topics which have different model internals as they are processed in different ways but don’t deserve separate tables.

    Now I may have a topic called Walking which is of type Walking and I do not wish for it to exist in plural, Walkings form. But where I can cancel pluralize in the environment I don’t wish a blanket cancel on all models, it hence is not an appropriate option.

    I tried setting table name to Topics but this yields an “undefined method Walkings” error.

    I like the inheritance hack so far but I’d rather not have a hack which may break in the future.

    Can you guys suggest something?

  11. JETMediaApril 04, 2009 @ 05:25 PM
    I meant to say

    “I tried setting table name to Topics but this yields an “undefined method Walkings_path” error.”

Comment