caikesouza

Chaining scopes in Rails 4

Coming up in Rails 4 are big changes in ActiveRecord and how scopes work when chained together into a single query.

Up until Rails 3.x, chaining scopes that use the same column have followed the “last where wins” rule. This means that only the last condition involving that specific column is actually used in the query.

# in Rails 3
class User < ActiveRecord::Base
  scope :active,   -> { where(state: 'active') }
  scope :inactive, -> { where(state: 'inactive') }
end

User.active.inactive
# => SELECT * FROM users WHERE state = 'inactive'

Because both scopes are using the same state column, the query in the last used scope overrides the first one. This behavior is confusing and violates the Principle of Least Surprise.

image

Starting in Rails 4, sequential calls to scopes that share a common column between them will simply append the conditions using the AND statement, without dropping any previous conditions.

User.active.inactive
# => SELECT * FROM posts WHERE state = 'active' AND state = 'inactive'

This is now the same behavior as combining class methods which return scopes.

class User < ActiveRecord::Base
  def self.active
    where(state: 'active')
  end
  def self.inactive
    where(state: 'inactive')
  end
end

User.active.inactive
# => SELECT * FROM posts WHERE state = 'active' AND state = 'inactive'

It is now up to the programmer to opt-in to the “last where wins” logic by explicitly using the merge method.

User.active.merge(User.inactive)
# => SELECT * FROM users WHERE state = 'inactive'

Changes like these tend to be the cause for mysterious bugs that take forever to be found in applications that lack a decent test suite, so having strong test coverage is crucial for upgrading your application to Rails 4.

To see the complete list of changes in ActiveRecord, checkout the CHANGELOG

- Carlos Souza

06.07.13 ← See All Posts
blog comments powered by Disqus