Simple Rails Multi-Tenancy

With some recent commits to rails comes the ability to have a `default_scope` with a lambda that gets evaluated at query time. Combine that with a multi-tenant column scoped database structure (wow, quite the mouthful) and you’ve got an quick and painless way to partition your tenant data without scattering code everywhere.

Getting Started

Let’s start off with a fresh rails application.

rails new depot && cd depot

Now with our rails application ready, let’s create a tenant model to control what tenants are in our application.

rails g model tenant name:string host:string

First we need to add two class methods to the `Tenant` model to support storing the current tenant for each request. Here we use the current thread’s local storage.

#####[_app/models/tenant.rb_][app/models/tenant.rb]#####

class Tenant < ActiveRecord::Base class << self def current Thread.current[:current_tenant] end def current=(tenant) Thread.current[:current_tenant] = tenant end end end And while we're at it, we should add an index to the `host` column otherwise performance will suffer significantly. #####[_db/migrate/XXX_create_tenants.rb_][db/migrate/20101210044554_create_tenants.rb] ##### class CreateTenants < ActiveRecord::Migration def self.up create_table :tenants add_index :tenants, :host end end Next we create our database and migrate rake db:create db:migrate And then we fire up a rails console and add a new tenant. What value you set the host attribute to will vary depending on your setup but you are most likely accessing the rails application using `localhost`. $ rails c ruby > Tenant.create(:name => ‘Me!’, :host => ‘localhost’)
=> #
ruby >

Scoping our Data

To get our models to scope to only the current tenant, we need an abstract model for all the other models to inherit from (you could monkey-patch `ActiveRecord::Base` but I would advise against it because you wouldn’t be able to exclude a model from tenant paritioning easily). Because the [com][b1b26a][mits][e68f33] that enable a lambda `default_scope` haven’t made their way into [3-0-stable][3-0-stable] we have to add them ourselves.

Now to generate our model (`–parent` is used to stop rails from generating a migration, which aren’t useful for abstract models).

rails g model tenant_scoped_model –parent=active_record/base

#####[_app/models/tenant_scoped_model.rb_][app/models/tenant_scoped_model.rb]#####

class TenantScopedModel < ActiveRecord::Base self.abstract_class = true class << self protected def current_scoped_methods last = scoped_methods.last last.respond_to?(:call) ? relation.scoping { last.call } : last end end belongs_to :tenant default_scope lambda { where('tenant_id = ?', Tenant.current) } before_save { self.tenant = Tenant.current } end The last thing we need is a `before_filter` in `ApplicationController` that loads the tenant we want to use for this request. #####[_app/controllers/application_controller.rb_][app/controllers/application_controller.rb]###### class ApplicationController < ActionController::Base before_filter do @tenant = Tenant.current = Tenant.find_by_host!(request.host) end end And now we're ready to go. Sample Model: Products --- rails g scaffold product name:string description:text quantity:integer To scope our `Product` model based on the current tenant all we have to do is change what class it inherits and add the `tenant_id` column to the migration (and an index to keep performance nice and high). #####[_app/models/product.rb_][app/models/product.rb]##### class Products < TenantScopedModel end #####[_db/migrate/XXX_create_products.rb_][db/migrate/20101210055112_create_products.rb]##### class CreateProducts < ActiveRecord::Migration def self.up create_table :products do |t| t.references :tenant end add_index :products, :tenant_id end end And there you go! See it in action --- For demo purposes I have set up 4 domains: [3][buraa] [that][wuxug] [exist][zovco] in the tenant table and [1 that does][nx] not. All code (including a few improvements, most notably is a custom page for missing tenant) is [available in a github repository][github-repo]. Caveats --- * Calling `unscoped` bypasses the `default_scope`; this is both good and bad because you can get all partitioned data from a model regardless of the current tenant. * You will get errors in the console when working with a scoped model unless you set `Tenant.current`. * `before_save { self.tenant = Tenant.current }` is necessary because rails does not automatically set the attributes from our `default_scope` because it is a lambda. _This is the 14th entry for [Ruby Advent Calendar jp-en: 2010][ruby-advent-calendar]. The [previous entry][previous-post] was written by [sakuro][previous-post-author] and the next will be written by [matschaffer][next-post-author]._ [b1b26a]: https://github.com/rails/rails/commit/b1b26af9a2f1c2037f7c2167d747ed33cc639763 [e68f33]: https://github.com/rails/rails/commit/e68f339aae4d3bc1bcf46b65cb8dcddc0ad2a435 [app/models/tenant.rb]: https://github.com/samuelkadolph/simple-rails-multi-tenancy/blob/master/app/models/tenant.rb [db/migrate/20101210044554_create_tenants.rb]: https://github.com/samuelkadolph/simple-rails-multi-tenancy/blob/master/db/migrate/20101210044554_create_tenants.rb [3-0-stable]: https://github.com/rails/rails/tree/3-0-stable [app/models/tenant_scoped_model.rb]: https://github.com/samuelkadolph/simple-rails-multi-tenancy/blob/master/app/models/tenant_scoped_model.rb [app/controllers/application_controller.rb]: https://github.com/samuelkadolph/simple-rails-multi-tenancy/blob/master/app/controllers/application_controller.rb [app/models/product.rb]: https://github.com/samuelkadolph/simple-rails-multi-tenancy/blob/master/app/models/product.rb [db/migrate/20101210055112_create_products.rb]: https://github.com/samuelkadolph/simple-rails-multi-tenancy/blob/master/db/migrate/20101210055112_create_products.rb [buraa]: http://buraa.simple-rails-multi-tenancy.samuel.kadolph.com [wuxug]: http://wuxug.simple-rails-multi-tenancy.samuel.kadolph.com [zovco]: http://zovco.simple-rails-multi-tenancy.samuel.kadolph.com [nx]: http://nx.simple-rails-multi-tenancy.samuel.kadolph.com [github-repo]: https://github.com/samuelkadolph/simple-rails-multi-tenancy/ [ruby-advent-calendar]: http://atnd.org/events/10439 [previous-post]: http://blog.2238club.org/2010/12/ruby-book-hajimete-no-ruby.html [previous-post-author]: http://blog.2238club.org/ [next-post-author]: http://matschaffer.com/

