#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 ведут себя по-другому