If you rewrite it to the new 3.0 syntax, your first attempt would probably be:

class Page < ActiveRecord::Base
default_scope where(:deleted_at => nil)
def self.deleted
with_exclusive_scope :find => where('pages.deleted_at IS NOT NULL') do
all
end
end
end

However, if you try it out on console, you will find out it does not work as expected:

Page.all #=> SELECT "pages".* FROM "pages" WHERE ("pages"."deleted_at" IS NULL)
Page.deleted.all #=> SELECT "pages".* FROM "pages" WHERE ("pages"."deleted_at" IS NULL) AND ("pages"."deleted_at" IS NOT NULL)

To understand why it does not work, let’s take a look at the source code!

Investigating the issue

With Active Relation, Active Record is no longer responsible to build queries. That said, ActiveRecord::Base is not the one that implements where() and friends, in fact, it simply delegates to an ActiveRecord::Relation object. From ActiveRecord::Base source code:

Besides, if there is any current_scoped_methods, the scoped method is responsible to merge this current scope into the raw relation. This is where things get interesting.

When you create your model, current_scoped_methods returns by default nil. However, when you define a default_scope, the current scope now becomes the relation given to default_scope, meaning that, every time you call scoped, it returns the raw relation merged with your default scope.

The whole idea of with_exclusive_scope is to be able to make a query without taking the default scope into account, just the relation you give in as argument. That said, it basically sets the current_scope_methods back to nil, so every time you call scoped to build your queries, it will be built on top of the raw relation without the default scope.

With that in mind, if we look again at the code which we were trying to port from Rails 2.3, we can finally understand what was happening:

def self.deleted
with_exclusive_scope :find => where('pages.deleted_at IS NOT NULL') do
self
end
end

When we called where('pages.deleted_at IS NOT NULL') above, we were doing the same as: scoped.where('pages.deleted_at IS NOT NULL'). But, as scoped was called outside the with_exclusive_scope block, it means that the relation given as argument to :find was built on top of default_scope explaining the query we saw as results.

For example, the following syntax would work as expected:

def self.deleted
with_exclusive_scope do
where('pages.deleted_at IS NOT NULL').all
end
end

Since we are calling where inside the block, the scoped method no longer takes the default scope into account. However, moving the relation inside the block is not the same as specifying it to :find, because if we were doing three queries inside the block, we would have to specify the same relation three times (or refactor the whole code to always do a query on top of this new relation).

That said, it seems the previous with_exclusive_scope syntax does not suit very well with ActiveRecord’s new API. Maybe is it time for change? Can we provide a better API? Which are the use cases?

Identifying the use cases

The with_exclusive_scope method has mainly two use cases. The first one, which we just discussed above, is to allow us to make a query without taking the default scope into account inside our models:

def self.deleted
with_exclusive_scope do
where('pages.deleted_at IS NOT NULL').all
end
end

While this code looks ok, if we think about relations, we will realize that we don’t need to give a block to achieve the behavior we want. If the scoped method returns a raw relation with the default scope, couldn’t we have a method that always returns the raw relation? Allowing us to build our query without taking the default scope into account?

In fact, this method was already implemented in Active Record and it is called unscoped. That said, the code above could simply be rewritten as:

def self.deleted
unscoped.where('pages.deleted_at IS NOT NULL').all
end

Much simpler! So, it seems that we don’t need to support the block usage at all, confirm?

Deny! Going back to the Page example above, it seems we should never see deleted pages, that’s why we set the default_scope to :deleted_at => nil. However, if this application has an admin section, the admin may want to see all pages, including the deleted ones.

That said, what we could do is to have one controller for the normal User and another for the Admin. In the former, we would always use Page.all, and Page.unscoped.all in the latter.

However, if these controllers and views are very similar, you may not want do duplicate everything. Maybe it would be easier if we do something like this:

def resource_class
if current_user.is_admin?
Page.unscoped
else
Page
end
end

And, instead of always referencing the Page class directly in our actions, we could call resource_class. While this solution is also ok, there is a final alternative, that would require no changes to the current code. If you want to use the same controller for different roles, but changing the scope of what they are allowed to see, you could simply use an around_filter to change the model scope during the execution of an action. Here is an example:

That said, being allowed to give a block to with_exclusive_scope is actually useful and since we want to deprecate with_exclusive_scope in favor of unscoped in the future, we brought this very same syntax to unscoped as well:

However, this feels way too hash-ish. Of course, we could use relations to make it a bit prettier:

Page.with_scope :find => where(:active => true) do
Page.all #=> Bring all active pages that were not deleted
end

This is ok, but it seems that we could improve it even more. That said, we added a new method to relations, called scoping:

Page.where(:active => true).scoping do
Page.all #=> Bring all active pages that were not deleted
end

Yeah! Sign me up 'cause this looks way better than the previous syntax! And, if you check the original commit, you will notice the unscoped method with a block simply delegates scoping:

def unscoped
block_given? ? relation.scoping { yield } : relation
end

So, with unscoped and scoping implemented, we just need to commit, git push and be happy, confirm? Deny! There is one last case to check.

create_with

If you payed attention properly, you can notice that every time we called with_exclusive_scope and with_scope, we always passed { :find => relation } as hash, instead of simply giving the relation. This happens because these methods accept two hash keys: find and create.

As you may expect, one specifies the behavior for create and the other for finding. In most of the cases, they are exactly the same and work with the new syntax:

page = Page.where(:active => true).new
page.active #=> true

However, for obvious reasons, this only works if the conditions are given as a hash. Consider this case:

page = Page.where("active = true").new
page.active #=> nil

That said, there may be a few scenarios where you want to specify the creation conditions on its own, explaining the :find and :create options in with_exclusive_scope and with_scope methods. So, how can I achieve it with the new syntax? Easy!

page = Page.create_with(:active => true).new
page.active #=> true

If you provide both conditions as a hash and create_with, create_with always have higher priority: