October 21st, 2:18pm 0 comments

Problem with has_many :through

I recently ran into a problem using has_many :through relationships. The edge code works fine when using standard id’s but for those using legacy databases or non-standard id’s in your join table the code fails when trying to add or delete an association.


Something like this would fail:


create_table :books, :force => true do |t|  t.column :name, :stringendcreate_table :citations, :id => false, :force => true do |t|  t.column :book1_id, :integer  t.column :book2_id, :integerendclass Book < ActiveRecord::Base  has_many :citations, :foreign_key => 'book1_id'  has_many :references, :through => :citations, :source => :reference_of, :uniq => trueendclass Citation < ActiveRecord::Base  belongs_to :reference_of, :class_name => "Book", :foreign_key => :book2_id  belongs_to :book1, :class_name => "Book", :foreign_key => :book1_id  belongs_to :book2, :class_name => "Book", :foreign_key => :book2_idendawdr = Book.create!(:name => "Agile Web Development with Rails")rfr = Book.create!(:name => "Ruby for Rails")awdr.references << rfrawdr.delete(rfr)

There’s further information at http://dev.rubyonrails.org/ticket/6466


If you’re running into this problem you can patch your local version of rails. First freeze edge in your tree. Then create the file has_many_through_patch.rb in your lib directory with the following code:


module ActiveRecord  class HasManyThroughCantDisassociateNewRecords < ActiveRecordError #:nodoc:    def initialize(owner, reflection)      super("Cannot disassociate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to delete the has_many :through record associating them.")    end  end  module Associations    class HasManyThroughAssociation      # Construct attributes for :through pointing to owner and associate.      def construct_join_attributes(associate)        construct_owner_attributes(@reflection.through_reflection).merge(@reflection.source_reflection.primary_key_name => associate.id)      end      # Remove +records+ from this association.  Does not destroy +records+.      def delete(*records)         return if records.empty?         records.each { |associate| raise_on_type_mismatch(associate) }         through = @reflection.through_reflection         raise ActiveRecord::HasManyThroughCantDisassociateNewRecords.new(@owner, through) if @owner.new_record?         load_target         klass = through.klass         klass.transaction do           flatten_deeper(records).each do |associate|             raise_on_type_mismatch(associate)             raise ActiveRecord::HasManyThroughCantDisassociateNewRecords.new(@owner, through) unless associate.respond_to?(:new_record?) && !associate.new_record?             @owner.send(@reflection.through_reflection.name).proxy_target.delete(klass.delete_all(construct_join_attributes(associate)))             @target.delete(associate)           end         end         self       end    end  endend

Then in your environment.rb add the following:


require 'has_many_through_patch'

You should be able to add and delete now until the patch is committed.

Filed under has_many patch rails ruby
Posted