2009-08-22 35 views
34

我有一個標準的多對許多用戶和角色在我的Rails應用程序之間的關係:Rails的成語,以避免重複中的has_many:通過

class User < ActiveRecord::Base 
    has_many :user_roles 
    has_many :roles, :through => :user_roles 
end 

我想確保一個用戶只能分配任何角色一次。任何嘗試插入重複都應該忽略該請求,不會拋出錯誤或導致驗證失敗。我真正想要表達的是一個「集合」,其中插入集合中已經存在的元素不起作用。 {1,2,3} U {1} = {1,2,3},而不是{1,1,2,3}。

我知道我能做到這一點是這樣的:通過創建一個包裝方法(如add_to_roles(role)

user.roles << role unless user.roles.include?(role) 

或者,但我希望對一些慣用的方法,使其通過關聯自動,這樣我可以寫:

user.roles << role # automatically checks roles.include? 

它只是爲我做的工作。這樣,我不必記得檢查dups或使用自定義方法。我錯過了框架中的某些東西嗎?我首先想到:has_many的uniq選項可以實現,但它基本上只是「選擇不同」。

有沒有辦法做到這一點聲明?如果沒有,也許通過使用關聯擴展?

這裏的默認行爲是如何失敗的一個例子:

 >> u = User.create 
     User Create (0.6ms) INSERT INTO "users" ("name") VALUES(NULL) 
    => #<User id: 3, name: nil> 
    >> u.roles << Role.first 
     Role Load (0.5ms) SELECT * FROM "roles" LIMIT 1 
     UserRole Create (0.5ms) INSERT INTO "user_roles" ("role_id", "user_id") VALUES(1, 3) 
     Role Load (0.4ms) SELECT "roles".* FROM "roles" INNER JOIN "user_roles" ON "roles".id = "user_roles".role_id WHERE (("user_roles".user_id = 3)) 
    => [#<Role id: 1, name: "1">] 
    >> u.roles << Role.first 
     Role Load (0.4ms) SELECT * FROM "roles" LIMIT 1 
     UserRole Create (0.5ms) INSERT INTO "user_roles" ("role_id", "user_id") VALUES(1, 3) 
    => [#<Role id: 1, name: "1">, #<Role id: 1, name: "1">]

回答

23

只要附加的角色是一個ActiveRecord對象,你在做什麼:

user.roles << role 

應該:has_many協會自動刪除重複。

對於has_many :through,嘗試:

class User 
    has_many :roles, :through => :user_roles do 
    def <<(new_item) 
     super(Array(new_item) - proxy_association.owner.roles) 
    end 
    end 
end 

如果超級不工作,你可能需要建立一個alias_method_chain。

+0

它不這樣工作。我會更新帖子以包含測試。 – KingPong 2009-08-22 17:24:55

+0

謝謝,我會嘗試關聯擴展。 – KingPong 2009-08-24 15:42:15

+0

完美運作。謝謝!當我嘗試類似這樣的事情時,我錯過的部分是proxy_owner位。 – KingPong 2009-08-24 18:05:03

0

也許可以創建驗證規則

validates_uniqueness_of :user_roles 

,然後趕上驗證異常並優雅矣。但是,如果可能的話,這種感覺真的很黑,並且非常不雅。

2

我認爲正確的驗證規則是在你的users_roles加盟模式:

validates_uniqueness_of :user_id, :scope => [:role_id] 
+0

謝謝。這實際上並沒有做我想做的事(這是一種類似於集合的行爲),我已經澄清了原始文章中的內容。對不起'回合。 – KingPong 2009-08-22 16:40:11

+0

我認爲這是解決您的問題的最佳答案。如果您在創建界面時非常小心,那麼用戶將不得不破解它以添加錯誤的角色,在這種情況下,驗證異常是完全合適的響應。 – austinfromboston 2009-08-23 23:42:07

+1

嘿,你瘋了嗎?用戶不會添加他們自己的角色:-) 典型的用例是用戶成爲角色的成員,作爲別的副作用。例如,購買特定的產品。其他產品也可能提供相同的作用,所以有重複的機會。我寧願在一個地方進行重複檢查,而不是在隨機的地方確保用戶的角色。從這個意義上講,給用戶一個他已經擁有的角色並不是一個錯誤狀態。 – KingPong 2009-08-24 15:40:28

3

您可以使用validates_uniqueness_of和壓倒一切的< <的組合中的主力機型,雖然這也將趕上其他任何驗證錯誤連接模型。

validates_uniqueness_of :user_id, :scope => [:role_id] 

class User 
    has_many :roles, :through => :user_roles do 
    def <<(*items) 
     super(items) rescue ActiveRecord::RecordInvalid 
    end 
    end 
end 
+1

難道你不能將這個異常改爲「ActiveRecord :: RecordNotUnique」嗎?我喜歡這個答案。不過,請注意[競態條件](http://apidock.com/rails/ActiveRecord/Validations/ClassMethods/validates_uniqueness_of)。 – Ashitaka 2014-01-06 02:17:39

+0

很好的答案。我沒有使用'validates_uniqueness_of',它在數據庫中聲明瞭唯一的索引並且很有魅力。 – 2016-02-06 14:55:50

0

我想你想要做的事,如:

user.roles.find_or_create_by(role_id: role.id) # saves association to database 
user.roles.find_or_initialize_by(role_id: role.id) # builds association to be saved later 
0

我就遇到了這個今天結束了使用#replace,這「將執行diff和刪除/添加只已改變的記錄」 。

因此,你需要通過現有角色的工會(所以他們不被刪除)和你的新角色(S):

new_roles = [role] 
user.roles.replace(user.roles | new_roles) 

需要注意的是兩個這樣的回答很重要,爲了執行Array diff(-)和聯合(|),已接受一個將相關的roles對象加載到內存中。如果您正在處理大量相關記錄,這可能會導致性能問題。

如果這是一個問題,您可能希望查看通過查詢首先檢查是否存在的選項,或者使用INSERT ON DUPLICATE KEY UPDATE(mysql)類型的查詢進行插入。