Module: Sequel::Plugins::ThroughAssociations::ClassMethods

Defined in:
lib/sequel/plugins/through_associations.rb

Instance Method Summary collapse

Instance Method Details

#associate_through(type, name, opts, &block) ⇒ Object

Associates a related model with the current model using another association as the intermediary.



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
# File 'lib/sequel/plugins/through_associations.rb', line 44

def associate_through type, name, opts, &block

  unless assoc_type = Sequel.synchronize{ASSOCIATION_THROUGH_TYPES[type]}
    raise Error, "#{type} does not support through associations"
  end

  result = find_association_path(**opts, name: name, models: self, from_through: true)

  # Remove the last table if it matches the destination table
  dest_model = result[:models].pop
  result[:tables].pop if result[:tables].last == dest_model.table_name

  # Build the association path
  path = []
  left_key = result[:keys].shift
  result[:tables].each do |table|
    path.push [table, result[:keys].shift, result[:keys].shift]
  end

  # Create the association
  if assoc_type.to_s.end_with? "_through_many"
    # *_through_many has a path argument
    self.send(assoc_type,
      name,
      path,
      left_primary_key: left_key,
      right_primary_key: result[:keys].shift,
      class: dest_model,
      **opts,
      originally_through: opts[:through],
      &block
    )
  else
    # *_through_one does not have a path argument
    self.send(assoc_type,
      name,
      left_primary_key: left_key,
      right_primary_key: result[:keys].shift,
      class: dest_model,
      **opts,
      originally_through: opts[:through],
      &block
    )
  end
end

#find_association_path(**opts) ⇒ Object

Recurses through associations until a path to the destination is completed



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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/sequel/plugins/through_associations.rb', line 91

def find_association_path **opts

  # Initialize arguments
  [:tables, :keys, :through, :models, :assocs].each do |k|
    opts[k] ||= []
    opts[k] = [opts[k]] unless Array === opts[k]
    opts[k] = opts[k].dup
  end

  # Find the linked association
  assoc = \
    opts[:models].last.association_reflection(opts[:through].last.to_s.pluralize.to_sym) \
    || opts[:models].last.association_reflection(opts[:through].last.to_s.singularize.to_sym)

  # Short circuit if association does not exist
  unless assoc

    # Determine if finished or if the last relation is missing
    if opts[:from_through]

      m = opts[:models].pop
      t = opts[:through].pop
      path = [m]

      opts[:models].zip(opts[:through]).each do |model, through|
        path.push "#{model}.#{through}"
      end

      raise MissingAssociation, "#{m} is missing through association :#{t} from #{path.join " -> "}"

    else

      if opts[:assocs].last[:name].to_s.singularize != (opts[:using] || opts[:name]).to_s.singularize
        text = "#{opts[:models].first}.#{opts[:name]} could not be resolved through path #{opts[:models].zip(opts[:through]).map{|model, through| "#{model}.#{through}"}.join " -> "}"
        raise MissingAssociation, text
      end

      return opts

    end

  end

  # Store the association
  opts[:assocs].push assoc

  # Handle *_through_many associations
  if assoc[:type].to_s.end_with? "_through_many"

    opts[:through].push assoc[:originally_through]
    opts[:from_through] = true

    # Search through the existing model first, falling back to the associated model
    search = [
      opts[:models].last,
      assoc[:class] || assoc[:class_name].constantize
    ]
    return begin
      model = search.shift
      raise NoAssociationPath, opts unless model
      self.find_association_path(**opts, models: opts[:models] + [model])
    rescue MissingAssociation
      # Try the next model in the search path
      retry
    end

  end

  # Move to the new model
  opts[:models].push assoc[:class] || assoc[:class_name].constantize

  # Read the through association if present
  if assoc[:through]
    opts[:through].push assoc[:using] || assoc[:through]
    opts[:from_through] = true
    opts[:using] = nil
    return self.find_association_path(**opts)
  end

  # Otherwise, add the new table to the stack
  if opts[:from_through] && opts[:models].last.respond_to?(:cti_tables)
    opts[:tables].push opts[:models].last.cti_tables.first
  else
    opts[:tables].push opts[:models].last.table_name
  end

  # Left side
  case assoc[:type]
    when :one_to_many, :one_to_one # 1:_
      opts[:keys].push assoc.primary_key
    when :many_to_one # n:_
      opts[:keys].push assoc[:key]
    else
      raise
  end

  # Right side
  case assoc[:type]
    when :many_to_one, :one_to_one # _:1
      opts[:keys].push assoc.primary_key
    when :one_to_many # _:n
      opts[:keys].push assoc[:key]
    else
      raise
  end

  # Check for a source association
  opts[:through].push opts[:using] || opts[:name]
  opts[:from_through] = false
  return self.find_association_path(**opts)

end