Skip to content
A simple multitenancy with activerecord/mongoid through scoping
Ruby
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
gemfiles
lib bump version Jan 18, 2016
spec
.coveralls.yml
.gitignore
.rspec
.ruby-gemset
.ruby-version
.travis.yml
CHANGELOG.md
Gemfile
Guardfile
LICENSE.txt
README.md
Rakefile
tenancy.gemspec

README.md

Tenancy Gem Version Build Status Dependency Status Coverage Status

Tenancy is a simple gem that provides multi-tenancy support on activerecord/mongoid (3/4) through scoping. I suggest you to watch an excellent RailsCast on Multitenancy with Scopes and read this book Multitenancy with Rails.

This README.md file is for the latest version, v1.0.0. For the previous version, check out this README.md. Please, see the CHANGELOG.md to do an upgrade.

Installation

Add this line to your application's Gemfile:

gem "tenancy"

And then execute:

$ bundle

Usage

This gem provides two modules: Tenancy::Resource and Tenancy::ResourceScope. Include them into your activerecord/mongoid models.

Tenancy::Resource

Tenancy::Resource is a module which you want others to be scoped by.

class Portal < ActiveRecord::Base
  include Tenancy::Resource
end

camyp = Portal.where(domain_name: 'yp.com.kh').first
# => <Portal id: 1, domain_name: 'yp.com.kh'>

# set current portal by id
Portal.current = camyp

# or portal object
Portal.current = 1

# get current portal
Portal.current
# => <Portal id: 1, domain_name: 'yp.com.kh'>

# scope with this portal
Portal.with_tenant(camyp) do
  # Do something here with this portal
end

Tenancy::ResourceScope

Tenancy::ResourceScope is a module which you want to scope itself to Tenancy::Resource.

class Listing < ActiveRecord::Base
  include Tenancy::Resource
  include Tenancy::ResourceScope

  scope_to :portal
  validates_uniqueness_in_scope :name, case_sensitive: false
end

class Communication < ActiveRecord::Base
  include Tenancy::ResourceScope

  scope_to :portal, :listing
  default_scope -> { where(is_active: true) }
  validates_uniqueness_in_scope :value
end

class ExtraCommunication < ActiveRecord::Base
  include Tenancy::ResourceScope

  # options here will send to #belongs_to
  scope_to :portal, class_name: 'Portal'
  scope_to :listing, class_name: 'Listing'
  validates_uniqueness_in_scope :value
end

> Portal.current = 1
> Listing.find(1)
# => SELECT "listings".* FROM "listings" WHERE "portal_id" = 1 AND "id" = 1

> Listing.current = 1
> Communication.find(1)
# => SELECT "communications".* FROM "communications" WHERE "portal_id" = 1 AND "listing_id" = 1 AND "is_active" = true AND "id" = 1

# include/exclude tenant_scope :current_portal, :current_listing
> Communication.tenant_scope(:portal).find(1)
# => SELECT "communications".* FROM "communications" WHERE "portal_id" = 1 AND "is_active" = true AND "id" = 1
> Communication.tenant_scope(:listing).find(1)
# => SELECT "communications".* FROM "communications" WHERE "listing_id" = 1 AND "is_active" = true AND "id" = 1
> Communication.tenant_scope(nil).find(1)
# => SELECT "communications".* FROM "communications" WHERE "is_active" = true AND "id" = 1

scope_to :portal does these things:

  1. it adds belongs_to :portal.

  2. it adds validates :portal, presence: true.

  3. it adds default_scope { where(portal_id: Portal.current) if Portal.current }.

  4. it overrides #portal so that it doesn't touch the database if portal_id in that record is the same as Portal.current_id.

  5. it overrides #portal_id so that it returns Portal.current_id. (mongoid 3 only)

  6. it overrides #shard_key_selector so that every update/delete query includes current tenant_id. (mongoid 3/4)

validates :value, uniqueness: true will validates uniqueness against the whole table. validates_uniqueness_in_scope validates uniqueness with the scopes you passed in scope_to.

Rails

class ApplicationController < ActionController::Base
  before_action :set_current_portal

  protected

    def current_portal
      Portal.current
    end

    def set_current_portal
      Portal.current = Portal.find_by_domain_name(request.host)
    end
end

Indexes

add_index :listings, :portal_id
add_index :communications, [:portal_id, :listing_id]

RSpec

In spec_helper.rb, you'll need to require the matchers:

require "tenancy/matchers"

Example:

describe Portal do
  it { should be_a_tenant }
end
describe Listing do
  it { should have_scope_to(:portal) }
  it { should have_scope_to(:portal).class_name('Portal') }
end
describe Mongo::Listing do
  it { should have_scope_to(:portal) }
  it { should have_scope_to(:portal).of_type(Mongo::Portal) }
end

I have this rspec configuration in my rails 4 apps:

RSpec.configure do |config|
  config.before(:suite) do
    DatabaseCleaner[:active_record].strategy = :transaction
    DatabaseCleaner[:mongoid].strategy       = :truncation

    DatabaseCleaner[:active_record].clean_with(:truncation)
    DatabaseCleaner[:mongoid].clean_with(:truncation)
  end

  config.around(:each) do |example|
    DatabaseCleaner[:active_record].start
    DatabaseCleaner[:mongoid].start

    current_portal = FactoryGirl.create(:portal, domain_name: "yellowpages-cambodia.dev")
    Yoolk::Portal.use(current_portal) do
      example.run
    end

    DatabaseCleaner[:active_record].clean
    DatabaseCleaner[:mongoid].clean if example.metadata[:mongodb]
  end
end

Authors

You can’t perform that action at this time.