Tom
Tom

Reputation: 334

Multiple Associations To Same Model With Has Many Through

I currently have these models:

class Base < ActiveRecord::Base
  has_many :base_tags
  has_many :tags, through: :base_tags
  has_many :primary_tags, through: :base_tags, class_name: 'Tag', source: :tag, conditions: ['base_tags.primary = ?', true]
  has_and_belongs_to_many :channels
end

class Tag < ActiveRecord::Base
  has_many :base_tags
  has_many :bases, through: :base_tags
end

class BaseTag < ActiveRecord::Base
  belongs_to :base
  belongs_to :tag
end

I'm struggling to implement the primary tags on the base model. Calling #primary_tags on a base instance retuns the correct records but whilst trying to create/update the record:

Base.create({tag_ids: [1,2], primary_tag_ids: [1]})

I'm running into the following error:

ActiveRecord::RecordNotUnique

Mysql2::Error: Duplicate entry '1-2' for key 'index_bases_tags_on_base_id_and_tag_id': INSERT INTO `base_tags` (`base_id`, `primary`, `tag_id`) VALUES (1, 0, 2)

ActiveRecord is trying to create the same association for the primary_tag_ids as the tag_ids when it should really be updating the relation and the primary attribute should be 1.

Is there any way of getting ActiveRecord to play nice? I imagine my has_many :primary_tags relation is incorrect.

Upvotes: 0

Views: 226

Answers (2)

Frederick Cheung
Frederick Cheung

Reputation: 84182

I think there are two problems.

Firstly, if you use the string form of conditions, then activerecord isn't able to parse that sql fragment and understand what attributes it should set.

Secondly, because this is a has many through, I think those conditions need to be on the join model association

With the model like so

class Base < ActiveRecord::Base
  has_many :base_tags
  has_many :primary_base_tags,  conditions: {primary:  true}, class_name: 'BaseTag'
  has_many :tags, through: :base_tags
  has_many :primary_tags, through: :primary_base_tags, class_name: 'Tag', source: :tag
end

Then activerecord sets the primary flag properly for me.

Upvotes: 2

Joe Kennedy
Joe Kennedy

Reputation: 9443

This solution might work for you. I also added an alternative version of accessing primary_tags, but since your version works, you probably don't need it.

class Base < ActiveRecord::Base
  has_many :base_tags
  has_many :tags, through: :base_tags
  has_and_belongs_to_many :channels

  def primary_tags
    self.tags.includes(:base_tags).where(base_tags: { primary: true } )
  end

  def primary_tags_ids=(ids)
    current_ids = self.base_tags.map(&:tag_id)
    ids.each do |id|
      if current_ids.include?(id)
        self.base_tags.select { |bt| bt.tag_id == id }.first.primary = true
      else
        self.base_tags.build( { tag_id: id, primary: true } )
      end
    end
  end
end

Upvotes: 0

Related Questions