14 Replies to “Simple Rails Multi-Tenancy”

  1. I have tried this approach and seems to working fine. But I have a problem when accessing the model from another controller.

    I have changed the name to AccountScoped instead of TenantScopedModel model.

    Let’s say I want to get the Products so I do on my controller… @products = Products.all
    But this only works fine from the ProductsController … which is really strange.

    It gives me this error from any other controller:

    SQLite3::SQLException: no such table: account_scopeds: SELECT “account_scopeds”.* FROM “account_scopeds” WHERE (account_id = 1)

    Hope you can help me.

    Thanks,
    Leo!

  2. So far I found out that is something with the default_scope, I have moved it to the Products class and it works fine. I will keep reviewing it.

  3. I can’t get this to work in Rails 3.1.0.rc4. The problem seems to be that when the lambda for the default_scope gets evaluated it triggers the computation of the table name in the context of TenantScopedModel instead of in the context of the descendent classes, which results in the wrong table name being memoized for the descendent classes.

    I switched to using the ActiveRecord::Base#default_scope method (i.e. the instance method instead the class method) and everything worked for me.

    The patch to current_scoped_methods is also not needed in 3.1.0.rc4

    1. Correction. Define TenantScopedModel::default_scope (class method):

      def self.default_scope
      where(‘tenant_id = ?’, Tenant.current)
      end

  4. I’m getting the same issue as Leo, when the model (that is tenanted) is access from an other controller, I get a very strange error where Rails is somehow assuming that ‘TenantScopedModel’ is a table in the database …

    SELECT organiser_scoped_models.* FROM organiser_scoped_models WHERE (organiser_id = 15) AND (organiser_scoped_models.event_id = 9)

  5. Hi ,

    do we have to maintain different directories for each tenant in this application ???
    How to manage it with different host name other than localhost in if the application is in Local are network.

    Thanks and Regards,
    Piyush S

  6. What’s the minimum version of rails you need to get this to work?

    It worked beautifully in 3.1, but I tried it in a 3.0.7 app, and I get this: undefined method `includes_values’ for # (NoMethodError) when I try to use a scoped model, for example Model.all.

Leave a Reply

Your email address will not be published. Required fields are marked *