Подтвердить что ты не робот

Rails идиома, чтобы избежать дублирования в has_many: через

У меня есть стандартное отношение "многие ко многим" между пользователями и ролями в приложении 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 сделает это, но в основном просто "select different".

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

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

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

Ответ 1

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

Ответ 2

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

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

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

user.roles            #=> []

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

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

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

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

Нашел этот прием на ответе Кару.

Ответ 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

Ответ 4

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

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

Ответ 5

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

validates_uniqueness_of :user_roles

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

Ответ 6

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

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

Ответ 7

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

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

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

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

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