multitenancy 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +16 -3
- data/Rakefile +9 -1
- data/lib/multitenancy.rb +16 -1
- data/lib/multitenancy/active_record/switch_db.rb +30 -0
- data/lib/multitenancy/version.rb +1 -1
- data/test/database.yml +5 -2
- data/test/{multitenancy.sqlite3 → db1.sqlite3} +0 -0
- data/test/db2.sqlite3 +0 -0
- data/test/test_config.rb +3 -1
- data/test/unit/multitenancy_test.rb +52 -0
- data/test/unit/rest_client_test.rb +17 -14
- data/test/unit/switch_db_test.rb +41 -0
- data/test/unit/tenant_test.rb +4 -0
- metadata +9 -4
data/README.md
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
# Multitenancy
|
2
2
|
|
3
|
-
Multitenancy gem nicely plugs in to activerecord to provide multitenant support within a
|
3
|
+
Multitenancy gem nicely plugs in to activerecord to provide multitenant support within a shared schema as well dedicated schema. It allows multitenancy at two levels, tenant and sub-tenant. For instance you can have SAAS application where the primary tenant could be an organization and sub-tenant will be users in that organization.
|
4
|
+
|
5
|
+
Shared Schema: All tennats will be part of the same instance. Additional columns (tenant_id and sub_tenant_id) will be added to your tables to support multitenancy
|
6
|
+
Dedicated Schema: Every tenant will have its own instance of db
|
4
7
|
|
5
8
|
## Installation
|
6
9
|
|
@@ -17,14 +20,13 @@ Or install it yourself as:
|
|
17
20
|
$ gem install multitenancy
|
18
21
|
|
19
22
|
## Usage
|
20
|
-
|
21
23
|
This gem expects the tenant and sub-tenant values passed in the request header. But you can enhace this to support other logic as well.
|
22
24
|
|
23
25
|
You use it in your padrino/sinatra application, add the below lines to the config.ru
|
24
26
|
|
25
27
|
Multitenancy.init(:tenant_header => 'X_COMPANY_ID', :sub_tenant_header => 'X_USER_ID')
|
26
28
|
Padrino.use Multitenancy::Filter
|
27
|
-
|
29
|
+
|
28
30
|
You can also outside of a filter or in a standalone application,
|
29
31
|
|
30
32
|
tenant = Multitenancy::Tenant.new('flipkart', 'ganeshs')
|
@@ -33,6 +35,17 @@ You can also outside of a filter or in a standalone application,
|
|
33
35
|
end
|
34
36
|
|
35
37
|
Any active record query executed within the tenant block, will be tenant/sub-tenant scoped. New records will persist the tenant and sub-tenant ids, find queries will be scoped to teanant and sub-tenant ids. If the sub-tenant id is not specified, it will be de-scoped.
|
38
|
+
|
39
|
+
### Shared Instance
|
40
|
+
You can pass on the db_type as shared while initializing multitenancy, which is by default set when db_type is not.
|
41
|
+
|
42
|
+
Multitenancy.init(:tenant_header => 'X_COMPANY_ID', :sub_tenant_header => 'X_USER_ID', :db_type => :shared)
|
43
|
+
|
44
|
+
### Dedicated Instance
|
45
|
+
|
46
|
+
Dedicated instance config takes in additional optional params,
|
47
|
+
|
48
|
+
Multitenancy.init(:tenant_header => 'X_COMPANY_ID', :sub_tenant_header => 'X_USER_ID', :db_type => :shared, :db_config_prefix => 'some_prefix_', :db_config_suffix => 'production')
|
36
49
|
|
37
50
|
## Contributing
|
38
51
|
|
data/Rakefile
CHANGED
data/lib/multitenancy.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require "active_record"
|
2
2
|
require "active_model"
|
3
3
|
require "multitenancy/version"
|
4
|
+
require "multitenancy/active_record/switch_db"
|
4
5
|
require "multitenancy/tenant"
|
5
6
|
require "multitenancy/rack/filter"
|
6
7
|
require "multitenancy/model_extensions"
|
@@ -12,6 +13,9 @@ module Multitenancy
|
|
12
13
|
@@sub_tenant_header = 'X_SUB_TENANT_ID'
|
13
14
|
@@append_headers_to_rest_calls = true
|
14
15
|
@@logger = (logger rescue nil) || Logger.new(STDOUT)
|
16
|
+
@@db_config_prefix = ''
|
17
|
+
@@db_config_suffix = '_development'
|
18
|
+
@@db_type = :shared # or :dedicated
|
15
19
|
|
16
20
|
class << self
|
17
21
|
def init(config)
|
@@ -19,6 +23,13 @@ module Multitenancy
|
|
19
23
|
@@sub_tenant_header = config[:sub_tenant_header]
|
20
24
|
@@logger = config[:logger] if config[:logger]
|
21
25
|
@@append_headers_to_rest_calls = config[:append_headers_to_rest_calls] unless config[:append_headers_to_rest_calls].nil?
|
26
|
+
@@db_config_prefix = config[:db_config_prefix] unless config[:db_config_prefix].nil?
|
27
|
+
@@db_config_suffix = config[:db_config_suffix] unless config[:db_config_suffix].nil?
|
28
|
+
@@db_type = (config[:db_type].nil? || ![:shared, :dedicated].include?(config[:db_type])) ? :shared : config[:db_type]
|
29
|
+
end
|
30
|
+
|
31
|
+
def db_type
|
32
|
+
@@db_type
|
22
33
|
end
|
23
34
|
|
24
35
|
def logger
|
@@ -45,7 +56,11 @@ module Multitenancy
|
|
45
56
|
old_tenant = self.current_tenant
|
46
57
|
self.current_tenant = tenant
|
47
58
|
begin
|
48
|
-
|
59
|
+
if db_type == :shared
|
60
|
+
return block.call
|
61
|
+
else
|
62
|
+
return ActiveRecord::Base.switch_db("#{@@db_config_prefix}#{tenant.tenant_id}#{@@db_config_suffix}".to_sym, &block)
|
63
|
+
end
|
49
64
|
ensure
|
50
65
|
self.current_tenant = old_tenant
|
51
66
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class ActiveRecord::Base
|
2
|
+
class << self
|
3
|
+
|
4
|
+
@@connection_handlers ||= {}
|
5
|
+
|
6
|
+
def connection_handler_with_multi_db_support(spec_symbol = nil)
|
7
|
+
return @@connection_handlers[spec_symbol] if spec_symbol
|
8
|
+
if Thread.current[:current_db]
|
9
|
+
@@connection_handlers[Thread.current[:current_db]] ||= ActiveRecord::ConnectionAdapters::ConnectionHandler.new
|
10
|
+
else
|
11
|
+
connection_handler_without_multi_db_support
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
alias_method :connection_handler_without_multi_db_support, :connection_handler
|
16
|
+
alias_method :connection_handler, :connection_handler_with_multi_db_support
|
17
|
+
|
18
|
+
def switch_db(db, &block)
|
19
|
+
Thread.current[:current_db] = db
|
20
|
+
unless ActiveRecord::Base.connection_handler.retrieve_connection_pool(ActiveRecord::Base)
|
21
|
+
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[db])
|
22
|
+
end
|
23
|
+
yield
|
24
|
+
ensure
|
25
|
+
ActiveRecord::Base.connection_handler.clear_active_connections! rescue puts "supressing error while clearing connections - #{$!.inspect}"
|
26
|
+
Thread.current[:current_db] = nil
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
data/lib/multitenancy/version.rb
CHANGED
data/test/database.yml
CHANGED
Binary file
|
data/test/db2.sqlite3
ADDED
Binary file
|
data/test/test_config.rb
CHANGED
@@ -15,7 +15,9 @@ $:.unshift(ROOT + "/lib")
|
|
15
15
|
|
16
16
|
config = YAML::load(IO.read(File.join(File.dirname(__FILE__), 'database.yml')))
|
17
17
|
ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), "debug.log"))
|
18
|
-
ActiveRecord::Base.
|
18
|
+
ActiveRecord::Base.configurations[:db1] = config[ENV['DB1'] || 'db1']
|
19
|
+
ActiveRecord::Base.configurations[:db2] = config[ENV['DB2'] || 'db2']
|
20
|
+
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[:db1])
|
19
21
|
|
20
22
|
class ActiveSupport::TestCase
|
21
23
|
|
@@ -8,6 +8,23 @@ module Multitenancy
|
|
8
8
|
assert_equal 'tenant_id', Multitenancy.tenant_header
|
9
9
|
end
|
10
10
|
|
11
|
+
context "configure db type" do
|
12
|
+
should "default to shared" do
|
13
|
+
Multitenancy.init(:tenant_header => 'tenant_id', :sub_tenant_header => 'seller_id')
|
14
|
+
assert_equal :shared, Multitenancy.db_type
|
15
|
+
end
|
16
|
+
|
17
|
+
should "set to shared on junk value" do
|
18
|
+
Multitenancy.init(:tenant_header => 'tenant_id', :sub_tenant_header => 'seller_id', :db_type => :junk)
|
19
|
+
assert_equal :shared, Multitenancy.db_type
|
20
|
+
end
|
21
|
+
|
22
|
+
should "set to dedicated" do
|
23
|
+
Multitenancy.init(:tenant_header => 'tenant_id', :sub_tenant_header => 'seller_id', :db_type => :dedicated)
|
24
|
+
assert_equal :dedicated, Multitenancy.db_type
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
11
28
|
should "return current tenant" do
|
12
29
|
tenant = Tenant.new('tenant_id', 'seller_id')
|
13
30
|
Multitenancy.current_tenant = tenant
|
@@ -16,6 +33,7 @@ module Multitenancy
|
|
16
33
|
|
17
34
|
context "block within context" do
|
18
35
|
setup do
|
36
|
+
Multitenancy.init(:tenant_header => 'tenant_id', :sub_tenant_header => 'seller_id')
|
19
37
|
tenant = Tenant.new('tenant_id', 'seller_id')
|
20
38
|
Multitenancy.current_tenant = tenant
|
21
39
|
end
|
@@ -26,6 +44,40 @@ module Multitenancy
|
|
26
44
|
assert_equal Multitenancy.current_tenant.sub_tenant_id, 'dummy_seller_id'
|
27
45
|
end
|
28
46
|
end
|
47
|
+
|
48
|
+
context "for dedicated db" do
|
49
|
+
setup do
|
50
|
+
Multitenancy.init(:tenant_header => 'tenant_id', :sub_tenant_header => 'seller_id', :db_type => :dedicated)
|
51
|
+
end
|
52
|
+
|
53
|
+
should "switch db" do
|
54
|
+
ActiveRecord::Base.expects(:switch_db).with(:db1_development)
|
55
|
+
Multitenancy.with_tenant(Tenant.new('db1')) do
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
should "call block" do
|
60
|
+
test = false
|
61
|
+
ActiveRecord::Base.stubs(:establish_connection)
|
62
|
+
Multitenancy.with_tenant(Tenant.new('db1')) do
|
63
|
+
test = true
|
64
|
+
end
|
65
|
+
assert_true test
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context "for dedicated type prefix and suffix" do
|
72
|
+
setup do
|
73
|
+
Multitenancy.init(:tenant_header => 'tenant_id', :sub_tenant_header => 'seller_id', :db_type => :dedicated, :db_config_prefix => 'db_', :db_config_suffix => '_suffix')
|
74
|
+
end
|
75
|
+
|
76
|
+
should "be used to switch db" do
|
77
|
+
ActiveRecord::Base.expects(:switch_db).with(:db_db1_suffix)
|
78
|
+
Multitenancy.with_tenant(Tenant.new('db1')) do
|
79
|
+
end
|
80
|
+
end
|
29
81
|
end
|
30
82
|
end
|
31
83
|
end
|
@@ -2,22 +2,25 @@ require File.expand_path(File.dirname(__FILE__) + '/../test_config.rb')
|
|
2
2
|
|
3
3
|
class RestClientTest < Test::Unit::TestCase
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
5
|
+
context "tenant headers in rest calls" do
|
6
|
+
setup do
|
7
|
+
Multitenancy.init({:tenant_header => 'X_COMPANY_ID', :sub_tenant_header => 'X_USER_ID', :append_headers_to_rest_calls => true})
|
8
|
+
end
|
9
|
+
|
10
|
+
should "not append tenant headers to rest calls outside of a tenant scope" do
|
11
|
+
Multitenancy.current_tenant = nil
|
12
|
+
RestClient::Request.any_instance.expects(:make_headers).with() {|headers| headers[Multitenancy.tenant_header].nil? && headers[Multitenancy.sub_tenant_header].nil?}
|
13
|
+
RestClient::Request.any_instance.expects(:execute)
|
13
14
|
RestClient.get('http://some/resource')
|
14
15
|
end
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
16
|
+
|
17
|
+
should "append tenant headers to rest calls within a tenant scope" do
|
18
|
+
RestClient::Request.any_instance.expects(:make_headers).with() {|headers| headers[Multitenancy.tenant_header] == 'Flipkart' && headers[Multitenancy.sub_tenant_header] == 'ganeshs'}
|
19
|
+
RestClient::Request.any_instance.expects(:execute)
|
20
|
+
Multitenancy.with_tenant(Multitenancy::Tenant.new('Flipkart', 'ganeshs')) do
|
21
|
+
RestClient.get('http://some/resource')
|
22
|
+
end
|
23
|
+
end
|
21
24
|
end
|
22
25
|
|
23
26
|
context "no tenant headers in rest calls" do
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../test_config.rb')
|
2
|
+
|
3
|
+
2.times do |i|
|
4
|
+
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations["db#{i + 1}".to_sym])
|
5
|
+
ActiveRecord::Schema.define(:version => 1) do
|
6
|
+
create_table :dummy, :force => true do |t|
|
7
|
+
t.column :org_id, :string
|
8
|
+
t.column :name, :string
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Dummy < ActiveRecord::Base
|
14
|
+
end
|
15
|
+
|
16
|
+
class SwitchDBTest < Test::Unit::TestCase
|
17
|
+
|
18
|
+
should "set current db name on switch db" do
|
19
|
+
Dummy.switch_db(:db1) do
|
20
|
+
assert_equal(:db1, Thread.current[:current_db])
|
21
|
+
end
|
22
|
+
Dummy.switch_db(:db2) do
|
23
|
+
assert_equal(:db2, Thread.current[:current_db])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
should "reset current db after switch db is completed" do
|
28
|
+
Dummy.switch_db(:db1) do
|
29
|
+
assert_equal(:db1, Thread.current[:current_db])
|
30
|
+
end
|
31
|
+
assert_nil(Thread.current[:current_db])
|
32
|
+
end
|
33
|
+
|
34
|
+
should "clear active connections after switch db block execution" do
|
35
|
+
ActiveRecord::ConnectionAdapters::ConnectionHandler.any_instance.expects(:clear_active_connections!)
|
36
|
+
Dummy.switch_db(:db1) do
|
37
|
+
assert_equal(:db1, Thread.current[:current_db])
|
38
|
+
end
|
39
|
+
assert_nil(Thread.current[:current_db])
|
40
|
+
end
|
41
|
+
end
|
data/test/unit/tenant_test.rb
CHANGED
@@ -2,6 +2,10 @@ require File.expand_path(File.dirname(__FILE__) + '/../test_config.rb')
|
|
2
2
|
|
3
3
|
module Multitenancy
|
4
4
|
class TenantTest < Test::Unit::TestCase
|
5
|
+
|
6
|
+
setup do
|
7
|
+
Multitenancy.init(:tenant_header => 'X_TENANT_ID', :sub_tenant_header => 'X_SUB_TENANT_ID')
|
8
|
+
end
|
5
9
|
|
6
10
|
should "create tenant" do
|
7
11
|
tenant = Tenant.new('tenant_id', 'seller_id')
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: multitenancy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-02-05 00:00:00.000000000Z
|
13
13
|
dependencies: []
|
14
14
|
description: Support multitennacy with active record
|
15
15
|
email:
|
@@ -24,6 +24,7 @@ files:
|
|
24
24
|
- README.md
|
25
25
|
- Rakefile
|
26
26
|
- lib/multitenancy.rb
|
27
|
+
- lib/multitenancy/active_record/switch_db.rb
|
27
28
|
- lib/multitenancy/model_extensions.rb
|
28
29
|
- lib/multitenancy/rack/filter.rb
|
29
30
|
- lib/multitenancy/rest_client/rest_client.rb
|
@@ -31,12 +32,14 @@ files:
|
|
31
32
|
- lib/multitenancy/version.rb
|
32
33
|
- multitenancy.gemspec
|
33
34
|
- test/database.yml
|
34
|
-
- test/
|
35
|
+
- test/db1.sqlite3
|
36
|
+
- test/db2.sqlite3
|
35
37
|
- test/test_config.rb
|
36
38
|
- test/unit/filter_test.rb
|
37
39
|
- test/unit/model_extensions_test.rb
|
38
40
|
- test/unit/multitenancy_test.rb
|
39
41
|
- test/unit/rest_client_test.rb
|
42
|
+
- test/unit/switch_db_test.rb
|
40
43
|
- test/unit/tenant_test.rb
|
41
44
|
homepage: https://github.com/Flipkart/multitenancy
|
42
45
|
licenses: []
|
@@ -64,11 +67,13 @@ specification_version: 3
|
|
64
67
|
summary: Support multitennacy with active record at tenant and sub-tenant level
|
65
68
|
test_files:
|
66
69
|
- test/database.yml
|
67
|
-
- test/
|
70
|
+
- test/db1.sqlite3
|
71
|
+
- test/db2.sqlite3
|
68
72
|
- test/test_config.rb
|
69
73
|
- test/unit/filter_test.rb
|
70
74
|
- test/unit/model_extensions_test.rb
|
71
75
|
- test/unit/multitenancy_test.rb
|
72
76
|
- test/unit/rest_client_test.rb
|
77
|
+
- test/unit/switch_db_test.rb
|
73
78
|
- test/unit/tenant_test.rb
|
74
79
|
has_rdoc:
|