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 

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

Что мне нужно сделать, чтобы избежать дубликатов (оснований на имени) в таблице тегов?

 lulalala14 нояб. 2012 г., 07:58
Вы имеете в виду дубликат в таблице соединений (как подсказывает заголовок) или таблице тегов?

Ответы на вопрос(12)

Prevent duplicates in the view only (Lazy solution)

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

In Rails 5:

has_and_belongs_to_many :tags, -> { distinct }

Note: Relation#uniq was depreciated in Rails 5 (совершить)

In Rails 4

has_and_belongs_to_many :tags, -> { uniq }
Prevent duplicate data from being saved (best solution)

Option 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

Option 2: Создать уникальный индекс

Самый надежный способpreventing duplicates должен иметь дублирующиеся ограничения на уровне базы данных. Это может быть достигнуто путем добавления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 ошибка. Обработка этого выходит за рамки этого вопроса. Посмотретьэтот ТАК вопрос.

rescue_from ActiveRecord::RecordNotUnique, :with => :some_method
 08 февр. 2017 г., 20:15
проголосовал за уникальный индекс базы данных
 16 мая 2018 г., 12:53
Я не переопределил бы никаких сеттеров, потому что такие вещи, скорее всего, причинят дополнительную боль в заднице вам или вашим сверстникам. Я бы создал отдельный метод для добавления связанного объекта, если он не был добавлен ранее. Другими словами имяtag= метод что-то вродеadd_unique_tag
 14 нояб. 2016 г., 13:07
Лучший ответ имхо. Вы делаете что-то не так, если у вас есть обманщики. Исправить проблему с уникальным индексом.

add :uniq to the has_and_belongs_to_many association adding unique index on the join table

Я бы сделал явную проверку, чтобы определить, существует ли связь. Например:

post = Post.find(1)
tag = Tag.find(2)
post.tags << tag unless post.tags.include?(tag)
 17 сент. 2013 г., 08:09
Чистый раствор. В вашемPost модель, добавитьdef tag=(tag); tags << tag unless tags.include?(tag); end для надежности.
 29 окт. 2014 г., 17:15
Кто-нибудь получил подтверждение для этого?
 17 июн. 2014 г., 22:54
Достаточно интересно,has_many association documentation (поиск включает?) рекомендует не использоватьunless ...include?() из-за условий гонки. Я предполагаю, что то же самое может иметь место для has_and_belongs_to_many.
 06 мая 2016 г., 23:43
какое-либо подтверждение по этому поводу?
 10 мая 2016 г., 01:24
@ Alfie Я использовал это в нескольких проектах на производстве и никогда не сталкивался с проблемой. Я не видел проблем, упомянутых в документах has_many, где-либо еще. Это не является окончательным, однако, только мой собственный опыт.

adding unique index on the join table

override << method in the relation

has_and_belongs_to_many :groups do
  def << (group)
    group -= self if group.respond_to?(:to_a)
    super group unless include?(group)
  end
end
 26 окт. 2016 г., 20:23
Работал хорошо для меня вместе с ограничением базы данных! Большое спасибо!

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
 27 дек. 2010 г., 23:14
Это не работает, как ожидалось. Вы по-прежнему получаете повторяющиеся ошибки ограничения при попытке добавить их через & lt; & lt ;.
 24 мар. 2011 г., 08:02
Я думал, что uniq игнорирует дубликаты при чтении, но все еще позволяет вам записывать дубликаты, которые вызовут исключение db, если у вас есть ограничение дублирования на уровне базы данных
 04 сент. 2014 г., 15:57
Это не предотвратит дублирование, оно просто скрывает их от показа
 28 дек. 2010 г., 12:05
Я только что попробовал (на Rails 2.3.5), и это не так. Вы должны задать свой собственный вопрос, в котором вы предоставляете достаточно информации для диагностики вашей проблемы.

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

Затем я обернул бы создание в логику, чтобы модели тегов использовались повторно, если они уже существуют. Возможно, я даже наложил уникальное ограничение на имя тега, чтобы применить его. Это делает поиск в любом случае более эффективным, поскольку вы можете просто использовать индексы в таблице соединений (чтобы найти все сообщения для определенного тега и все теги для определенного сообщения).

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

Решение Вопроса

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

