File: through_association.rb

package info (click to toggle)
rails 2%3A7.2.2.1%2Bdfsg-7
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 43,352 kB
  • sloc: ruby: 349,799; javascript: 30,703; yacc: 46; sql: 43; sh: 29; makefile: 27
file content (132 lines) | stat: -rw-r--r-- 4,868 bytes parent folder | download
1
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# frozen_string_literal: true

module ActiveRecord
  module Associations
    # = Active Record Through Association
    module ThroughAssociation # :nodoc:
      delegate :source_reflection, to: :reflection

      private
        def transaction(&block)
          through_reflection.klass.transaction(&block)
        end

        def through_reflection
          @through_reflection ||= begin
            refl = reflection.through_reflection

            while refl.through_reflection?
              refl = refl.through_reflection
            end

            refl
          end
        end

        def through_association
          @through_association ||= owner.association(through_reflection.name)
        end

        # We merge in these scopes for two reasons:
        #
        #   1. To get the default_scope conditions for any of the other reflections in the chain
        #   2. To get the type conditions for any STI models in the chain
        def target_scope
          scope = super
          reflection.chain.drop(1).each do |reflection|
            relation = reflection.klass.scope_for_association
            scope.merge!(
              relation.except(:select, :create_with, :includes, :preload, :eager_load, :joins, :left_outer_joins)
            )
          end
          scope
        end

        # Construct attributes for :through pointing to owner and associate. This is used by the
        # methods which create and delete records on the association.
        #
        # We only support indirectly modifying through associations which have a belongs_to source.
        # This is the "has_many :tags, through: :taggings" situation, where the join model
        # typically has a belongs_to on both side. In other words, associations which could also
        # be represented as has_and_belongs_to_many associations.
        #
        # We do not support creating/deleting records on the association where the source has
        # some other type, because this opens up a whole can of worms, and in basically any
        # situation it is more natural for the user to just create or modify their join records
        # directly as required.
        def construct_join_attributes(*records)
          ensure_mutable

          association_primary_key = source_reflection.association_primary_key(reflection.klass)

          if Array(association_primary_key) == reflection.klass.composite_query_constraints_list && !options[:source_type]
            join_attributes = { source_reflection.name => records }
          else
            assoc_pk_values = records.map { |record| record._read_attribute(association_primary_key) }
            join_attributes = { source_reflection.foreign_key => assoc_pk_values }
          end

          if options[:source_type]
            join_attributes[source_reflection.foreign_type] = [ options[:source_type] ]
          end

          if records.count == 1
            join_attributes.transform_values!(&:first)
          else
            join_attributes
          end
        end

        # Note: this does not capture all cases, for example it would be impractical
        # to try to properly support stale-checking for nested associations.
        def stale_state
          if through_reflection.belongs_to?
            Array(through_reflection.foreign_key).filter_map do |foreign_key_column|
              owner[foreign_key_column]
            end.presence
          end
        end

        def foreign_key_present?
          through_reflection.belongs_to? && Array(through_reflection.foreign_key).all? do |foreign_key_column|
            !owner[foreign_key_column].nil?
          end
        end

        def ensure_mutable
          unless source_reflection.belongs_to?
            if reflection.has_one?
              raise HasOneThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
            else
              raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
            end
          end
        end

        def ensure_not_nested
          if reflection.nested?
            if reflection.has_one?
              raise HasOneThroughNestedAssociationsAreReadonly.new(owner, reflection)
            else
              raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection)
            end
          end
        end

        def build_record(attributes)
          if source_reflection.collection?
            inverse = source_reflection.inverse_of
            target = through_association.target

            if inverse && target && !target.is_a?(Array)
              Array(target.id).zip(Array(inverse.foreign_key)).map do |primary_key_value, foreign_key_column|
                attributes[foreign_key_column] = primary_key_value
              end
            end
          end

          super
        end
    end
  end
end