Запретить повышение ActiveRecord ::RecordInvalid или добавление дважды в ассоциацию has_many

#ruby-on-rails #ruby #activerecord

#ruby-on-rails #ruby #activerecord

Вопрос:

Я хочу изменить поведение ассоциации has_many

учитывая эту базовую модель данных

 class Skill < ActiveRecord::Base
  has_many :users, through: :skills_users
  has_many :skills_users
end

class User < ActiveRecord::Base
  has_many :skills, through: :skills_users, validate: true
  has_many :skills_users
end

class SkillsUser < ActiveRecord::Base
  belongs_to :user
  belongs_to :skill

  validates :user, :skill, presence: true
end
  

Для добавления нового навыка мы можем легко это сделать :

 john = User.create(name: 'John Doe')
tidy = Skill.create(name: 'Tidy')

john.skills << tidy
  

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

Возможность предотвратить это — проверить перед добавлением

 john.skills << tidy unless john.skills.include?(tidy)
  

Но это довольно подло…

Мы также можем изменить ActiveRecord::Associations::CollectionProxy#<< поведение, подобное

 module InvalidModelIgnoredSilently
  def <<(*records)
    super(records.to_a.keep_if { |r| !!include?(r) })
  end
end 
ActiveRecord::Associations::CollectionProxy.send :prepend, InvalidModelIgnoredSilently
  

заставить CollectionProxy игнорировать прозрачное добавление повторяющихся записей.

Но я этим не доволен.

Мы можем добавить проверку при дополнительной проверке на SkillsUser

 class SkillsUser < ActiveRecord::Base
  belongs_to :user
  belongs_to :skill

  validates :user, :skill, presence: true
  validates :user, uniqueness: { scope: :skill }
end
  

но в этом случае добавление дважды приведет к увеличению ActiveRecord::RecordInvalid , и снова мы должны проверить перед добавлением

или сделайте более уродливый взлом на CollectionProxy

 module InvalidModelIgnoredSilently

  def <<(*records)
    super(valid_records(records))
  end

  private

  def valid_records(records)
    records.with_object([]).each do |record, _valid_records|
      begin
        proxy_association.dup.concat(record)
        _valid_records << record
      rescue ActiveRecord::RecordInvalid
      end
    end
  end
end
ActiveRecord::Associations::CollectionProxy.send :prepend, InvalidModelIgnoredSilently
  

Но я все еще этим недоволен.

Для меня идеальными и, возможно, отсутствующими методами в CollectionProxy являются :

 john.skills.push(tidy)
=> false
  

и

 john.skills.push!(tidy)
=> ActiveRecord::RecordInvalid
  

Есть идеи, как я могу это сделать красиво?

— РЕДАКТИРОВАТЬ —

Я нашел способ избежать исключения исключения!

 class User < ActiveRecord::Base
  has_many :skills, through: :skills_users, before_add: :check_presence
  has_many :skills_users

  private

  def check_presence(skill)
    raise ActiveRecord::Rollback if skills.include?(skill)
  end
end
  

Не основано ни на проверках, ни на общем решении, но может помочь…

Комментарии:

1. Вы пробовали has_many :skills, -> { uniq }, through: :skills_users ?

2. Работает, только если вы удалите проверку validates :user, uniqueness: { scope: :skill }

3. Основная проблема заключается в том, что это скрывает правду…. Записи записываются в базу данных, просто не передаются через эту специальную ОТДЕЛЬНУЮ область, это не то, что я хочу.

4. Вы пробовали удалять validate: true и использовать validates_associated :skills для пользователя?

5. Нет, потому что я использую его для пользователя, и вы не можете использовать его с обеих сторон ПРЕДУПРЕЖДЕНИЕ: эта проверка не должна использоваться на обоих концах ассоциации. Это приведет к циклической зависимости и вызовет бесконечную рекурсию.

Ответ №1:

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

  • Добавьте ограничение на уровне БД, чтобы убедиться, что данные чистые, независимо от того, как все реализовано
  • Убедитесь, что навык не добавляется несколько раз (на клиенте)

Комментарии:

1. Привет, у меня уже есть ограничение в базе данных, и это моя забота, потому что без проверки я получаю ActiveRecord::RecordNotUnique: PG::UniqueViolation: ERROR: duplicate key value violates unique constraint , поэтому вторая часть — это то, что я пытаюсь сделать более приятным способом, чем добавление базового условия, например john.skills << tidy unless john.skills.include?(tidy)

2. Я не думаю, что возня с ассоциациями приятнее или удобочитаемее для одноразовой проблемы.

Ответ №2:

Можете ли вы показать мне миграцию, которая создала вашу таблицу SkillsUser. тем лучше, если вы покажете мне индексы таблицы SkillsUser, которые у вас есть. я обычно использую has_and_belongs_to_many вместо has_many — through . попробуйте добавить эту миграцию

 $ rails g migration add_id_to_skills_users id:primary_key
# change the has_many - through TO has_and_belongs_to_many
  

нет необходимости в проверке, если у вас двойной индекс «skills_users».
надеюсь, это вам поможет.

Комментарии:

1. Это не то, что я хочу. Я хочу has_many отношение. Я знаю, что HBTM ведут себя по-другому