2014-09-24 94 views
1

注:原來的問題已經改變了一下。我找到了兩種解決方案,並可能在完全改變設計的方式上。在任何情況下,我都會想知道,爲什麼RequestStore不工作(是因爲Warden在中間件堆棧中攔截消息?),Thread.current如何工作,以及爲什麼實例變量是不穩定的解。設計和多租戶範圍


我已經在我的應用程序中使用default_scope啓用了多租戶,包括Devise User模型。

在application_controller.rb,我有

around_filter :set_request_store 

def set_request_store 
    Tenant.current = current_tenant.id 
    yield 
ensure 
    Tenant.current = nil 
end 

而且Tenant.current又設置了一個RequestStore哈希鍵。

在tenant.rb

def self.current 
    RequestStore.store[:current_tenant_id] 
end 

def self.current=(tenant_id) 
    RequestStore.store[:current_tenant_id] = tenant_id 
end 

在我的routes.rb文件,我有以下

unauthenticated do 
    root to: 'home#index', as: :public_root 
    end 

    authenticated :user do 
    root to: 'dashboard#index', as: :application_root 
    end 

我面臨的問題是更好地通過日誌說明。

Started POST "https://stackoverflow.com/users/sign_in" for 127.0.0.1 at 2014-09-24 14:57:13 +0530 
Processing by Devise::SessionsController#create as HTML 
    Parameters: {"utf8"=>"✓", "authenticity_token"=>"[FILTERED]", "user"=>{"tenant_id"=>"1", "email"=>"[email protected]", "password"=>"[FILTERED]"}} 
    Tenant Load (0.9ms) SELECT "tenants".* FROM "tenants" WHERE "tenants"."subdomain" = 'test' ORDER BY "tenants"."id" ASC LIMIT 1 
    User Load (0.8ms) SELECT "users".* FROM "users" WHERE "users"."tenant_id" = 1 AND "users"."email" = '[email protected]' ORDER BY "users"."id" ASC LIMIT 1 
    (0.2ms) BEGIN 
    SQL (0.5ms) UPDATE "users" SET "current_sign_in_at" = $1, "last_sign_in_at" = $2, "sign_in_count" = $3, "updated_at" = $4 WHERE "users"."id" = 1 [["current_sign_in_at", "2014-09-24 09:27:13.553818"], ["last_sign_in_at", "2014-09-24 09:26:31.548568"], ["sign_in_count", 44], ["updated_at", "2014-09-24 09:27:13.556155"]] 
    (1.1ms) COMMIT 

在一個成功的標誌。設計重定向應用(應用程序)的根路徑。實際上,公共和應用程序根目錄的路徑是相同的。

Redirected to http://test.com.dev/ 
Completed 302 Found in 90ms (ActiveRecord: 3.4ms) 

在路由未經身份驗證的方法調用(可能)試圖給用戶(在中間件???使用看守的地方)認證和tenant_id沒有設置在這一點上。請參閱tenant_id的WHERE子句。

Started GET "/" for 127.0.0.1 at 2014-09-24 14:57:13 +0530 
    User Load (0.8ms) SELECT "users".* FROM "users" WHERE "users"."tenant_id" IS NULL AND "users"."id" = 1 ORDER BY "users"."id" ASC LIMIT 1 
Processing by HomeController#index as HTML 

有沒有人遇到過這樣的問題並解決了它?


解決方案1:

首先,我一直在使用Thread.current解決它。出於某種原因,RequestStore.store沒有在Devise方法中設置。

以下代碼可解決登錄問題。但是,我無法找到安全取消Thread.current值的地方。

在user.rb

devise ..., 
     request_keys: [:subdomain] 


default_scope { where(tenant_id: (Tenant.current || Thread.current[:current_tenant_id])) } 

protected 

def self.find_for_authentication(warden_conditions) 
    subdomain = warden_conditions.delete(:subdomain) 
    Thread.current[:current_tenant_id] = Tenant.where(subdomain: subdomain).first.id 
    super 
end 

解決方案2:

UPDATE:這也有問題。這不總是工作。

改爲使用實例變量。

在用戶。RB

devise ..., 
     request_keys: [:subdomain] 


default_scope { where(tenant_id: (Tenant.current || @tenant_id)) } 

protected 

def self.find_for_authentication(warden_conditions) 
    subdomain = warden_conditions.delete(:subdomain) 
    @tenant_id = Tenant.where(subdomain: subdomain).first.id 
    super 
end 

我想知道這將是一個更安全的方法,或是否有解決的更好的方法。

回答

5

我寧願靠一個專門的模塊:

module TenantScope 
    extend self 

    class Error < StandardError 
    end 

    def current 
    threadsafe_storage[:current] 
    end 

    def current=(tenant) 
    threadsafe_storage[:current] = tenant 
    end 

    def with(tenant) 
    previous_scope = current 

    raise Error.new("Tenant can't be nil in #{self.name}.with") if tenant.nil? 

    self.current = tenant 
    yield(current) if block_given? 
    ensure 
    self.current = previous_scope 
    nil 
    end 

    private 

    def threadsafe_storage 
    Thread.current[:tenant_scope] ||= {} 
    end 

end 

然後,我用它爲對象的default_scope。只是include TenantScope::ModelMixin在模型中(而不是在租戶):

module TenantScope 
    module ModelMixin 

    def self.included(base) 
     base.belongs_to :tenant 
     base.validates_presence_of :tenant_id 

     base.send(:default_scope, lambda { 
     if TenantScope.current 
      return base.where("#{base.table_name}.tenant_id" => TenantScope.current.id) 
     end 

     raise Error.new('Scoped class method called without a tenant being set') 
     }) 
    end 

    end 
end 

而且我用的中間件來設置範圍。

module TenantScope 
    class Rack 

    attr_reader :request 

    def initialize(app) 
     @app = app 
    end 

    def call(env) 
     @request = ::Rack::Request.new(env) 

     unless tenant = Tenant.find_from_host(@request.host) 
     logger.error "[TenantScope] tenant not found: #{request.host}" 
     return [404, { 'Content-Type' => 'text/plain', 'Content-Length' => '29' }, ["This tenant does not exist"]] 
     end 

     logger.debug "[TenantScope] tenant found: #{tenant.name}" 
     TenantScope.with(tenant) do 
     @app.call(env) 
     end 
    end 

    def logger 
     Rails.logger 
    end 

    end 
end 

使用中間件,並確保每個對您的模型的訪問發生在該中間件的下方。控制器中發生的一切都是這種情況。

我給你幾個線索。你看到我非常嚴格,並且必須始終設置租戶,即使在遷移期間或控制檯中也是如此。

需要注意的是,現在,通過所有用戶走,在例如遷移,例如,你所要做的:

Tenant.each do |tenant| 
    TenantScope.with(tenant) do 
    User.all.each do |user| 
     # do your stuff here. 
    end 
    end 
end 
+0

我原來的問題解決更優雅的使用這種方法。謝謝! – 2014-10-02 10:27:09

+0

這個發現是一個絕對的創業板。有沒有這個測試套件或寶石? – 2014-10-12 22:38:38