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
|
module ActiveRecord
# = Active Record Has And Belongs To Many Association
module Associations
class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
attr_reader :join_table
def initialize(owner, reflection)
@join_table_name = reflection.options[:join_table]
@join_table = Arel::Table.new(@join_table_name)
super
end
def columns
@reflection.columns(@join_table_name, "#{@join_table_name} Columns")
end
def reset_column_information
@reflection.reset_column_information
end
def has_primary_key?
@has_primary_key ||= @owner.connection.supports_primary_key? && @owner.connection.primary_key(@join_table_name)
end
protected
def count_records
load_target.size
end
def insert_record(record, force = true, validate = true)
if record.new_record?
return false unless save_record(record, force, validate)
end
if @reflection.options[:insert_sql]
@owner.connection.insert(interpolate_sql(@reflection.options[:insert_sql], record))
else
relation = join_table
timestamps = record_timestamp_columns(record)
timezone = record.send(:current_time_from_proper_timezone) if timestamps.any?
attributes = columns.map do |column|
name = column.name
value = case name.to_s
when @reflection.foreign_key.to_s
@owner.id
when @reflection.association_foreign_key.to_s
record.id
when *timestamps
timezone
else
@owner.send(:quote_value, record[name], column) if record.has_attribute?(name)
end
[relation[name], value] unless value.nil?
end
stmt = relation.compile_insert Hash[attributes]
@owner.connection.insert stmt.to_sql
end
true
end
def delete_records(records)
if sql = @reflection.options[:delete_sql]
records.each { |record| @owner.connection.delete(interpolate_sql(sql, record)) }
else
relation = join_table
stmt = relation.where(relation[@reflection.foreign_key].eq(@owner.id).
and(relation[@reflection.association_foreign_key].in(records.map { |x| x.id }.compact))
).compile_delete
@owner.connection.delete stmt.to_sql
end
end
def construct_joins
right = join_table
left = @reflection.klass.arel_table
condition = left[@reflection.klass.primary_key].eq(
right[@reflection.association_foreign_key])
right.create_join(right, right.create_on(condition))
end
def construct_owner_conditions
super(join_table)
end
def association_scope
scope = super.joins(construct_joins)
scope = scope.readonly if ambiguous_select?(@reflection.options[:select])
scope
end
def select_value
super || [@reflection.klass.arel_table[Arel.star], join_table[Arel.star]]
end
# Join tables with additional columns on top of the two foreign keys must be considered
# ambiguous unless a select clause has been explicitly defined. Otherwise you can get
# broken records back, if, for example, the join column also has an id column. This will
# then overwrite the id column of the records coming back.
def ambiguous_select?(select)
extra_join_columns? && select.nil?
end
def extra_join_columns?
columns.size > 2
end
private
def record_timestamp_columns(record)
if record.record_timestamps
record.send(:all_timestamp_attributes).map { |x| x.to_s }
else
[]
end
end
def invertible_for?(record)
false
end
def find_by_sql(*args)
options = args.extract_options!
ambiguous = ambiguous_select?(@reflection.options[:select] || options[:select])
scoped.readonly(ambiguous).find(*(args << options))
end
end
end
end
|