Simple Rails Multi-Tenancy II

I suppose it's about time I updated my Simple Rails Multi-Tenancy post to use the latest rails 3.1.1. Not much has changed from the 3.0 beta to 3.1.1 in terms of the method I use to achieve multi-tenancy but the code has become a bit cleaner (but the patch required is a bit larger). This time around I'll skip the commands and just go straight to the code itself.

app/models/tenant.rb
class Tenant < ActiveRecord::Base
  class << self
    def current
      Thread.current[:tenant]
    end

    def current=(tenant)
      Thread.current[:tenant] = tenant
    end
  end

  validates :name, :host, presence: true

  def with
    previous, Tenant.current = Tenant.current, self
    yield
  ensure
    Tenant.current = previous
  end
end
app/models/tenant_scoped_model.rb
class TenantScopedModel < ActiveRecord::Base
  class << self
    def build_default_scope
      if method(:default_scope).owner != ActiveRecord::Base.singleton_class
        evaluate_default_scope { default_scope }
      elsif default_scopes.any?
        evaluate_default_scope do
          default_scopes.inject(relation) do |default_scope, scope|
            if scope.is_a?(Hash)
              default_scope.apply_finder_options(scope)
            elsif !scope.is_a?(ActiveRecord::Relation) && scope.respond_to?(:call)
              if scope.respond_to?(:arity) && scope.arity == 1
                scope = scope.call(self)
              else
                scope = scope.call
              end
              default_scope.merge(scope)
            else
              default_scope.merge(scope)
            end
          end
        end
      end
    end
  end

  self.abstract_class = true

  belongs_to :tenant

  default_scope do |model|
    model.where(:tenant_id => Tenant.current)
  end
end
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery

  around_filter :with_tenant

  protected
    attr_reader :current_tenant

    def with_tenant
      @current_tenant = Tenant.find_by_host!(request.host)
      @current_tenant.with { yield }
    end
end
app/helpers/application_helper.rb
module ApplicationHelper
  attr_reader :current_tenant
end

All of the migrations and the products model remain the same.

Demo sites are still set up: 3 that exist in the tenant table and 1 that does not. All code is available in a github repository.

Join the conversation

2 Comments

  1. Hi Samuel – trying to implement your strategy on rails 3.2.1 and getting this stack trace when trying to list products:

    activerecord (3.2.1) lib/active_record/relation.rb:500:in with_default_scope'
    activerecord (3.2.1) lib/active_record/relation.rb:166:in
    exec_queries’
    activerecord (3.2.1) lib/active_record/relation.rb:159:in block in to_a'
    activerecord (3.2.1) lib/active_record/explain.rb:40:in
    logging_query_plan’
    activerecord (3.2.1) lib/active_record/relation.rb:158:in to_a'
    activerecord (3.2.1) lib/active_record/relation/finder_methods.rb:159:in
    all’
    activerecord (3.2.1) lib/active_record/querying.rb:5:in all'
    app/controllers/products_controller.rb:5:in
    index’

    You can recreate easily in your example code by upgrading the gem file (I am also using sqlite):
    gem ‘rails’, ‘3.2.1’

    group :assets do
    gem ‘sass-rails’, ‘~> 3.2.3’
    gem ‘coffee-rails’, ‘~> 3.2.1’
    gem ‘uglifier’, ‘>= 1.0.3’
    end

    Do you have any idea how to fix this?
    Many thanks

Leave a comment

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