Don't Use default_scope For Multi-tenancy

December 30, 2014

I learned a lot while developing my app StudioJoy, which is a Rails 3.2 application. Here’s a look at how I went from using default_scope and back again and why.

StudioJoy is a multi-tenant application, meaning that each studio (or tenant) lives in the same shared database. Obviously we want to limit access of a studio’s data to only that studio and disallow access to all others.

There are several ways to do this in a Rails application such as using the Apartment gem, Multitenant gem or by rolling your own. I chose the latter using default_scope.

In my application, the Studio class models a single customer (e.g. Rex’s Tae Kwon Do) and a Studio has many Members (the students).

Here’s how my Member model looked initially without any scoping:

class Member < ActiveRecord::Base
  ....
  belongs_to :studio
end

And here it is again using default_scope:

class Member < ActiveRecord::Base
  default_scope { where(studio_id: Studio.current_id) }
  ....
  belongs_to :studio
end

The benefit of default_scope is that it allows you to reference your models as if they are already ‘scoped’ to your liking. Here’s an example of something I did in my controller:

class MembersController < ApplicationController
  def index
    # Using default scope
    @members = Member.all
  end
end

In this case the Member model is already scoped to the ‘current studio’, which is set in the ApplicationController to the currently logged in user’s Studio.

This would generate the SQL:

SELECT "members".* FROM "members" WHERE "members"."studio_id" = 2

This works well and good for the ‘typical’ use-cases as long as you only reference the models from within the normal MVC workflow. Once you move beyond this by referencing your models in custom jobs, rake tasks or from the command line, things can get complicated.

For example, lets say we wanted to fire up a rails console and list all of the members in the database for all studios. Normally we would be able to do something like this:

[1] pry(main)> Member.all

However, since we are using default_scope, we get back an empty array since Rails has no idea which Studio to join on:

  Member Load (0.6ms)  SELECT "members".* FROM "members" WHERE "members"."studio_id" IS NULL
=> []

In order to make Rails do what we want, we have to remember to use the unscoped method:

[2] pry(main)> Member.unscoped.all
  Member Load (0.9ms)  SELECT "members".* FROM "members"
=> [#<Member id: 2, first_name: "Zula", last_name: "Kihn", street_address: "303 Bins Lake", city: "West Rickybury", state: "Iowa", zip: "84320", phone: "(555) 555-5555", email: "serena@kutch.info
", birthday: "1967-08-06", active: true, image: nil, start_date: "2014-10-12", end_date: "2015-10-12", membership_type: "monthly", membership_price: #<BigDecimal:7fcf05a74898,'0.5E2',9(18)>, stud
io_id: 2, created_at: "2014-10-12 21:09:00", updated_at: "2014-10-12 21:09:00", level_id: 4, source_id: 5>,

This requires us to remember that we are no longer in the context of the normal MVC workflow and that default_scope will no longer work (since there is no ApplicationController to set the current studio). This also requires us to deal with the framework instead of focusing on the business domain.

There are other unexpected consequences of using default_scope as described in this Rails Best Practices post: Default Scope is Evil.

I finally had enough with default_scope and ripped it out completely from my application. My controller from earlier now looks like this:

class MembersController < ApplicationController
  def index
    @members = current_studio.members
  end
  ...
end

This code is a tiny bit more verbose however you can tell exactly what it’s doing just by looking at it. There is no hidden ‘magic’ to be aware of which allows me to focus more on the problem at hand instead of the framework that I happen to be using.

At first, default_scope seems to be a clean way to implement logic such as this where a model is ‘always’ scoped by another or by some other property while embracing the DRY principle that Rails is known for. However, you can quickly run into issues when trying to do anything outside of the norm when using default_scope, and things that seem intuitive can often produce undesired results. Because of this, I recommend using the common pattern of scoping your models and relationships manually instead of relying on the framework to do it for you.

Let me know about your thoughts and experiences with default_scope in the comments below.

Did you find this content helpful?


Let me send you more stuff like this! Unsubscribe at any time. No spam ever. Period.


Subscribe to MarkPhelps.me

* indicates required

Discussion, links, and tweets

Mark Phelps

I'm a Software Engineer in Durham, NC. I mostly write about Go, Ruby, and some Java from time to time.