Posts Tagged activerecord

DRY your scopes

I encountered a pattern today that I would like to share:

There is no justification to write such a code in ruby (or in any language)
ActiveRecord 3 with ARel does not defer class singleton methods from scopes and in my opinion they are more readable.
We could write the same functionality with:

Now, the path is short to go to:

I like it – do you?

We didn’t only clean the code.
We did much more – every new status in the STATUS list will effortlessly represented with a matching method.

For instance :curator key which had no scope in the original version is now represented with the User.curator method.

DRY is not (just) about aesthetics. DRY is about maintainability and simplicity.

, , ,

6 Comments

ActiveRecord default_scope

ActiveRecord, starting at v2.3.2, provides with a convenient way to perform default scoping on a model. This feature must be handled with extra thought before applying. Let’s look at the following class:

class Post < ActiveRecord::Base
default_scope where(:deleted_at => nil)
end

We’re going to use a nice trick here in order to display the actual query rails generates for us:
ActiveRecord::Base.logger = Logger.new(STDOUT)

Now let’s start playing with our Post model a little bit:

:046 >; Post.all
SELECT `posts`.* FROM `posts` WHERE `posts`.`deleted_at` IS NULL
=>; []

As expected, activerecord chained the default scope into our initial query.

:047 >; Post.unscoped
SELECT `posts`.* FROM `posts`
=>; []

The unscoped method rips the model of any chained scopes it has been applied with.

To make things a little bit more interesting I’m adding another scope to the Post model:

class Post
scope :id_is_one, lambda { where(:id =>; 1) }
end

and a post(:id =>; 1, :deleted_at =>; Time.now) to the database.

Back to rails console:

:060 >; Post.id_is_one
SELECT `posts`.* FROM `posts`
WHERE `posts`.`deleted_at` IS NULL AND `posts`.`id` = 1
=>; []

Returns an empty result since our post does have a delete_at field which is different than nil.

:061 >; Post.unscoped.id_is_one
SELECT `posts`.* FROM `posts`
WHERE `posts`.`deleted_at` IS NULL AND `posts`.`id` = 1
=>; []

What just happened here? didn’t we unscope our Post model before querying for a post with id 1? We did, but it looks like rails just ignored it and chained the default_scope again.

More interestingly:

:062 >; Post.unscoped.where(:id =>; 1)
SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 1
=>; [#;]

Replacing the named scope with a simple where clause of ActiveRecord does the job.
So, what do we have so far?
1. Chaining an unscoped with named scopes does not supply with expected results.
2. Chaining unscoped with ActiveRecord query methods does supply with expected results.

This raises the obvious question: Can’t we use named scopes ignoring the default scope of a model? And the answer is: yes, we can! (sounds familiar?!) How? It is very simple:

:063 >; Post.unscoped { Post.id_is_one }
SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 1
=>; [#;]

Passing the desired query inside a block does the job as expected.

To conclude: Dealing with default_scope may look appealing at first, but may also give you some hard times when you want to ignore it. It’s efficiency decreases the more times you find yourself trying to ignore it, so make a serious consideration before going this slippery slope.

, , ,

Leave a comment