PostgreSQL的schema是一個非常方便的功能, 適合拿來做所謂的Multi-Tenancy 類服務; 例如無名或Pixnet這類的BSP(Blog Service Provider)或是像我的FREEBBS的免費論壇服務(Forum Service Provider), 通常稱為SAAS(Software As A Service)系統, 可以把N個相同結構的schema放在同一個資料庫裡面; 比起MySQL只能分別放在不同的資料庫裡會有很多好處。
假設今天我用PostgreSQL的schema功能來做一個BSP服務的話, DB內的schema會分成兩大類:
管理用schema(只有一個)
Blog Schema(有無數個, 每個的結構相同)
一般狀況下, 我們在使用ActiveRecord連接PosttgreSQL時可以在連接選項設定使用Schema, 像這樣:
database.yml
1
2
3
4
5
6
development :
adapter : postgresql
encoding : unicode
database : SHOPON_development
pool : 5
schema_search_path : base, blog1
schema_search_path的預設值是public, 如果照以上設定, 當你執行任何migration時會先從base這個schema執行起, 也就是所有的migration都只會跑在base這個schema上面,
但場景回到Rails/ActiveRecord上, 假如你要做一個BSP, 一般而言你有以下幾種方式:
管理端(管理BSP的)和服務端(Blog服務本身)分別各自一個Rails Application目錄, 然後各有各的migrate
將兩端放在同一個Rails App裡
如果用一般的方式migrate, Rails會依照你database.yml裡設定的schema_search_path的順序找第一順位來執行migrate; 這樣你要如何migrate你的資料到不同schema呢?
今天要講的就是將兩端放在同app裡的方式, 也就是
在migrate檔內設定只能在服務端schema或是管理端schema執行
有單獨migrate管理端 和單獨migrate服務端 的功能
實現1.的方式如下 :
migrate_control_side.rb
1
2
3
4
5
6
7
8
9
10
11
class CreateSites < ActiveRecord : :Migration
def change
if ActiveRecord : :Base . connection . schema_search_path == 'base' #base為管理端schema
create_table :sites do | t |
t . string :subdn
t . string :name
t . timestamps
end
end
end
end
簡單來說就是當現在的 schema_search_path 不等於base則不執行create_table的動作; 如果是服務端, 則反過來設定schema_search_path不為base或public即可, 更進一步的可以將這兩種檢查寫成ActiveRecord::Migration的module, 像這樣:
multitenancy_migration.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module ActiveRecord::MultitenancyMigration
def must_migrate_in_base #管理端
if ActiveRecord : :Base . connection . schema_search_path == 'base'
yield
end
end
def must_migrate_in_site #服務端
schema_search_path = ActiveRecord : :Base . connection . schema_search_path
if ! schema_search_path . index ( 'base' ) && ! schema_search_path . index ( 'public' )
yield
end
end
end
ActiveRecord : :Migration . class_eval do
include ActiveRecord : :MultitenancyMigration
extend ActiveRecord : :MultitenancyMigration
end
由於migration檔的內部可能是以self.up(class method)或是up(instance method)的方式編寫, 因此需要同時include和extend這個模組。
實現2.的方式如下 :
需要建立for 管理端以及 for 服務端的task 來migrate 各自的schema
base.rake
1
2
3
4
5
6
7
8
9
10
11
12
namespace :base do
task migrate_db : :environment do
ActiveRecord : :Base . connection . schema_search_path = 'base'
Rake : :Task [ 'db:migrate' ]. invoke
end
task rollback_db : :environment do
ActiveRecord : :Base . connection . schema_search_path = 'base'
Rake : :Task [ 'db:rollback' ]. invoke
end
end
site.rake
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
namespace :site do
def check_subdn_exists
if ENV . has_key? ( 'SUBDN' )
ActiveRecord : :Base . connection . schema_search_path = ENV [ 'SUBDN' ]
yield
else
puts "Please assign SUBDN first!"
end
end
desc 'Migrate Database of Individual site, have to assign SUBDN paramater!'
task migrate_db : :environment do
check_subdn_exists do
Rake : :Task [ 'db:migrate' ]. invoke
end
end
desc 'Rollback migration of single site'
task rollback_db : :environment do
check_subdn_exists do
Rake : :Task [ 'db:rollback' ]. invoke
end
end
end
簡單來說就是在migrate前先切換schema, 然後用Rake的指令呼叫原本的db:migrate/rollback出來, 當然你也可以寫一個task是去掃現存的服務端列表然後再各自migrate, 如果要寫在controller或model的話, 則必需先require rake然後再load Rakefile才行。
以上