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

Мне нужна работа

  • добавление уникального индекса в таблицу соединений
  • переопределить < метод в отношении

    has_and_belongs_to_many :groups do
      def << (group)
        group -= self if group.respond_to?(:to_a)
        super group unless include?(group)
      end
    end
    

Ответ 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