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
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
|
module ThinkingSphinx
# Association tracks a specific reflection and join to reference data that
# isn't in the base model. Very much an internal class for Thinking Sphinx -
# perhaps because I feel it's not as strong (or simple) as most of the rest.
#
class Association
attr_accessor :parent, :reflection, :join
# Create a new association by passing in the parent association, and the
# corresponding reflection instance. If there is no parent, pass in nil.
#
# top = Association.new nil, top_reflection
# child = Association.new top, child_reflection
#
def initialize(parent, reflection)
@parent, @reflection = parent, reflection
@children = {}
end
# Get the children associations for a given association name. The only time
# that there'll actually be more than one association is when the
# relationship is polymorphic. To keep things simple though, it will always
# be an Array that gets returned (an empty one if no matches).
#
# # where pages is an association on the class tied to the reflection.
# association.children(:pages)
#
def children(assoc)
@children[assoc] ||= Association.children(@reflection.klass, assoc, self)
end
# Get the children associations for a given class, association name and
# parent association. Much like the instance method of the same name, it
# will return an empty array if no associations have the name, and only
# have multiple association instances if the underlying relationship is
# polymorphic.
#
# Association.children(User, :pages, user_association)
#
def self.children(klass, assoc, parent=nil)
ref = klass.reflect_on_association(assoc)
return [] if ref.nil?
return [Association.new(parent, ref)] unless ref.options[:polymorphic]
# association is polymorphic - create associations for each
# non-polymorphic reflection.
polymorphic_classes(ref).collect { |klass|
Association.new parent, ::ActiveRecord::Reflection::AssociationReflection.new(
ref.macro,
"#{ref.name}_#{klass.name}".to_sym,
casted_options(klass, ref),
ref.active_record
)
}
end
# Link up the join for this model from a base join - and set parent
# associations' joins recursively.
#
def join_to(base_join)
parent.join_to(base_join) if parent && parent.join.nil?
@join ||= ::ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation.new(
@reflection, base_join, parent ? parent.join : base_join.joins.first
)
end
# Returns the association's join SQL statements - and it replaces
# ::ts_join_alias:: with the aliased table name so the generated reflection
# join conditions avoid column name collisions.
#
def to_sql
@join.association_join.gsub(/::ts_join_alias::/,
"#{@reflection.klass.connection.quote_table_name(@join.parent.aliased_table_name)}"
)
end
# Returns true if the association - or a parent - is a has_many or
# has_and_belongs_to_many.
#
def is_many?
case @reflection.macro
when :has_many, :has_and_belongs_to_many
true
else
@parent ? @parent.is_many? : false
end
end
# Returns an array of all the associations that lead to this one - starting
# with the top level all the way to the current association object.
#
def ancestors
(parent ? parent.ancestors : []) << self
end
def has_column?(column)
@reflection.klass.column_names.include?(column.to_s)
end
def primary_key_from_reflection
if @reflection.options[:through]
@reflection.source_reflection.options[:foreign_key] ||
@reflection.source_reflection.primary_key_name
else
nil
end
end
def table
if @reflection.options[:through]
@join.aliased_join_table_name
else
@join.aliased_table_name
end
end
private
# Returns all the objects that could be currently instantiated from a
# polymorphic association. This is pretty damn fast if there's an index on
# the foreign type column - but if there isn't, it can take a while if you
# have a lot of data.
#
def self.polymorphic_classes(ref)
ref.active_record.connection.select_all(
"SELECT DISTINCT #{ref.options[:foreign_type]} " +
"FROM #{ref.active_record.table_name} " +
"WHERE #{ref.options[:foreign_type]} IS NOT NULL"
).collect { |row|
row[ref.options[:foreign_type]].constantize
}
end
# Returns a new set of options for an association that mimics an existing
# polymorphic relationship for a specific class. It adds a condition to
# filter by the appropriate object.
#
def self.casted_options(klass, ref)
options = ref.options.clone
options[:polymorphic] = nil
options[:class_name] = klass.name
options[:foreign_key] ||= "#{ref.name}_id"
quoted_foreign_type = klass.connection.quote_column_name ref.options[:foreign_type]
case options[:conditions]
when nil
options[:conditions] = "::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
when Array
options[:conditions] << "::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
when Hash
options[:conditions].merge!(ref.options[:foreign_type] => klass.name)
else
options[:conditions] << " AND ::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
end
options
end
end
end
|