Posted about 10 years ago. Visible to the public. Repeats.

Merging two arbitrary ActiveRecord scopes

Rails 3+ allows you to join two scopes from arbitrary sources by calling the merge method:

Copy
scope1 = User.where(:email => 'foo@bar.com') scope2 = User.where(:first_name => 'hans') merged_scope = scope1.merge(scope2)

merged_scope.to_a will now trigger a query for the combined scope chain:

Copy
SELECT `users`.* FROM `users` WHERE `users`.`email` = 'foo@bar.com' AND `users`.`first_name` = 'hans'

If you are joining two models, you can also merge scopes for different models (old blog post, card.

Also note that #merge has a bug in Rails 3.x where merging two scopes with conditions on the same column will discard all by the last condition. This bug is fixed in our forks).

This is nice for nested resources

Assume a model where a deal has many documents:

Copy
class Deal < ApplicationRecord has_many :documents end class Document < ApplicationRecord belongs_to :deal end

You also have a Consul power that specifies which deals documents are accessible by a user:

Copy
class Power include Consul::Power ... power :deals do Deal.where(author_id: user.id) end power :documents do if admin? Document.all else Document.where(visibility: 'public') end end end

In our routes.rb we have defined a resource to list all documents:

Copy
resources :documents, only: :index

Here is a DocumentsController that respects the authorization rules from our Consul power:

Copy
class DocumentsController < ApplicationController def index @documents = current_power.documents end end

Now we get a new requirement: There should also be a screen to list documents pertaining to a given deal.

To implement this, we nest a :documents resource into our existing :deals resource:

Copy
resources :documents, only: :index resources :deals do resources :documents, only: :index, controller: 'deal/documents' end

This creates a routing helper that take a deal argument:

Copy
deal = Deal.find(5) deals_path # => /deals deal_path(deal) # => /deals/5 deal_documents_path(deal) # => /deals/5/documents

When we implement DealDocumentsController we must filter the document list by two conditions:

  • Only show documents pertaining to the given deal (Deal#documents)
  • Only show documents that the user is allowed to see (`Power#documents)

We can use the #merge method to combine both conditions:

Copy
class DealDocumentsController < ApplicationController def index @deal = current_power.deals.find(params[:deal_id]) @documents = current_power.documents.merge(@deal.documents) end end
Growing Rails Applications in Practice
Check out our new e-book:
Learn to structure large Ruby on Rails codebases with the tools you already know and love.

Owner of this card:

Avatar
Henning Koch
Last edit:
6 days ago
by Henning Koch
Keywords:
chain, rails, relation
About this deck:
We are makandra and do test-driven, agile Ruby on Rails software development.
License for source code
Posted by Henning Koch to makandra dev
This website uses short-lived cookies to improve usability.
Accept or learn more