Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fully separate schema: common vs tenant #117

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Reworked things to:
1) maintain separation between "common" and tenant schemas
   (means that "common" tables are not also in the tenant schema!)
2) taks to process all tenants or a single tenant
3) support for per tenant migrations (potentially scary)
4) changed default so that db:migrate does NOT run
   apartment migrations.
  • Loading branch information
mjsteckel committed Mar 3, 2014
commit dbcb828bde27536ff63f302b59c1ede514303e04
4 changes: 2 additions & 2 deletions lib/apartment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class << self

extend Forwardable

ACCESSOR_METHODS = [:use_schemas, :seed_after_create, :prepend_environment, :append_environment]
ACCESSOR_METHODS = [:use_schemas, :seed_after_create, :prepend_environment, :append_environment, :migration_path]
WRITER_METHODS = [:tenant_names, :database_schema_file, :excluded_models, :default_schema, :persistent_schemas, :connection_class, :tld_length, :db_migrate_tenants]

attr_accessor(*ACCESSOR_METHODS)
Expand All @@ -33,7 +33,7 @@ def tenant_names
def db_migrate_tenants
return @db_migrate_tenants if defined?(@db_migrate_tenants)

@db_migrate_tenants = true
@db_migrate_tenants = false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I Don't think I agree with this default. Can you explain why you think Apartment shouldn't migrate the tenants by default?

I'm just trying to look at things from a new user's perspective. Someone that doesn't know Apartment that well would have to know to enable this, rather than it just working out of the box. More advanced users can disable it to their liking, but I'd like to err on the side of simplicity

end

# Default to empty array
Expand Down
10 changes: 5 additions & 5 deletions lib/apartment/adapters/abstract_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ def drop(tenant)
#
# @param {String?} tenant Database or schema to connect to
#
def process(tenant = nil)
def process(tenant = nil, exclusive = false)
previous_tenant = current_tenant
switch(tenant)
switch(tenant, exclusive)
yield if block_given?

ensure
Expand All @@ -93,11 +93,11 @@ def reset
#
# @param {String} tenant Database name
#
def switch(tenant = nil)
def switch(tenant = nil, exclusive = false)
# Just connect to default db and return
return reset if tenant.nil?

connect_to_new(tenant).tap do
connect_to_new(tenant, exclusive).tap do
ActiveRecord::Base.connection.clear_query_cache
end
end
Expand Down Expand Up @@ -126,7 +126,7 @@ def create_tenant(tenant)
#
# @param {String} tenant Database name
#
def connect_to_new(tenant)
def connect_to_new(tenant, exclusive = false)
Apartment.establish_connection multi_tenantify(tenant)
Apartment.connection.active? # call active? to manually check if this connection is valid

Expand Down
26 changes: 23 additions & 3 deletions lib/apartment/adapters/postgresql_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,22 @@ def drop(tenant)
raise SchemaNotFound, "The schema #{tenant.inspect} cannot be found."
end

def load(tenant)
# require 'active_record/schema_dumper'
# filename = File.join(Apartment.migration_path, "#{tenant}_schema.rb")
# File.open(filename, "w:utf-8") do |file|
# ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
# end
end

def dump(tenant)
require 'active_record/schema_dumper'
filename = File.join(Apartment.migration_path, "..", "#{tenant}_schema.rb")
File.open(filename, "w:utf-8") do |file|
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
end
end

# Reset search path to default search_path
# Set the table_name to always use the default namespace for excluded models
#
Expand Down Expand Up @@ -84,12 +100,16 @@ def current_tenant

# Set schema search path to new schema
#
def connect_to_new(tenant = nil)
def connect_to_new(tenant = nil, exclusive = false)
return reset if tenant.nil?
raise ActiveRecord::StatementInvalid.new("Could not find schema #{tenant}") unless Apartment.connection.schema_exists? tenant

@current_tenant = tenant.to_s
Apartment.connection.schema_search_path = full_search_path
if exclusive
Apartment.connection.schema_search_path = @current_tenant
else
Apartment.connection.schema_search_path = full_search_path
end

rescue *rescuable_exceptions
raise SchemaNotFound, "One of the following schema(s) is invalid: #{tenant}, #{full_search_path}"
Expand All @@ -113,7 +133,7 @@ def full_search_path
end

