multitenant-mysql 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.rdoc +5 -2
- data/lib/generators/multitenant/install/install_generator.rb +1 -1
- data/lib/generators/multitenant/install/templates/multitenant_mysql_conf.rb +2 -2
- data/lib/generators/multitenant/migrations/migration_builder.rb +2 -2
- data/lib/multitenant-mysql.rb +2 -54
- data/lib/multitenant-mysql/action_controller_extension.rb +11 -0
- data/lib/multitenant-mysql/active_record_extension.rb +31 -0
- data/lib/multitenant-mysql/arc.rb +52 -0
- data/lib/multitenant-mysql/connection_switcher.rb +17 -14
- data/lib/multitenant-mysql/version.rb +1 -1
- data/rails/init.rb +1 -34
- data/spec/action_controller_extension_spec.rb +17 -1
- data/spec/{multitenant_mysql_spec.rb → arc_spec.rb} +12 -0
- data/spec/connection_switcher_spec.rb +14 -2
- data/spec/rails/active_record_base_spec.rb +1 -0
- metadata +6 -5
data/README.rdoc
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
== Multitenant::Mysql
|
2
2
|
|
3
|
-
Web app often
|
3
|
+
Web app often faces multitenancy problem and there are already few gems that help to solve this issue but this one uses different approach.
|
4
4
|
Instead of default scopes or creating a separate database for every single tenant this gem uses mysql views and triggers.
|
5
5
|
The idea is taken from http://blog.empowercampaigns.com/post/1044240481/multi-tenant-data-and-mysql
|
6
6
|
|
@@ -12,6 +12,7 @@ The advantages of such approach:
|
|
12
12
|
|
13
13
|
== Code Status
|
14
14
|
|
15
|
+
* {<img src="https://fury-badge.herokuapp.com/rb/multitenant-mysql.png" alt="Gem Version" />}[http://badge.fury.io/rb/multitenant-mysql]
|
15
16
|
* {<img src="https://travis-ci.org/eugenekorpan/multitenant-mysql.png?branch=master"/>}[http://travis-ci.org/eugenekorpan/multitenant-mysql]
|
16
17
|
* {<img src="https://gemnasium.com/eugenekorpan/multitenant-mysql.png" alt="Dependency Status" />}[https://gemnasium.com/eugenekorpan/multitenant-mysql]
|
17
18
|
* {<img src="https://codeclimate.com/github/eugenekorpan/multitenant-mysql.png" />}[https://codeclimate.com/github/eugenekorpan/multitenant-mysql]
|
@@ -70,7 +71,9 @@ E.g.
|
|
70
71
|
|
71
72
|
if method used by `set_current_tenant` returns blank name then `root` account is used
|
72
73
|
|
73
|
-
|
74
|
+
Note: if you want to use subdomain as a tenant name then you can use `set_current_tenant_by_subdomain` method. Just add it into your application_controller. In this case `set_current_tenant` is not needed.
|
75
|
+
|
76
|
+
== How It Works
|
74
77
|
|
75
78
|
About the main principles you can read here http://blog.empowercampaigns.com/post/1044240481/multi-tenant-data-and-mysql.
|
76
79
|
|
@@ -7,7 +7,7 @@ module Multitenant
|
|
7
7
|
CONFIG_FILE_NAME = 'multitenant_mysql_conf.rb'
|
8
8
|
|
9
9
|
def copy_conf_file_into_app
|
10
|
-
dest =
|
10
|
+
dest = "config/#{CONFIG_FILE_NAME}"
|
11
11
|
return if FileTest.exist?(dest) # to prevent overwritting of existing file
|
12
12
|
src = File.expand_path(File.dirname(__FILE__)) + "/templates/#{CONFIG_FILE_NAME}"
|
13
13
|
FileUtils.copy_file src, dest
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# simple example
|
2
2
|
#
|
3
|
-
# Multitenant::Mysql.
|
3
|
+
# Multitenant::Mysql.arc = {
|
4
4
|
# models: ['Book', 'Task'],
|
5
5
|
# tenant_model: { name: 'Subdomain', tenant_name_attr: name }
|
6
6
|
# }
|
@@ -11,7 +11,7 @@
|
|
11
11
|
# name - model name
|
12
12
|
# tenant_name_attr - attribute used to fetch tenant name
|
13
13
|
|
14
|
-
Multitenant::Mysql.
|
14
|
+
Multitenant::Mysql.arc = {
|
15
15
|
models: [],
|
16
16
|
tenant_model: { name: '' }
|
17
17
|
}
|
@@ -13,7 +13,7 @@ module Multitenant
|
|
13
13
|
"add_column :#{model.table_name}, :tenant, :string"
|
14
14
|
}
|
15
15
|
|
16
|
-
dest_path =
|
16
|
+
dest_path = "db/migrate/#{migration_number}_#{MIGRATION_NAME}.rb"
|
17
17
|
migration = File.new(dest_path, "w")
|
18
18
|
migration.puts(migration_code(actions))
|
19
19
|
migration.close
|
@@ -35,7 +35,7 @@ end)
|
|
35
35
|
end
|
36
36
|
|
37
37
|
def migration_exists?
|
38
|
-
migrations = Dir.entries(
|
38
|
+
migrations = Dir.entries("db/migrate")
|
39
39
|
migrations.any? { |m| m.include?(MIGRATION_NAME) }
|
40
40
|
end
|
41
41
|
end
|
data/lib/multitenant-mysql.rb
CHANGED
@@ -1,58 +1,6 @@
|
|
1
1
|
require 'multitenant-mysql/version'
|
2
|
-
require 'multitenant-mysql/active_record_extension'
|
3
2
|
require 'multitenant-mysql/action_controller_extension'
|
4
3
|
require 'multitenant-mysql/conf_file'
|
4
|
+
require 'multitenant-mysql/arc'
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
module Multitenant
|
9
|
-
module Mysql
|
10
|
-
|
11
|
-
class << self
|
12
|
-
|
13
|
-
DEFAULT_TENANT_NAME_ATTR = [:name, :title]
|
14
|
-
|
15
|
-
def active_record_configs=(configs)
|
16
|
-
@configs = configs
|
17
|
-
end
|
18
|
-
|
19
|
-
def active_record_configs
|
20
|
-
@configs
|
21
|
-
end
|
22
|
-
|
23
|
-
alias_method :arc, :active_record_configs
|
24
|
-
|
25
|
-
def tenant
|
26
|
-
arc[:tenant_model][:name].constantize
|
27
|
-
rescue
|
28
|
-
if arc.blank? || arc[:tenant_model].blank? || arc[:tenant_model][:name].blank?
|
29
|
-
raise "
|
30
|
-
Multitenant::Mysql: You should specify model which stores info about tenants.
|
31
|
-
E.g. in initializer:
|
32
|
-
Multitenant::Mysql.arc = {
|
33
|
-
tenant_model: { name: 'Subdomain' }
|
34
|
-
}
|
35
|
-
"
|
36
|
-
else
|
37
|
-
raise "Please check your multitenant-mysql configs. Seems like you are trying to use model which doesn't exist: #{arc[:tenant_model][:name]}"
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
def models
|
42
|
-
active_record_configs[:models]
|
43
|
-
end
|
44
|
-
|
45
|
-
def tenant_name_attr
|
46
|
-
return active_record_configs[:tenant_model][:tenant_name_attr] if active_record_configs[:tenant_model][:tenant_name_attr]
|
47
|
-
|
48
|
-
DEFAULT_TENANT_NAME_ATTR.each do |name|
|
49
|
-
return name if tenant.column_names.include?(name.to_s)
|
50
|
-
end
|
51
|
-
|
52
|
-
raise 'You should specify tenant name attribute or use one default: name, title'
|
53
|
-
end
|
54
|
-
|
55
|
-
end
|
56
|
-
|
57
|
-
end
|
58
|
-
end
|
6
|
+
require 'multitenant-mysql/active_record_extension'
|
@@ -14,4 +14,15 @@ class ActionController::Base
|
|
14
14
|
Multitenant::Mysql::ConnectionSwitcher.new(self, @@tenant_method).execute
|
15
15
|
end
|
16
16
|
end
|
17
|
+
|
18
|
+
def self.set_current_tenant_by_subdomain
|
19
|
+
require Multitenant::Mysql::ConfFile.path
|
20
|
+
|
21
|
+
before_filter :establish_tenant_connection_by_subdomain
|
22
|
+
|
23
|
+
def establish_tenant_connection_by_subdomain
|
24
|
+
tenant_name = request.subdomain
|
25
|
+
Multitenant::Mysql::ConnectionSwitcher.set_tenant(tenant_name)
|
26
|
+
end
|
27
|
+
end
|
17
28
|
end
|
@@ -7,4 +7,35 @@ class ActiveRecord::Base
|
|
7
7
|
ActiveRecord::Base.connection.execute "flush privileges;"
|
8
8
|
end
|
9
9
|
end
|
10
|
+
|
11
|
+
def self.inherited(child)
|
12
|
+
return unless FileTest.exist?(Multitenant::Mysql::ConfFile.full_path) # do nothing if no config file provided
|
13
|
+
require Multitenant::Mysql::ConfFile.path
|
14
|
+
|
15
|
+
model_name = child.to_s
|
16
|
+
if Multitenant::Mysql.models.include? model_name
|
17
|
+
view_name = model_name.to_s.downcase.pluralize + "_view"
|
18
|
+
|
19
|
+
# check whether the view exists in db
|
20
|
+
if ActiveRecord::Base.connection.table_exists? view_name
|
21
|
+
child.class_eval do
|
22
|
+
cattr_accessor :original_table_name
|
23
|
+
|
24
|
+
self.original_table_name = self.table_name
|
25
|
+
self.table_name = view_name
|
26
|
+
self.primary_key = :id
|
27
|
+
|
28
|
+
def self.new(*args)
|
29
|
+
object = super(*args)
|
30
|
+
object.id = nil # workaround for https://github.com/rails/rails/issues/5982
|
31
|
+
object
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
if Multitenant::Mysql.tenant == child
|
38
|
+
child.send :acts_as_tenant
|
39
|
+
end
|
40
|
+
end
|
10
41
|
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Multitenant
|
2
|
+
module Mysql
|
3
|
+
|
4
|
+
class << self
|
5
|
+
|
6
|
+
DEFAULT_TENANT_NAME_ATTR = [:name, :title]
|
7
|
+
|
8
|
+
def active_record_configs=(configs)
|
9
|
+
@configs = configs
|
10
|
+
end
|
11
|
+
|
12
|
+
def active_record_configs
|
13
|
+
@configs
|
14
|
+
end
|
15
|
+
|
16
|
+
alias_method :arc, :active_record_configs
|
17
|
+
alias_method :arc=, :active_record_configs=
|
18
|
+
|
19
|
+
def tenant
|
20
|
+
arc[:tenant_model][:name].constantize
|
21
|
+
rescue
|
22
|
+
if arc.blank? || arc[:tenant_model].blank? || arc[:tenant_model][:name].blank?
|
23
|
+
raise "
|
24
|
+
Multitenant::Mysql: You should specify model which stores info about tenants.
|
25
|
+
E.g. in initializer:
|
26
|
+
Multitenant::Mysql.arc = {
|
27
|
+
tenant_model: { name: 'Subdomain' }
|
28
|
+
}
|
29
|
+
"
|
30
|
+
else
|
31
|
+
raise "Please check your multitenant-mysql configs. Seems like you are trying to use model which doesn't exist: #{arc[:tenant_model][:name]}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def models
|
36
|
+
active_record_configs[:models]
|
37
|
+
end
|
38
|
+
|
39
|
+
def tenant_name_attr
|
40
|
+
return active_record_configs[:tenant_model][:tenant_name_attr] if active_record_configs[:tenant_model][:tenant_name_attr]
|
41
|
+
|
42
|
+
DEFAULT_TENANT_NAME_ATTR.each do |name|
|
43
|
+
return name if tenant.column_names.include?(name.to_s)
|
44
|
+
end
|
45
|
+
|
46
|
+
raise 'You should specify tenant name attribute or use one default: name, title'
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -2,6 +2,21 @@ module Multitenant
|
|
2
2
|
module Mysql
|
3
3
|
class NoTenantRegistratedError < StandardError; end;
|
4
4
|
|
5
|
+
class DB
|
6
|
+
class << self
|
7
|
+
def configs
|
8
|
+
Rails.configuration.database_configuration[Rails.env]
|
9
|
+
end
|
10
|
+
|
11
|
+
def establish_connection_for tenant_name
|
12
|
+
config = configs
|
13
|
+
config['username'] = tenant_name.blank? ? 'root' : tenant_name
|
14
|
+
ActiveRecord::Base.establish_connection(config)
|
15
|
+
true
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
5
20
|
class Tenant
|
6
21
|
def self.exists? tenant_name
|
7
22
|
return true if tenant_name.blank?
|
@@ -22,25 +37,13 @@ module Multitenant
|
|
22
37
|
|
23
38
|
def self.set_tenant(tenant_name)
|
24
39
|
return unless Tenant.exists?(tenant_name)
|
25
|
-
|
26
|
-
config = Rails.configuration.database_configuration[Rails.env]
|
27
|
-
config['username'] = tenant_name.blank? ? 'root' : tenant_name
|
28
|
-
ActiveRecord::Base.establish_connection(config)
|
40
|
+
DB.establish_connection_for tenant_name
|
29
41
|
end
|
30
42
|
|
31
43
|
def execute
|
32
|
-
config = db_config
|
33
|
-
|
34
44
|
tenant_name = action_controller.send(method)
|
35
45
|
return unless Tenant.exists?(tenant_name)
|
36
|
-
|
37
|
-
config['username'] = tenant_name.blank? ? 'root' : tenant_name
|
38
|
-
ActiveRecord::Base.establish_connection(config)
|
39
|
-
true
|
40
|
-
end
|
41
|
-
|
42
|
-
def db_config
|
43
|
-
Rails.configuration.database_configuration[Rails.env]
|
46
|
+
DB.establish_connection_for tenant_name
|
44
47
|
end
|
45
48
|
end
|
46
49
|
|
data/rails/init.rb
CHANGED
@@ -1,34 +1 @@
|
|
1
|
-
|
2
|
-
def self.inherited(child)
|
3
|
-
|
4
|
-
return unless FileTest.exist?(Multitenant::Mysql::ConfFile.full_path) # do nothing if no config file provided
|
5
|
-
|
6
|
-
require Multitenant::Mysql::ConfFile.path
|
7
|
-
|
8
|
-
model_name = child.to_s
|
9
|
-
if Multitenant::Mysql.models.include? model_name
|
10
|
-
view_name = model_name.to_s.downcase.pluralize + "_view"
|
11
|
-
|
12
|
-
# check whether the view exists in db
|
13
|
-
if ActiveRecord::Base.connection.table_exists? view_name
|
14
|
-
child.class_eval do
|
15
|
-
cattr_accessor :original_table_name
|
16
|
-
|
17
|
-
self.original_table_name = self.table_name
|
18
|
-
self.table_name = view_name
|
19
|
-
self.primary_key = :id
|
20
|
-
|
21
|
-
def self.new(*args)
|
22
|
-
object = super(*args)
|
23
|
-
object.id = nil # workaround for https://github.com/rails/rails/issues/5982
|
24
|
-
object
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
if Multitenant::Mysql.tenant == child
|
31
|
-
child.send :acts_as_tenant
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
1
|
+
require 'multitenant-mysql'
|
@@ -1,9 +1,10 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe ActionController::Base do
|
4
|
-
subject { ActionController::Base }
|
5
4
|
|
6
5
|
context '.set_current_tenant' do
|
6
|
+
subject { ActionController::Base }
|
7
|
+
|
7
8
|
it 'should raise an error when no tenant method provided' do
|
8
9
|
expect { subject.set_current_tenant }.to raise_error
|
9
10
|
end
|
@@ -17,4 +18,19 @@ describe ActionController::Base do
|
|
17
18
|
expect( subject.new.establish_tenant_connection ).to eql('ok')
|
18
19
|
end
|
19
20
|
end
|
21
|
+
|
22
|
+
context '.set_current_tenant_by_subdomain' do
|
23
|
+
subject { ActionController::Base.new }
|
24
|
+
|
25
|
+
before do
|
26
|
+
ActionController::Base.set_current_tenant_by_subdomain
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should set current tenant by subdomain' do
|
30
|
+
subject.stub_chain(:request, :subdomain).and_return('yahoo')
|
31
|
+
Multitenant::Mysql::ConnectionSwitcher.should_receive(:set_tenant).with('yahoo').and_return(true)
|
32
|
+
|
33
|
+
expect( subject.establish_tenant_connection_by_subdomain ).to be
|
34
|
+
end
|
35
|
+
end
|
20
36
|
end
|
@@ -63,5 +63,17 @@ describe Multitenant::Mysql do
|
|
63
63
|
end
|
64
64
|
end
|
65
65
|
|
66
|
+
context 'alias methods' do
|
67
|
+
it 'should use aliased setter' do
|
68
|
+
subject.arc = 'ARC'
|
69
|
+
expect(subject.active_record_configs).to eql('ARC')
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'should use aliased getter' do
|
73
|
+
subject.active_record_configs = 'ABC'
|
74
|
+
expect(subject.arc).to eql('ABC')
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
66
78
|
end
|
67
79
|
end
|
@@ -14,7 +14,7 @@ describe Multitenant::Mysql::ConnectionSwitcher do
|
|
14
14
|
ac_mock = double('ActionController::Base')
|
15
15
|
ac_mock.should_receive(:send).and_return('unexisting tenant')
|
16
16
|
switcher = Multitenant::Mysql::ConnectionSwitcher.new(ac_mock, :tenant_method)
|
17
|
-
|
17
|
+
Multitenant::Mysql::DB.stub(:configs).and_return({ 'username' => 'root' })
|
18
18
|
|
19
19
|
expect { switcher.execute }.to raise_error(Multitenant::Mysql::NoTenantRegistratedError)
|
20
20
|
end
|
@@ -27,11 +27,23 @@ describe Multitenant::Mysql::ConnectionSwitcher do
|
|
27
27
|
ac_mock = double('ActionController::Base')
|
28
28
|
ac_mock.should_receive(:tenant_method)
|
29
29
|
switcher = Multitenant::Mysql::ConnectionSwitcher.new(ac_mock, :tenant_method)
|
30
|
-
|
30
|
+
Multitenant::Mysql::DB.stub(:configs).and_return({ 'username' => 'root' })
|
31
31
|
|
32
32
|
ActiveRecord::Base.should_receive(:establish_connection)
|
33
33
|
|
34
34
|
expect( switcher.execute ).to be
|
35
35
|
end
|
36
36
|
end
|
37
|
+
|
38
|
+
context '.set_tenant' do
|
39
|
+
subject { Multitenant::Mysql::ConnectionSwitcher }
|
40
|
+
|
41
|
+
it 'should change db connection' do
|
42
|
+
Multitenant::Mysql::Tenant.stub(:exists?).and_return(true)
|
43
|
+
Multitenant::Mysql::DB.stub(:configs).and_return({ 'username' => 'root' })
|
44
|
+
|
45
|
+
ActiveRecord::Base.should_receive(:establish_connection).with({ 'username' => 'google'})
|
46
|
+
expect( subject.set_tenant('google') ).to be
|
47
|
+
end
|
48
|
+
end
|
37
49
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: multitenant-mysql
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.2
|
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: 2013-
|
12
|
+
date: 2013-03-06 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|
@@ -51,15 +51,16 @@ files:
|
|
51
51
|
- lib/multitenant-mysql.rb
|
52
52
|
- lib/multitenant-mysql/action_controller_extension.rb
|
53
53
|
- lib/multitenant-mysql/active_record_extension.rb
|
54
|
+
- lib/multitenant-mysql/arc.rb
|
54
55
|
- lib/multitenant-mysql/conf_file.rb
|
55
56
|
- lib/multitenant-mysql/connection_switcher.rb
|
56
57
|
- lib/multitenant-mysql/version.rb
|
57
58
|
- multitenant-mysql.gemspec
|
58
59
|
- rails/init.rb
|
59
60
|
- spec/action_controller_extension_spec.rb
|
61
|
+
- spec/arc_spec.rb
|
60
62
|
- spec/conf_file_spec.rb
|
61
63
|
- spec/connection_switcher_spec.rb
|
62
|
-
- spec/multitenant_mysql_spec.rb
|
63
64
|
- spec/rails/active_record_base_spec.rb
|
64
65
|
- spec/spec_helper.rb
|
65
66
|
- spec/support/multitenant_mysql_conf.rb
|
@@ -83,15 +84,15 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
83
84
|
version: '0'
|
84
85
|
requirements: []
|
85
86
|
rubyforge_project:
|
86
|
-
rubygems_version: 1.8.
|
87
|
+
rubygems_version: 1.8.21
|
87
88
|
signing_key:
|
88
89
|
specification_version: 3
|
89
90
|
summary: Add multi-tenancy to Rails application using MySql views
|
90
91
|
test_files:
|
91
92
|
- spec/action_controller_extension_spec.rb
|
93
|
+
- spec/arc_spec.rb
|
92
94
|
- spec/conf_file_spec.rb
|
93
95
|
- spec/connection_switcher_spec.rb
|
94
|
-
- spec/multitenant_mysql_spec.rb
|
95
96
|
- spec/rails/active_record_base_spec.rb
|
96
97
|
- spec/spec_helper.rb
|
97
98
|
- spec/support/multitenant_mysql_conf.rb
|