multitenant-mysql 1.0.1 → 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|