apartment 0.15.0 → 0.16.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.pryrc +3 -0
- data/.travis.yml +5 -0
- data/HISTORY.md +8 -0
- data/README.md +66 -14
- data/Rakefile +11 -10
- data/apartment.gemspec +6 -5
- data/lib/apartment.rb +21 -3
- data/lib/apartment/adapters/{mysql_adapter.rb → mysql2_adapter.rb} +7 -7
- data/lib/apartment/adapters/postgresql_adapter.rb +26 -15
- data/lib/apartment/database.rb +3 -0
- data/lib/apartment/elevators/domain.rb +18 -0
- data/lib/apartment/elevators/generic.rb +27 -0
- data/lib/apartment/elevators/subdomain.rb +5 -19
- data/lib/apartment/version.rb +1 -1
- data/spec/adapters/{mysql_adapter_spec.rb → mysql2_adapter_spec.rb} +7 -7
- data/spec/adapters/postgresql_adapter_spec.rb +4 -4
- data/spec/config/database.yml +2 -1
- data/spec/database_spec.rb +49 -26
- data/spec/dummy/config/application.rb +3 -0
- data/spec/examples/db_adapter_examples.rb +8 -8
- data/spec/examples/elevator_examples.rb +31 -0
- data/spec/examples/generic_adapter_examples.rb +10 -10
- data/spec/examples/schema_adapter_examples.rb +125 -38
- data/spec/integration/delayed_job_integration_spec.rb +1 -1
- data/spec/integration/middleware/domain_elevator_spec.rb +9 -0
- data/spec/integration/middleware/generic_elevator_spec.rb +10 -0
- data/spec/integration/middleware/subdomain_elevator_spec.rb +6 -49
- data/spec/spec_helper.rb +5 -2
- data/spec/support/apartment_helpers.rb +13 -10
- data/spec/support/contexts.rb +54 -0
- data/spec/support/requirements.rb +4 -4
- data/spec/unit/config_spec.rb +0 -4
- data/spec/unit/middleware/domain_elevator_spec.rb +26 -0
- data/spec/unit/middleware/subdomain_elevator_spec.rb +7 -7
- metadata +119 -108
data/.pryrc
ADDED
data/.travis.yml
ADDED
data/HISTORY.md
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
# 0.16.0
|
2
|
+
* June 1, 2012
|
3
|
+
|
4
|
+
- Apartment now supports a default_schema to be set, rather than relying on ActiveRecord's default schema_search_path
|
5
|
+
- Additional schemas can always be maintained in the schema_search_path by configuring persistent_schemas
|
6
|
+
- There is now a full domain based elevator to switch dbs based on the whole domain [lcowell]
|
7
|
+
- There is now a generic elevator that takes a Proc to switch dbs based on the return value of that proc.
|
8
|
+
|
1
9
|
# 0.15.0
|
2
10
|
* March 18, 2012
|
3
11
|
|
data/README.md
CHANGED
@@ -60,8 +60,9 @@ database, call switch with no arguments.
|
|
60
60
|
|
61
61
|
You can have Apartment route to the appropriate database by adding some Rack middleware.
|
62
62
|
Apartment can support many different "Elevators" that can take care of this routing to your data.
|
63
|
-
|
64
|
-
|
63
|
+
|
64
|
+
**Switch on subdomain**
|
65
|
+
In house, we use the subdomain elevator, which analyzes the subdomain of the request and switches to a database schema of the same name. It can be used like so:
|
65
66
|
|
66
67
|
# application.rb
|
67
68
|
module My Application
|
@@ -71,6 +72,29 @@ to a database schema of the same name. It can be used like so:
|
|
71
72
|
end
|
72
73
|
end
|
73
74
|
|
75
|
+
**Switch on domain**
|
76
|
+
To switch based on full domain (excluding subdomains *ie 'www'* and top level domains *ie '.com'* ) use the following:
|
77
|
+
|
78
|
+
# application.rb
|
79
|
+
module My Application
|
80
|
+
class Application < Rails::Application
|
81
|
+
|
82
|
+
config.middleware.use 'Apartment::Elevators::Domain'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
**Custom Elevator**
|
87
|
+
A Generic Elevator exists that allows you to pass a `Proc` (or anything that responds to `call`) to the middleware. This Object will be passed in an `ActionDispatch::Request` object when called for you to do your magic. Apartment will use the return value of this proc to switch to the appropriate database. Use like so:
|
88
|
+
|
89
|
+
# application.rb
|
90
|
+
module My Application
|
91
|
+
class Application < Rails::Application
|
92
|
+
# Obviously not a contrived example
|
93
|
+
config.middleware.use 'Apartment::Elevators::Generic', Proc.new { |request| request.host.reverse }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
|
74
98
|
## Config
|
75
99
|
|
76
100
|
The following config options should be set up in a Rails initializer such as:
|
@@ -85,20 +109,33 @@ To set config options, add this to your initializer:
|
|
85
109
|
|
86
110
|
### Excluding models
|
87
111
|
|
88
|
-
If you have some models that should always access the 'root' database, you can specify this by configuring
|
89
|
-
Apartment using `Apartment.configure`. This will yield a config object for you. You can set excluded models like so:
|
112
|
+
If you have some models that should always access the 'root' database, you can specify this by configuring Apartment using `Apartment.configure`. This will yield a config object for you. You can set excluded models like so:
|
90
113
|
|
91
114
|
config.excluded_models = ["User", "Company"] # these models will not be multi-tenanted, but remain in the global (public) namespace
|
92
115
|
|
93
116
|
Note that a string representation of the model name is now the standard so that models are properly constantized when reloaded in development
|
94
117
|
|
95
|
-
###
|
118
|
+
### Postgresql Schemas
|
96
119
|
|
97
|
-
|
98
|
-
|
99
|
-
and test environments. If you wish to turn this option off in production, you could do something like:
|
120
|
+
**Providing a Different default_schema**
|
121
|
+
By default, ActiveRecord will use `"$user", public` as the default `schema_search_path`. This can be modified if you wish to use a different default schema be setting:
|
100
122
|
|
101
|
-
config.
|
123
|
+
config.default_schema = "some_other_schema"
|
124
|
+
|
125
|
+
With that set, all excluded models will use this schema as the table name prefix instead of `public` and `reset` on `Apartment::Database` will return to this schema also
|
126
|
+
|
127
|
+
**Persistent Schemas**
|
128
|
+
Apartment will normally just switch the `schema_search_path` whole hog to the one passed in. This can lead to problems if you want other schemas to always be searched as well. Enter `persistent_schemas`. You can configure a list of other schemas that will always remain in the search path, while the default gets swapped out:
|
129
|
+
|
130
|
+
config.persistent_schemas = ['some', 'other', 'schemas']
|
131
|
+
|
132
|
+
This has numerous useful applications. [Hstore](http://www.postgresql.org/docs/9.1/static/hstore.html), for instance, is a popular storage engine for Postgresql. In order to use Hstore, you have to install to a specific schema and have that always in the `schema_search_path`. This could be achieved like so:
|
133
|
+
|
134
|
+
# In a rake task, or on the console...
|
135
|
+
ActiveRecord::Base.connection.execute("CREATE SCHEMA hstore; CREATE EXTENSION HSTORE SCHEMA hstore")
|
136
|
+
|
137
|
+
# configure Apartment to maintain the `hstore` schema in the `schema_search_path`
|
138
|
+
config.persistent_schemas = ['hstore']
|
102
139
|
|
103
140
|
### Managing Migrations
|
104
141
|
|
@@ -119,7 +156,15 @@ You can then migration your databases using the rake task:
|
|
119
156
|
This basically invokes `Apartment::Database.migrate(#{db_name})` for each database name supplied
|
120
157
|
from `Apartment.database_names`
|
121
158
|
|
122
|
-
###
|
159
|
+
### Handling Environments
|
160
|
+
|
161
|
+
By default, when not using postgresql schemas, Apartment will prepend the environment to the database name
|
162
|
+
to ensure there is no conflict between your environments. This is mainly for the benefit of your development
|
163
|
+
and test environments. If you wish to turn this option off in production, you could do something like:
|
164
|
+
|
165
|
+
config.prepend_environment = !Rails.env.production?
|
166
|
+
|
167
|
+
## Delayed::Job
|
123
168
|
|
124
169
|
If using Rails ~> 3.2, you *must* use `delayed_job ~> 3.0`. It has better Rails 3 support plus has some major changes that affect the serialization of models.
|
125
170
|
I haven't been able to get `psych` working whatsoever as the YAML parser, so to get things to work properly, you must explicitly set the parser to `syck` *before* requiring `delayed_job`
|
@@ -139,18 +184,25 @@ In order to make ActiveRecord models play nice with DJ and Apartment, include `A
|
|
139
184
|
include Apartment::Delayed::Requirements
|
140
185
|
end
|
141
186
|
|
187
|
+
Any classes that are being used as a Delayed::Job Job need to include the `Apartment::Delayed::Job::Hooks` module into the class. This ensures that when a job runs, it switches to the appropriate tenant before performing its task. It is also required (manually at the moment) that you set a `@database` attribute on your job so the hooks know what tennant to switch to
|
188
|
+
|
142
189
|
class SomeDJ
|
143
190
|
|
144
|
-
|
145
|
-
|
146
|
-
|
191
|
+
include Apartment::Delayed::Job::Hooks
|
192
|
+
|
193
|
+
def initialize
|
194
|
+
@database = Apartment::Database.current_database
|
147
195
|
end
|
148
196
|
|
149
197
|
def perform
|
150
|
-
# do some stuff
|
198
|
+
# do some stuff (will automatically switch to @database before performing and switch back after)
|
151
199
|
end
|
152
200
|
end
|
153
201
|
|
202
|
+
All jobs *must* stored in the global (public) namespace, so add it to the list of excluded models:
|
203
|
+
|
204
|
+
config.excluded_models = ["Delayed::Job"]
|
205
|
+
|
154
206
|
## Development
|
155
207
|
|
156
208
|
* The Local setup for development assumes that a root user with no password exists for both mysql and postgresl
|
data/Rakefile
CHANGED
@@ -7,16 +7,17 @@ require "rspec/core/rake_task"
|
|
7
7
|
|
8
8
|
RSpec::Core::RakeTask.new(:spec => "db:test:prepare") do |spec|
|
9
9
|
spec.pattern = "spec/**/*_spec.rb"
|
10
|
+
# spec.rspec_opts = '--order rand:16996'
|
10
11
|
end
|
11
12
|
|
12
13
|
namespace :spec do
|
13
|
-
|
14
|
+
|
14
15
|
[:tasks, :unit, :adapters, :integration].each do |type|
|
15
16
|
RSpec::Core::RakeTask.new(type => :spec) do |spec|
|
16
17
|
spec.pattern = "spec/#{type}/**/*_spec.rb"
|
17
18
|
end
|
18
19
|
end
|
19
|
-
|
20
|
+
|
20
21
|
end
|
21
22
|
|
22
23
|
task :default => :spec
|
@@ -30,39 +31,39 @@ end
|
|
30
31
|
namespace :postgres do
|
31
32
|
require 'active_record'
|
32
33
|
require "#{File.join(File.dirname(__FILE__), 'spec', 'support', 'config')}"
|
33
|
-
|
34
|
+
|
34
35
|
desc 'Build the PostgreSQL test databases'
|
35
36
|
task :build_db do
|
36
|
-
%x{ createdb -E UTF8 #{pg_config['database']} } rescue "test db already exists"
|
37
|
+
%x{ createdb -E UTF8 #{pg_config['database']} -Upostgres } rescue "test db already exists"
|
37
38
|
ActiveRecord::Base.establish_connection pg_config
|
38
39
|
ActiveRecord::Migrator.migrate('spec/dummy/db/migrate')
|
39
40
|
end
|
40
|
-
|
41
|
+
|
41
42
|
desc "drop the PostgreSQL test database"
|
42
43
|
task :drop_db do
|
43
44
|
puts "dropping database #{pg_config['database']}"
|
44
|
-
%x{ dropdb #{pg_config['database']} }
|
45
|
+
%x{ dropdb #{pg_config['database']} -Upostgres }
|
45
46
|
end
|
46
|
-
|
47
|
+
|
47
48
|
end
|
48
49
|
|
49
50
|
namespace :mysql do
|
50
51
|
require 'active_record'
|
51
52
|
require "#{File.join(File.dirname(__FILE__), 'spec', 'support', 'config')}"
|
52
|
-
|
53
|
+
|
53
54
|
desc 'Build the MySQL test databases'
|
54
55
|
task :build_db do
|
55
56
|
%x{ mysqladmin -u root create #{my_config['database']} } rescue "test db already exists"
|
56
57
|
ActiveRecord::Base.establish_connection my_config
|
57
58
|
ActiveRecord::Migrator.migrate('spec/dummy/db/migrate')
|
58
59
|
end
|
59
|
-
|
60
|
+
|
60
61
|
desc "drop the MySQL test database"
|
61
62
|
task :drop_db do
|
62
63
|
puts "dropping database #{my_config['database']}"
|
63
64
|
%x{ mysqladmin -u root drop #{my_config['database']} --force}
|
64
65
|
end
|
65
|
-
|
66
|
+
|
66
67
|
end
|
67
68
|
|
68
69
|
# TODO clean this up
|
data/apartment.gemspec
CHANGED
@@ -17,15 +17,16 @@ Gem::Specification.new do |s|
|
|
17
17
|
s.licenses = ["MIT"]
|
18
18
|
s.require_paths = ["lib"]
|
19
19
|
s.rubygems_version = %q{1.3.7}
|
20
|
-
|
20
|
+
|
21
21
|
s.add_dependency 'activerecord', '>= 3.1.2' # must be >= 3.1.2 due to bug in prepared_statements
|
22
|
-
s.add_dependency 'rack', '
|
23
|
-
|
22
|
+
s.add_dependency 'rack', '>= 1.3.6'
|
23
|
+
|
24
|
+
s.add_development_dependency 'pry', '~> 0.9.9'
|
24
25
|
s.add_development_dependency 'rails', '>= 3.1.2'
|
25
26
|
s.add_development_dependency 'rake', '~> 0.9.2'
|
26
27
|
s.add_development_dependency 'sqlite3'
|
27
|
-
s.add_development_dependency 'rspec', '~> 2.
|
28
|
-
s.add_development_dependency 'rspec-rails', '~> 2.
|
28
|
+
s.add_development_dependency 'rspec', '~> 2.10.0'
|
29
|
+
s.add_development_dependency 'rspec-rails', '~> 2.10.0'
|
29
30
|
s.add_development_dependency 'capybara', '~> 1.0.0'
|
30
31
|
s.add_development_dependency 'pg', '>= 0.11.0'
|
31
32
|
s.add_development_dependency 'mysql2', '~> 0.3.10'
|
data/lib/apartment.rb
CHANGED
@@ -3,8 +3,11 @@ require 'apartment/railtie' if defined?(Rails)
|
|
3
3
|
module Apartment
|
4
4
|
|
5
5
|
class << self
|
6
|
-
|
7
|
-
|
6
|
+
ACCESSOR_METHODS = [:use_postgres_schemas, :seed_after_create, :prepend_environment]
|
7
|
+
WRITER_METHODS = [:database_names, :excluded_models, :default_schema, :persistent_schemas]
|
8
|
+
|
9
|
+
attr_accessor(*ACCESSOR_METHODS)
|
10
|
+
attr_writer(*WRITER_METHODS)
|
8
11
|
|
9
12
|
# configure apartment with available options
|
10
13
|
def configure
|
@@ -21,6 +24,19 @@ module Apartment
|
|
21
24
|
@excluded_models || []
|
22
25
|
end
|
23
26
|
|
27
|
+
def default_schema
|
28
|
+
@default_schema || "public"
|
29
|
+
end
|
30
|
+
|
31
|
+
def persistent_schemas
|
32
|
+
@persistent_schemas || []
|
33
|
+
end
|
34
|
+
|
35
|
+
# Reset all the config for Apartment
|
36
|
+
def reset
|
37
|
+
(ACCESSOR_METHODS + WRITER_METHODS).each{|method| instance_variable_set(:"@#{method}", nil) }
|
38
|
+
end
|
39
|
+
|
24
40
|
end
|
25
41
|
|
26
42
|
autoload :Database, 'apartment/database'
|
@@ -33,7 +49,9 @@ module Apartment
|
|
33
49
|
end
|
34
50
|
|
35
51
|
module Elevators
|
36
|
-
autoload :
|
52
|
+
autoload :Generic, 'apartment/elevators/generic'
|
53
|
+
autoload :Subdomain, 'apartment/elevators/subdomain'
|
54
|
+
autoload :Domain, 'apartment/elevators/domain'
|
37
55
|
end
|
38
56
|
|
39
57
|
module Delayed
|
@@ -1,16 +1,16 @@
|
|
1
1
|
module Apartment
|
2
2
|
|
3
3
|
module Database
|
4
|
-
|
4
|
+
|
5
5
|
def self.mysql2_adapter(config)
|
6
|
-
Adapters::
|
6
|
+
Adapters::Mysql2Adapter.new config
|
7
7
|
end
|
8
8
|
end
|
9
|
-
|
9
|
+
|
10
10
|
module Adapters
|
11
|
-
|
12
|
-
class
|
13
|
-
|
11
|
+
|
12
|
+
class Mysql2Adapter < AbstractAdapter
|
13
|
+
|
14
14
|
protected
|
15
15
|
|
16
16
|
# Connect to new database
|
@@ -26,4 +26,4 @@ module Apartment
|
|
26
26
|
end
|
27
27
|
end
|
28
28
|
end
|
29
|
-
end
|
29
|
+
end
|
@@ -4,7 +4,7 @@ module Apartment
|
|
4
4
|
|
5
5
|
def self.postgresql_adapter(config)
|
6
6
|
Apartment.use_postgres_schemas ?
|
7
|
-
Adapters::PostgresqlSchemaAdapter.new(config
|
7
|
+
Adapters::PostgresqlSchemaAdapter.new(config) :
|
8
8
|
Adapters::PostgresqlAdapter.new(config)
|
9
9
|
end
|
10
10
|
end
|
@@ -33,27 +33,21 @@ module Apartment
|
|
33
33
|
# Separate Adapter for Postgresql when using schemas
|
34
34
|
class PostgresqlSchemaAdapter < AbstractAdapter
|
35
35
|
|
36
|
-
|
37
|
-
#
|
38
|
-
# @return {String} current schema search path
|
39
|
-
#
|
40
|
-
def current_database
|
41
|
-
ActiveRecord::Base.connection.schema_search_path
|
42
|
-
end
|
36
|
+
attr_reader :current_database
|
43
37
|
|
44
38
|
# Drop the database schema
|
45
39
|
#
|
46
40
|
# @param {String} database Database (schema) to drop
|
47
41
|
#
|
48
42
|
def drop(database)
|
49
|
-
ActiveRecord::Base.connection.execute(
|
43
|
+
ActiveRecord::Base.connection.execute(%{DROP SCHEMA "#{database}" CASCADE})
|
50
44
|
|
51
45
|
rescue ActiveRecord::StatementInvalid
|
52
46
|
raise SchemaNotFound, "The schema #{database.inspect} cannot be found."
|
53
47
|
end
|
54
48
|
|
55
49
|
# Reset search path to default search_path
|
56
|
-
# Set the table_name to always use the
|
50
|
+
# Set the table_name to always use the default namespace for excluded models
|
57
51
|
#
|
58
52
|
def process_excluded_models
|
59
53
|
Apartment.excluded_models.each do |excluded_model|
|
@@ -66,14 +60,14 @@ module Apartment
|
|
66
60
|
|
67
61
|
excluded_model.constantize.tap do |klass|
|
68
62
|
# some models (such as delayed_job) seem to load and cache their column names before this,
|
69
|
-
# so would never get the
|
63
|
+
# so would never get the default prefix, so reset first
|
70
64
|
klass.reset_column_information
|
71
65
|
|
72
66
|
# Ensure that if a schema *was* set, we override
|
73
67
|
table_name = klass.table_name.split('.', 2).last
|
74
68
|
|
75
69
|
# Not sure why, but Delayed::Job somehow ignores table_name_prefix... so we'll just manually set table name instead
|
76
|
-
klass.table_name = "
|
70
|
+
klass.table_name = "#{Apartment.default_schema}.#{table_name}"
|
77
71
|
end
|
78
72
|
end
|
79
73
|
end
|
@@ -83,7 +77,8 @@ module Apartment
|
|
83
77
|
# @return {String} default schema search path
|
84
78
|
#
|
85
79
|
def reset
|
86
|
-
|
80
|
+
@current_database = Apartment.default_schema
|
81
|
+
ActiveRecord::Base.connection.schema_search_path = full_search_path
|
87
82
|
end
|
88
83
|
|
89
84
|
protected
|
@@ -92,7 +87,9 @@ module Apartment
|
|
92
87
|
#
|
93
88
|
def connect_to_new(database = nil)
|
94
89
|
return reset if database.nil?
|
95
|
-
|
90
|
+
|
91
|
+
@current_database = database.to_s
|
92
|
+
ActiveRecord::Base.connection.schema_search_path = full_search_path
|
96
93
|
|
97
94
|
rescue ActiveRecord::StatementInvalid
|
98
95
|
raise SchemaNotFound, "The schema #{database.inspect} cannot be found."
|
@@ -101,12 +98,26 @@ module Apartment
|
|
101
98
|
# Create the new schema
|
102
99
|
#
|
103
100
|
def create_database(database)
|
104
|
-
ActiveRecord::Base.connection.execute(
|
101
|
+
ActiveRecord::Base.connection.execute(%{CREATE SCHEMA "#{database}"})
|
105
102
|
|
106
103
|
rescue ActiveRecord::StatementInvalid
|
107
104
|
raise SchemaExists, "The schema #{database} already exists."
|
108
105
|
end
|
109
106
|
|
107
|
+
private
|
108
|
+
|
109
|
+
# Generate the final search path to set including persistent_schemas
|
110
|
+
#
|
111
|
+
def full_search_path
|
112
|
+
persistent_schemas = persistent_schema_string
|
113
|
+
@current_database.to_s + (persistent_schemas.empty? ? "" : ", #{persistent_schemas}")
|
114
|
+
end
|
115
|
+
|
116
|
+
# Cached persistent schemas joined in a valid search path format (comma separated)
|
117
|
+
#
|
118
|
+
def persistent_schema_string
|
119
|
+
Apartment.persistent_schemas.join(', ')
|
120
|
+
end
|
110
121
|
end
|
111
122
|
end
|
112
123
|
end
|
data/lib/apartment/database.rb
CHANGED
@@ -3,12 +3,15 @@ require 'active_support/core_ext/module/delegation'
|
|
3
3
|
module Apartment
|
4
4
|
|
5
5
|
# The main entry point to Apartment functions
|
6
|
+
#
|
6
7
|
module Database
|
7
8
|
|
8
9
|
extend self
|
9
10
|
|
10
11
|
delegate :create, :current_database, :drop, :process, :process_excluded_models, :reset, :seed, :switch, :to => :adapter
|
11
12
|
|
13
|
+
attr_writer :config
|
14
|
+
|
12
15
|
# Initialize Apartment config options such as excluded_models
|
13
16
|
#
|
14
17
|
def init
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Apartment
|
2
|
+
module Elevators
|
3
|
+
# Provides a rack based db switching solution based on domain
|
4
|
+
# Assumes that database name should match domain
|
5
|
+
# Parses request host for second level domain
|
6
|
+
# eg. example.com => example
|
7
|
+
# www.example.bc.ca => example
|
8
|
+
#
|
9
|
+
class Domain < Generic
|
10
|
+
|
11
|
+
def parse_database_name(request)
|
12
|
+
return nil if request.host.blank?
|
13
|
+
|
14
|
+
request.host.match(/(www.)?(?<sld>[^.]*)/)["sld"]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|