has_and_belongs_to_many, избегая дублирования в таблице соединений - PullRequest
56 голосов
/ 15 июля 2009

У меня довольно простой набор моделей 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 

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

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

Ответы [ 12 ]

40 голосов
/ 05 сентября 2016

Предотвращение дублирования только в представлении (Ленивое решение)

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

В Rails 5:

has_and_belongs_to_many :tags, -> { distinct }

Примечание: Relation#uniq устарел в Rails 5 ( commit )

В рельсах 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. Обработка этого выходит за рамки этого вопроса. Посмотреть этот ТАК вопрос .

rescue_from ActiveRecord::RecordNotUnique, :with => :some_method
25 голосов
/ 01 сентября 2012

В дополнение к предложениям выше:

  1. добавить :uniq к ассоциации has_and_belongs_to_many
  2. добавление уникального индекса в таблицу соединений

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

post = Post.find(1)
tag = Tag.find(2)
post.tags << tag unless post.tags.include?(tag)
21 голосов
/ 19 февраля 2014

В Rails4:

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

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

Рельсовая документация

20 голосов
/ 15 июля 2009

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

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

13 голосов
/ 23 июня 2010

Установите уникальную опцию:

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
5 голосов
/ 11 августа 2009

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

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

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

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

4 голосов
/ 15 июля 2009

Я решил эту проблему, создав фильтр 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 голосов
/ 25 мая 2016

Для меня работа

  1. добавление уникального индекса в таблицу соединений
  2. переопределить << метод в отношении </p>

    has_and_belongs_to_many :groups do
      def << (group)
        group -= self if group.respond_to?(:to_a)
        super group unless include?(group)
      end
    end
    
2 голосов
/ 21 октября 2015

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

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

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

1 голос
/ 09 мая 2014

Извлеките имя тега для безопасности. Проверьте, существует ли тег в вашей таблице тегов, а затем создайте его, если его нет:

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

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

@taggable.tags << @new_tag unless @taggable.tags.exists?(@new_tag)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...