Has_and_belongs_to_many, избегая обмана в таблице соединений
У меня довольно простой набор моделей HABTM
class Tag < ActiveRecord::Base
has_and_belongs_to_many :posts
end
class Post < ActiveRecord::Base
has_and_belongs_to_many :tags
def tags= (tag_list)
self.tags.clear
tag_list.strip.split(' ').each do
self.tags.build(:name => tag)
end
end
end
Теперь все работает хорошо, за исключением того, что я получаю тонну дубликатов в таблице тегов.
Что мне нужно сделать, чтобы избежать дублирования (базы по имени) в таблице тегов?
Ответы
Ответ 1
Я работал над этим, создав фильтр before_save, который исправляет все.
class Post < ActiveRecord::Base
has_and_belongs_to_many :tags
before_save :fix_tags
def tag_list= (tag_list)
self.tags.clear
tag_list.strip.split(' ').each do
self.tags.build(:name => tag)
end
end
def fix_tags
if self.tags.loaded?
new_tags = []
self.tags.each do |tag|
if existing = Tag.find_by_name(tag.name)
new_tags << existing
else
new_tags << tag
end
end
self.tags = new_tags
end
end
end
Он может быть немного оптимизирован для работы в пакетах с тегами, а также может потребоваться немного лучше транзакционная поддержка.
Ответ 2
Кроме того, предложения выше:
- добавить
:uniq
в ассоциацию has_and_belongs_to_many
- добавление уникального индекса в таблицу соединений
Я бы сделал явную проверку, чтобы определить, существует ли связь уже. Например:
post = Post.find(1)
tag = Tag.find(2)
post.tags << tag unless post.tags.include?(tag)
Ответ 3
Предотвращение дублирования только в представлении (Lazy solution)
Следующий не позволяет не дублировать отношения с базой данных, а только гарантирует, что методы find
игнорируют дубликаты.
В Rails 5:
has_and_belongs_to_many :tags, -> { distinct }
Примечание: Relation#uniq
был обесценен в Rails 5 (commit)
В Rails 4
has_and_belongs_to_many :tags, -> { uniq }
Предотвращение сохранения повторяющихся данных (лучшее решение)
Вариант 1: Предотвращение дублирования с контроллера:
post.tags << tag unless post.tags.include?(tag)
Тем не менее, несколько пользователей могут попробовать post.tags.include?(tag)
одновременно, поэтому это зависит от условий гонки. Здесь обсуждается здесь.
Для надежности вы можете также добавить это в модель Post (post.rb)
def tag=(tag)
tags << tag unless tags.include?(tag)
end
Вариант 2: Создать уникальный индекс
Самый надежный способ предотвращения дублирования - иметь дублирующие ограничения на уровне базы данных. Этого можно достичь, добавив unique index
в таблицу.
rails g migration add_index_to_posts
# migration file
add_index :posts_tags, [:post_id, :tag_id], :unique => true
add_index :posts_tags, :tag_id
После того, как у вас есть уникальный индекс, попытка добавить дублируемую запись вызовет ошибку ActiveRecord::RecordNotUnique
. Обработка этого вопроса выходит за рамки этого вопроса. Просмотрите этот вопрос SO.
rescue_from ActiveRecord::RecordNotUnique, :with => :some_method
Ответ 4
В Rails4:
class Post < ActiveRecord::Base
has_and_belongs_to_many :tags, -> { uniq }
(будьте осторожны, -> { uniq }
должен быть непосредственно после имени отношения, перед другими параметрами)
Документация Rails
Ответ 5
Вы можете передать опцию :uniq
как описанную в документации. Также обратите внимание, что параметры :uniq
не препятствуют созданию повторяющихся отношений, но только обеспечивают, чтобы методы accessor/find выбирали их один раз.
Если вы хотите предотвратить дублирование в таблице ассоциаций, вы должны создать уникальный индекс и обработать исключение. Также validates_uniqueness_of работает не так, как ожидалось, потому что вы можете попасть в случай, когда второй запрос записывается в базу данных между моментом, когда первый запрос проверяет дубликаты и записывает в базу данных.
Ответ 6
Задайте опцию uniq:
class Tag < ActiveRecord::Base
has_and_belongs_to_many :posts , :uniq => true
end
class Post < ActiveRecord::Base
has_and_belongs_to_many :tags , :uniq => true
Ответ 7
Я бы предпочел настроить модель и создать классы следующим образом:
class Tag < ActiveRecord::Base
has_many :taggings
has_many :posts, :through => :taggings
end
class Post < ActiveRecord::Base
has_many :taggings
has_many :tags, :through => :taggings
end
class Tagging < ActiveRecord::Base
belongs_to :tag
belongs_to :post
end
Затем я бы завернул создание в логике, чтобы модели Tag были повторно использованы, если они уже существовали. Я бы даже поставил уникальное ограничение на имя тега, чтобы обеспечить его соблюдение. Это делает более эффективным поиск в любом случае, поскольку вы можете просто использовать индексы в таблице соединений (чтобы найти все сообщения для определенного тега и все теги для определенного сообщения).
Единственное, что вы не можете позволить переименовать теги, поскольку изменение имени тега повлияет на все использования этого тега. Попросите пользователя удалить тег и создать новый.
Ответ 8
Мне нужна работа
Ответ 9
Извлечь имя тега для обеспечения безопасности. Проверьте, существует или нет тег в таблице тэгов, а затем создайте его, если это не так:
name = params[:tag][:name]
@new_tag = Tag.where(name: name).first_or_create
Затем проверьте, существует ли она в этой конкретной коллекции, и нажмите ее, если это не так:
@taggable.tags << @new_tag unless @taggable.tags.exists?(@new_tag)
Ответ 10
Это действительно старый, но я думал, что поделюсь этим путем.
class Tag < ActiveRecord::Base
has_and_belongs_to_many :posts
end
class Post < ActiveRecord::Base
has_and_belongs_to_many :tags
end
В коде, где мне нужно добавлять теги к сообщению, я делаю что-то вроде:
new_tag = Tag.find_by(name: 'cool')
post.tag_ids = (post.tag_ids + [new_tag.id]).uniq
Это приводит к автоматическому добавлению/удалению тегов по мере необходимости или ничего не делать, если в этом случае.
Ответ 11
Вы должны добавить индекс свойства tag: name, а затем использовать метод find_or_create в методе создания тегов
docs
Ответ 12
Просто добавьте проверку своего контроллера перед добавлением записи. Если это так, ничего не делайте, если нет, добавьте новый:
u = current_user
a = @article
if u.articles.exists?(a)
else
u.articles << a
end
Подробнее: "4.4.1.14 collection.exists? (...)"
http://edgeguides.rubyonrails.org/association_basics.html#scopes-for-has-and-belongs-to-many