def persistent_schemas
[@current_tenant, Apartment.persistent_schemas].flatten
[@current_tenant, Apartment.persistent_schemas].flatten.uniq
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/apartment/database.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ module Database
extend self
extend Forwardable

def_delegators :adapter, :create, :current_tenant, :current, :current_database, :drop, :process, :process_excluded_models, :reset, :seed, :switch
def_delegators :adapter, :create, :current_tenant, :current, :current_database, :drop, :process, :process_excluded_models, :reset, :seed, :switch, :dump, :load

attr_writer :config

Expand Down
24 changes: 18 additions & 6 deletions lib/apartment/migrator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,39 @@ module Migrator

# Migrate to latest
def migrate(database)
Database.process(database) do
Database.process(database, true) do
version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil

ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, version) do |migration|
ActiveRecord::Migrator.migrate(migration_paths(database), version) do |migration|
ENV["SCOPE"].blank? || (ENV["SCOPE"] == migration.scope)
end
Database.dump(database)
end
end

# Migrate up/down to a specific version
def run(direction, database, version)
Database.process(database) do
ActiveRecord::Migrator.run(direction, ActiveRecord::Migrator.migrations_paths, version)
Database.process(database, true) do
ActiveRecord::Migrator.run(direction, migration_paths(database), version)
Database.dump(database)
end
end

# rollback latest migration `step` number of times
def rollback(database, step = 1)
Database.process(database) do
ActiveRecord::Migrator.rollback(ActiveRecord::Migrator.migrations_paths, step)
Database.process(database, true) do
ActiveRecord::Migrator.rollback(migration_paths(database), step)
Database.dump(database)
end
end

private

def migration_paths(tenant)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure the previous impl, where one takes the AR migration_paths is so that engines that provide migrations could also be run. I didn't implement it, it was a PR, but I'm pretty sure this would revert that change. @linki can you comment?

paths = [Apartment.migration_path]
paths << "#{Apartment.migration_path}/../#{tenant}" if File.exists?("#{Apartment.migration_path}/../#{tenant}")
paths
end

end
end
4 changes: 2 additions & 2 deletions lib/apartment/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ class Railtie < Rails::Railtie
#
config.before_initialize do
Apartment.configure do |config|
config.db_migrate_tenants = false
config.excluded_models = []
config.use_schemas = true
config.tenant_names = []
config.seed_after_create = false
config.prepend_environment = false
config.append_environment = false
config.tld_length = 1
config.migration_path = "#{Rails.root}/db/apartment/migrate"
end

ActiveRecord::Migrator.migrations_paths = Rails.application.paths['db/migrate'].to_a
end

# Hook into ActionDispatch::Reloader to ensure Apartment is properly initialized
Expand Down
9 changes: 5 additions & 4 deletions lib/generators/apartment/install/templates/apartment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
#
Apartment.configure do |config|

# Determines whether db:migrations will automatically
# run apartment migrations or not. Defautls to false
#
# config.db_migrate_tenants = true

# These models will not be multi-tenanted,
# but remain in the global (public) namespace
#
Expand Down Expand Up @@ -43,7 +48,3 @@
# Rails.application.config.middleware.use 'Apartment::Elevators::Domain'

Rails.application.config.middleware.use 'Apartment::Elevators::Subdomain'

##
# Rake enhancements so that db:migrate etc... also runs migrations on all tenants
require 'apartment/tasks/enhancements'
105 changes: 81 additions & 24 deletions lib/tasks/apartment.rake
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ require 'apartment/migrator'

apartment_namespace = namespace :apartment do

task :init => ['environment', 'db:load_config']

desc "Create all tenants"
task create: 'db:migrate' do
task create: :init do
tenants.each do |tenant|
begin
puts("Creating #{tenant} tenant")
Expand All @@ -15,8 +17,8 @@ apartment_namespace = namespace :apartment do
end

desc "Migrate all tenants"
task :migrate do
warn_if_tenants_empty
task migrate: :init do
err_if_tenants_empty

tenants.each do |tenant|
begin
Expand All @@ -29,8 +31,8 @@ apartment_namespace = namespace :apartment do
end

desc "Seed all tenants"
task :seed do
warn_if_tenants_empty
task seed: :init do
err_if_tenants_empty

