has_and_belongs_to_many.rb 4.1 KB
Newer Older
1
module ActiveRecord::Associations::Builder
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
  class HABTM
    class JoinTableResolver
      KnownTable = Struct.new :join_table

      class KnownClass
        def initialize(rhs_class, lhs_class_name)
          @rhs_class      = rhs_class
          @lhs_class_name = lhs_class_name
          @join_table     = nil
        end

        def join_table
          @join_table ||= [@rhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_")
        end

        private
        def klass; @lhs_class_name.constantize; end
      end

      def self.build(rhs_class, name, options)
        if options[:join_table]
          KnownTable.new options[:join_table]
        else
          class_name = options.fetch(:class_name) {
            name.to_s.camelize.singularize
          }
          KnownClass.new rhs_class, class_name
        end
      end
    end

    attr_reader :lhs_model, :association_name, :options

    def initialize(association_name, lhs_model, options)
      @association_name = association_name
      @lhs_model = lhs_model
      @options = options
    end

    def through_model
      habtm = JoinTableResolver.build lhs_model, association_name, options

      join_model = Class.new(ActiveRecord::Base) {
        class << self;
          attr_accessor :class_resolver
          attr_accessor :name
          attr_accessor :table_name_resolver
49 50
          attr_accessor :left_reflection
          attr_accessor :right_reflection
51 52 53 54 55 56 57 58 59 60 61 62
        end

        def self.table_name
          table_name_resolver.join_table
        end

        def self.compute_type(class_name)
          class_resolver.compute_type class_name
        end

        def self.add_left_association(name, options)
          belongs_to name, options
63
          self.left_reflection = reflect_on_association(name)
64 65 66 67 68
        end

        def self.add_right_association(name, options)
          rhs_name = name.to_s.singularize.to_sym
          belongs_to rhs_name, options
69
          self.right_reflection = reflect_on_association(rhs_name)
70 71 72 73 74 75 76 77 78 79 80 81 82
        end

      }

      join_model.name                = "HABTM_#{association_name.to_s.camelize}"
      join_model.table_name_resolver = habtm
      join_model.class_resolver      = lhs_model

      join_model.add_left_association :left_side, class: lhs_model
      join_model.add_right_association association_name, belongs_to_options(options)
      join_model
    end

83 84 85 86 87 88 89 90 91 92 93 94 95
    def middle_reflection(join_model)
      middle_name = [lhs_model.name.downcase.pluralize,
                     association_name].join('_').gsub(/::/, '_').to_sym
      middle_options = middle_options join_model
      hm_builder = HasMany.create_builder(lhs_model,
                                          middle_name,
                                          nil,
                                          middle_options)
      hm_builder.build lhs_model
    end

    private

96 97 98
    def middle_options(join_model)
      middle_options = {}
      middle_options[:class] = join_model
99
      middle_options[:source] = join_model.left_reflection.name
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
      if options.key? :foreign_key
        middle_options[:foreign_key] = options[:foreign_key]
      end
      middle_options
    end

    def belongs_to_options(options)
      rhs_options = {}

      if options.key? :class_name
        rhs_options[:foreign_key] = options[:class_name].foreign_key
        rhs_options[:class_name] = options[:class_name]
      end

      if options.key? :association_foreign_key
        rhs_options[:foreign_key] = options[:association_foreign_key]
      end

      rhs_options
    end
  end

122
  class HasAndBelongsToMany < CollectionAssociation #:nodoc:
123 124 125
    def macro
      :has_and_belongs_to_many
    end
126

127
    def valid_options
128
      super + [:join_table, :association_foreign_key]
129
    end
130

131
    def self.define_callbacks(model, reflection)
132
      super
A
Aaron Patterson 已提交
133
      name = reflection.name
J
Jon Leighton 已提交
134 135 136
      model.send(:include, Module.new {
        class_eval <<-RUBY, __FILE__, __LINE__ + 1
          def destroy_associations
137
            association(:#{name}).delete_all
J
Jon Leighton 已提交
138 139 140 141 142
            super
          end
        RUBY
      })
    end
143 144
  end
end