Rails идиома, чтобы избежать дубликатов в has_many: through - PullRequest
35 голосов
/ 22 августа 2009

У меня стандартное отношение «многие ко многим» между пользователями и ролями в моем приложении Rails:

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}.

Я понимаю, что могу сделать это так:

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

или путем создания метода-оболочки (например, add_to_roles(role)), но я надеялся на какой-то идиоматический способ сделать его автоматическим с помощью ассоциации, чтобы я мог написать:

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

и это просто работает для меня. Таким образом, мне не нужно проверять наличие дуплекса или использовать собственный метод. Есть ли что-то в рамках, что я пропускаю? Сначала я подумал, что это сделает опция: uniq для has_many, но в основном это просто «выбрать отличное».

Есть ли способ сделать это декларативно? Если нет, может быть, с помощью расширения ассоциации?

Вот пример того, как поведение по умолчанию терпит неудачу:

    >> <strong>u = User.create</strong>
      User Create (0.6ms)   INSERT INTO "users" ("name") VALUES(NULL)
    => #<User id: 3, name: nil>
    >> <strong>u.roles << Role.first</strong>
      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">]
    >> <strong>u.roles << Role.first</strong>
      Role Load (0.4ms)   SELECT * FROM "roles" LIMIT 1
      UserRole Create (0.5ms)   INSERT INTO "user_roles" ("role_id", "user_id") VALUES(1, 3)
    <strong><em>=> [#<Role id: 1, name: "1">, #<Role id: 1, name: "1">]</em></strong>

Ответы [ 7 ]

23 голосов
/ 22 августа 2009

Пока добавленная роль является объектом 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

если super не работает, вам может потребоваться настроить alias_method_chain.

9 голосов
/ 11 марта 2018

Используйте метод массива |= Метод соединения.

Вы можете использовать метод массива |= для добавления элементаМассив, если он уже не присутствует.Просто убедитесь, что вы обернули элемент в массив.

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

user.roles            #=> []

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

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

Может также использоваться для добавления нескольких элементов, которые могут присутствовать или не присутствовать: этот ответ StackOverflow .

3 голосов
/ 16 марта 2011

Вы можете использовать комбинацию validates_uniqueness_of и overriding << в основной модели, хотя это также перехватит любые другие ошибки проверки в модели соединения. </p>

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
2 голосов
/ 22 августа 2009

Я думаю, что правильное правило проверки есть в вашей модели присоединения users_roles:

validates_uniqueness_of :user_id, :scope => [:role_id]
0 голосов
/ 25 апреля 2017

Я столкнулся с этим сегодня и в итоге использовал # replace , который "выполнит сравнение и удалит / добавит только записи, которые изменились".

Следовательно, вам нужно передать объединение существующих ролей (чтобы они не удалялись) и вашей новой роли:

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

Важно отметить, что и этот, и принятый ответы загружают связанные объекты roles в память для выполнения различий в массиве (-) и объединения (|). Это может привести к проблемам с производительностью, если вы имеете дело с большим количеством связанных записей.

Если это вызывает озабоченность, вы можете сначала посмотреть опции, которые проверяют существование с помощью запросов, или использовать запрос типа INSERT ON DUPLICATE KEY UPDATE (mysql) для вставки.

0 голосов
/ 06 апреля 2017

Я думаю, вы хотите сделать что-то вроде:

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 голосов
/ 22 августа 2009

Возможно, возможно создать правило проверки

validates_uniqueness_of :user_roles

затем поймайте исключительную ситуацию и продолжайте грациозно. Тем не менее, это кажется действительно хакерским и очень не элегантным, если вообще возможно.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...