Он может быть немного оптимизирован для работы с тегами, а также может потребоваться немного лучшая поддержка транзакций.

 Sam Saffron15 июл. 2009 г., 12:40
на самом деле, нетgithub.com/rails/rails/blob/… это проверка, которая будет исключать, если есть дублирование, она не обрабатывает ее прозрачно, у нее есть задокументированные проблемы параллелизма, и ее нужно использовать с уникальным индексом, чтобы гарантировать отсутствие ошибок.
 14 нояб. 2016 г., 13:08
Да, сделайте это так, а также добавьте уникальный индекс в базу данных. Позже вы можете использовать эту модель и добавлять в нее новые данные на случай, если вам потребуется хранить больше информации о каждой ассоциации.
 15 июл. 2009 г., 10:46
Это именно то, что делает валидатор validates_uniqueness_of. Однако одно только это решение не предотвращает дублирование элементов, поскольку между вашим find_by_name и ассоциацией другой запрос может записать в базу данных. Вы можете использовать это, но вы должны знать, что могут возникнуть некоторые исключения (потому что у вас есть индекс), и вы должны их перехватить.

:uniq вариант какописано в документации, Также обратите внимание, что:uniq Опции не предотвращают создание дублирующих отношений, а только гарантируют, что методы accessor / find выберут их один раз.

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

 Sam Saffron15 июл. 2009 г., 10:04
Я попробовал это, это действительно не решает проблему чисто. Я хочу избежать исключений в первую очередь. Я думаю, что мое решение как бы делает это, хотя и требует небольшой доработки.
 Sam Saffron15 июл. 2009 г., 10:05
Примечание: у меня уже было уникальное ограничение, которое приводило к исключениям, обрабатывать их - огромная боль.
 29 нояб. 2017 г., 02:39
последние документы рельсов, кажется, удалили этот метод
 31 июл. 2012 г., 01:08
В текущей версии will_paginate (3.0.3) наличие дубликатов в таблице соединений делает невозможным уникальное разбиение на страницы.

но я думал, что поделюсь своим способом сделать это.

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

Это приводит к автоматическому добавлению / удалению тегов по мере необходимости или к бездействию, если это так.

 19 нояб. 2017 г., 19:10
Еще один способ выразить последнюю строкуpost.tag_ids |= [new_tag.id] или жеpost.tags |= [new_tag], с помощьюArray's set-like operators

class Post < ActiveRecord::Base 
  has_and_belongs_to_many :tags, -> { uniq }

(будьте осторожны,-> { uniq } должен быть непосредственно после имени отношения, перед другими параметрами)

Rails документация

 23 февр. 2016 г., 21:21
-> {uniq} scope doesn't prevent duplicating data but helps displaying just one in the view.<,/span> – Francisco Quintero Feb 23 '16 at 20:21
 18 апр. 2014 г., 18:12
Ноafter_add обратный вызов будет повторяться каждый раз снова
 29 окт. 2014 г., 16:11
@nerith Если у меня есть другой & quot; has_and_belongs_to_many & quot; в другой модели, ссылающейся на первую модель, вы думаете, я должен установить там тоже {uniq}?
 06 февр. 2017 г., 17:06
Проголосовал, потому что это явноdoes not avoid duplicates in the table
 29 окт. 2014 г., 16:09
Не могли бы вы уточнить это? Вы хотите сказать, чтоafter_add будет работать даже в том случае, если запись не сохранена (благодаря {uniq})?

существует ли тег в вашей таблице тегов, а затем создайте его, если он не:

name = params[:tag][:name]
@new_tag = Tag.where(name: name).first_or_create

Затем проверьте, существует ли он в этой конкретной коллекции, и нажмите его, если это не так:

@taggable.tags << @new_tag unless @taggable.tags.exists?(@new_tag)

сли это так, ничего не делайте, если это не так, добавьте новый:

u = current_user
a = @article
if u.articles.exists?(a)

else
  u.articles << a
end

Подробнее: & quot; 4.4.1.14 collection.exists? (...) & quot; http://edgeguides.rubyonrails.org/association_basics.html#scopes-for-has-and-belongs-to-many

name и затем использовать метод find_or_create в методе Tags # create

документы

Ваш ответ на вопрос