tenants.each do |tenant|
begin
Expand All @@ -45,8 +47,8 @@ apartment_namespace = namespace :apartment do
end

desc "Rolls the migration back to the previous version (specify steps w/ STEP=n) across all tenants."
task :rollback do
warn_if_tenants_empty
task rollback: :init do
err_if_tenants_empty

step = ENV['STEP'] ? ENV['STEP'].to_i : 1

Expand All @@ -60,10 +62,75 @@ apartment_namespace = namespace :apartment do
end
end

namespace :tenant do
desc "Migrate a single tenant: required paramter TENANT=name"
task :migrate,[:tenant] => :init do |t, args|
tenant = args.tenant || ENV['TENANT']

err_if_tenants_empty
raise 'TENANT is required' unless tenant
raise "TENANT #{tenant} is unknown" if !tenants.include?(tenant)

begin
puts("Migrating #{tenant} tenant")
Apartment::Migrator.migrate tenant
rescue Apartment::TenantNotFound => e
puts e.message
end
end

desc "Rolls the migration back to the previous version (specify steps w/ STEP=n) for one TENANT."
task :rollback,[:tenant, :step] => :init do |t, args|
tenant = args.tenant || ENV['TENANT']
step = args.step || ENV['STEP'] ? ENV['STEP'].to_i : 1

err_if_tenants_empty
raise 'TENANT is required' unless tenant
raise "TENANT #{tenant} is unknown" if !tenants.include?(tenant)

begin
puts("Rolling back #{tenant} tenant")
Apartment::Migrator.rollback tenant, step
rescue Apartment::TenantNotFound => e
puts e.message
end
end

desc 'Create a db/schema.rb file that can be portably used against any DB supported by AR'
task :dump, [:tenant] => ['environment', 'db:load_config'] do |t, args|

tenant = args.tenant || ENV['TENANT']

err_if_tenants_empty
raise 'TENANT is required' unless tenant
raise "TENANT #{tenant} is unknown" if !tenants.include?(tenant)

Apartment::Database.dump(tenant)

# apartment_namespace['dump'].reenable
end

# desc 'Load a schema.rb file into the database'
# task :load,[:tenant] => ['environment', 'db:load_config'] do |t, args|
# tenant = args.tenant || ENV['TENANT']

# err_if_tenants_empty
# raise 'TENANT is required' unless tenant
# raise "TENANT #{tenant} is unknown" if !tenants.include?(tenant)

# puts "apartment:tenant:load(#{tenant})"

# Apartment::Database.load(tenant)
# end

end


namespace :migrate do

desc 'Runs the "up" for a given migration VERSION across all tenants.'
task :up do
warn_if_tenants_empty
task up: :init do
err_if_tenants_empty

version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil
raise 'VERSION is required' unless version
Expand All @@ -79,8 +146,8 @@ apartment_namespace = namespace :apartment do
end

desc 'Runs the "down" for a given migration VERSION across all tenants.'
task :down do
warn_if_tenants_empty
task down: :init do
err_if_tenants_empty

version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil
raise 'VERSION is required' unless version
Expand All @@ -96,7 +163,7 @@ apartment_namespace = namespace :apartment do
end

desc 'Rolls back the tenant one migration and re migrate up (options: STEP=x, VERSION=x).'
task :redo do
task redo: :init do
if ENV['VERSION']
apartment_namespace['migrate:down'].invoke
apartment_namespace['migrate:up'].invoke
Expand All @@ -111,17 +178,7 @@ apartment_namespace = namespace :apartment do
ENV['DB'] ? ENV['DB'].split(',').map { |s| s.strip } : Apartment.tenant_names || []
end

def warn_if_tenants_empty
if tenants.empty?
puts <<-WARNING
[WARNING] - The list of tenants to migrate appears to be empty. This could mean a few things:

1. You may not have created any, in which case you can ignore this message
2. You've run `apartment:migrate` directly without loading the Rails environment
* `apartment:migrate` is now deprecated. Tenants will automatically be migrated with `db:migrate`

Note that your tenants currently haven't been migrated. You'll need to run `db:migrate` to rectify this.
WARNING
end
def err_if_tenants_empty
raise "tenants is empty" if tenants.empty?
end
end