2009-08-22 11 views
34

Mam standardu wiele-do-wielu między użytkownikami i rolami w moim app szyn:Szyny idiom, aby uniknąć duplikatów w has_many: poprzez

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

chcę, aby upewnić się, że użytkownik może być przypisany tylko jakakolwiek rola raz. Każda próba wstawienia duplikatu powinna zignorować żądanie, nie może powodować błędu ani powodować niepowodzenia sprawdzania poprawności. To, co naprawdę chcę reprezentować, to "zbiór", w którym wstawienie elementu, który już istnieje w zestawie, nie ma żadnego efektu. {1,2,3} U {1} = {1,2,3}, a nie {1,1,2,3}.

Zdaję sobie sprawę, że mogę to zrobić tak:

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

lub tworząc metodę otoki (np add_to_roles(role)), ale miałem nadzieję na jakiś idiomatycznym sposób, aby to automatyczny poprzez stowarzyszenia, tak że Mogę napisać:

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

i to po prostu działa za mnie. W ten sposób nie muszę pamiętać, aby sprawdzić dups lub użyć niestandardowej metody. Czy jest coś w ramie, której mi brakuje? Najpierw myślałem, że opcja: uniq do has_many to zrobi, ale jest to po prostu "wybierz odrębny".

Czy istnieje sposób, aby to zrobić w sposób deklaratywny? Jeśli nie, może korzystając z rozszerzenia powiązania?

Oto przykład tego, jak domyślne zachowanie nie powiedzie się:

 >> 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">]

Odpowiedz

23

Dopóki załączonych rola jest obiektem ActiveRecord, co robisz:

user.roles << role 

Gdyby de-powielać automatycznie :has_many skojarzeń.

Dla has_many :through, spróbuj:

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

jeśli bardzo nie działa, być może trzeba skonfigurować alias_method_chain.

+0

To nie działa w ten sposób. Zaktualizuję post, aby dołączyć test. – KingPong

+0

Dzięki, spróbuję rozszerzenia stowarzyszenia. – KingPong

+0

To działało idealnie. Dzięki! Część, której mi brakowało, gdy próbowałam czegoś takiego, to był kawałek proxy_owner. – KingPong

0

Być może jest to możliwe, aby utworzyć regułę walidacji

validates_uniqueness_of :user_roles 

następnie złapać wyjątek sprawdzania i prowadzić bezpiecznie. Jednak czuje się naprawdę hacky i jest bardzo nieelegancki, o ile to możliwe.

2

myślę właściwe reguły poprawności jest w twoich users_roles dołączyć model:

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

Dzięki. To jednak nie robi tego, co chcę (co jest zachowaniem podobnym do zestawu) i wyjaśniłem, co to jest w oryginalnym poście. Przepraszam za to. – KingPong

+0

Myślę, że to najlepsza odpowiedź na twój problem. Jeśli będziesz ostrożny w tworzeniu interfejsu, użytkownik musiałby zhakować go, aby dodać niewłaściwą rolę, w takim przypadku wyjątek sprawdzania poprawności jest całkowicie odpowiednią odpowiedzią. – austinfromboston

+1

Heh, zwariowałeś? Użytkownicy nie dodają własnych ról :-) Typowym przykładem użycia jest to, że użytkownik staje się członkiem roli jako efekt uboczny czegoś innego. Na przykład zakup konkretnego produktu. Inne produkty mogą również pełnić tę samą rolę, więc istnieje szansa na ich powielenie. Wolałbym sprawdzać duplikację w jednym miejscu niż w dowolnych miejscach, w których trzeba się upewnić, że użytkownik ma rolę. W tym sensie dawanie użytkownikowi roli, którą już posiada, NIE jest warunkiem błędu. – KingPong

3

Można użyć kombinacji validates_uniqueness_of i nadrzędnym < < w głównym modelu, choć będzie to również złapać jakieś inne błędy sprawdzania poprawności w model łączenia.

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

Nie można zmienić tego wyjątku na "ActiveRecord :: RecordNotUnique"? Podoba mi się ta odpowiedź. Należy jednak pamiętać o [warunkach wyścigu] (http://apidock.com/rails/ActiveRecord/Validations/ClassMethods/validates_uniqueness_of). – Ashitaka

+0

Dobra odpowiedź. Użyłem go bez 'validates_uniqueness_of', po zadeklarowaniu unikalnego indeksu w bazie danych i działa uroczo. –

0

Myślę, że chcesz zrobić coś takiego:

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

wpadłem na to dzisiaj, a zakończył się przy użyciu #replace, która „będzie wykonać diff i usuwać/dodawać tylko rekordy, które zostały zmienione” .

Dlatego trzeba zdać unii istniejących ról (więc nie zostaną usunięte) i nowej roli (s):

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

Ważne jest, aby pamiętać, że zarówno tę odpowiedź i akceptowane są ładowanie powiązanych obiektów roles do pamięci, aby wykonać różnicę macierzy (-) i połączenie (|). Może to prowadzić do problemów z wydajnością, jeśli masz do czynienia z dużą liczbą powiązanych rekordów.

Jeśli to niepokojące, możesz zajrzeć do opcji, które najpierw sprawdzają istnienie za pomocą zapytań lub użyć zapytania typu INSERT ON DUPLICATE KEY UPDATE (mysql) do wstawienia.