Rails idiom, aby uniknąć duplikatów w ma wiele: przez

Mam standardową relację między użytkownikami a rolami w mojej aplikacji Rails:

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

Chcę się upewnić, że użytkownik może być przypisany do każdej roli tylko raz. Każda próba Wstawienia duplikatu powinna ignorować żądanie, a nie wywoływać błędu lub powodować niepowodzenie walidacji. To, co naprawdę chcę reprezentować, to "zestaw", w którym wstawianie 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ć jak to:

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

Lub tworząc metodę wrappera( np. add_to_roles(role)), ale liczyłem na jakiś idiomatyczny sposób, aby uczynić ją automatyczną poprzez skojarzenie, żebym mógł napisać:

user.roles << role  # automatically checks roles.include?
I to po prostu robi za mnie robotę. W ten sposób nie muszę pamiętać, aby sprawdzić dups lub użyć niestandardowej metody. Czy jest coś w ramce, o czym Nie wiem? Na początku myślałem, że opcja: uniq do has_many to zrobi, ale jest to po prostu " select distinct."

Czy jest na to sposób deklaratywnie? Jeśli nie, może używając rozszerzenia asocjacji?

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

    >> 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">]
Author: KingPong, 2009-08-22

7 answers

Dopóki dołączona rola jest obiektem ActiveRecord, to co robisz:

user.roles << role

Powinien automatycznie usuwać duplikaty dla :has_many asocjacji.

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 super nie działa, może być konieczne skonfigurowanie alias_method_chain.

 23
Author: austinfromboston,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-12-17 05:08:39

Użyj tablicy |= metoda Join.

Możesz użyć tablic |= metoda join dodaje element do tablicy, chyba że jest już obecny. Tylko upewnij się, że zawijasz element w tablicę.

role                  #=> #<Role id: 1, name: "1">

user.roles            #=> []

user.roles |= [role]  #=> [#<Role id: 1, name: "1">]

user.roles |= [role]  #=> [#<Role id: 1, name: "1">]

Może być również używany do dodawania wielu elementów, które mogą lub nie muszą być już obecne:

role1                         #=> #<Role id: 1, name: "1">
role2                         #=> #<Role id: 2, name: "2">

user.roles                    #=> [#<Role id: 1, name: "1">]

user.roles |= [role1, role2]  #=> [#<Role id: 1, name: "1">, #<Role id: 2, name: "2">]

user.roles |= [role1, role2]  #=> [#<Role id: 1, name: "1">, #<Role id: 2, name: "2">]

Znalazłem tę technikę na tej odpowiedzi StackOverflow .

 5
Author: Joshua Pinter,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-03-22 18:04:18

Możesz użyć kombinacji validates_uniqueness_of i overriding

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
 3
Author: Pete Campbell,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-03-16 19:01:15

Myślę, że właściwa Reguła walidacji jest w modelu users_roles join:

validates_uniqueness_of :user_id, :scope => [:role_id]
 2
Author: bdon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-08-22 06:49:13

Być może istnieje możliwość utworzenia reguły walidacji

validates_uniqueness_of :user_roles

Następnie złap wyjątek walidacji i kontynuuj z wdziękiem. Jednak wydaje się to bardzo chwiejne i jest bardzo nieeleganckie, jeśli w ogóle jest to możliwe.

 0
Author: Schrockwell,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-08-22 05:41:55

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
Author: Richard Jones,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-04-05 23:23:19

Wpadłem na to dzisiaj i skończyło się na #replace, które "wykona diff i usunie/doda tylko rekordy, które się zmieniły".

Dlatego musisz przekazać związek istniejących ról (aby nie zostały usunięte) i Twojej nowej roli(y):

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

Ważne jest, aby pamiętać, że zarówno ta odpowiedź, jak i zaakceptowana ładują powiązane obiekty roles do pamięci w celu wykonania tablicy diff (-) i union (|). Może to prowadzić do wydajności problemy, jeśli masz do czynienia z dużą liczbą powiązanych rekordów.

Jeśli jest to problemem, możesz najpierw przyjrzeć się opcjom, które sprawdzają istnienie za pomocą zapytań lub użyć zapytania typu INSERT ON DUPLICATE KEY UPDATE (mysql)do wstawiania.

 0
Author: Tyler Johnson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-04-25 18:16:27