問題
Gem版本:
rails 4.2.5
activerecord-oracle_enhanced-adapter 1.6.6
兩個模型:
class ProjectTable < ActiveRecord::Base
establish_connection :projects
...
end
class TrialApplication < ProjectTable
self.table_name = 'trial_applications'
end
BUG:
TrialApplication.create的時候,ID本來應該是取自序列trial_application_seq.nextval,但是發現完全不對,創建數據的ID與已有的ID發生沖突,引發ORA-00001報錯!
追蹤
初步懷疑
因為在Rails中,Oracle和Mysql有點不同,Mysql的主鍵ID是自增長的,而Oracle一般都是通過序列來獲取,一開始就懷疑新版的activerecord-oracle_enhanced-adapter 是否有調整,造成通過序列獲取ID出現了問題。于是開始追蹤activerecord-oracle_enhanced-adapter 的源代碼。
開始追蹤
于是我通過 sequence 這個關鍵詞在activerecord-oracle_enhanced-adapter 的源代碼中進行搜索,查到了這個方法(activerecord-oracle_enhanced-adapter-1.6.6/lib/active_record/connection_adapters/oracle_enhanced/database_statements.rb):
# Executes an INSERT statement and returns the new record's ID
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
# if primary key value is already prefetched from sequence
# or if there is no primary key
if id_value || pk.nil?
execute(sql, name)
return id_value
end
sql_with_returning = sql + @connection.returning_clause(quote_column_name(pk))
log(sql, name) do
@connection.exec_with_returning(sql_with_returning)
end
end
# New method in ActiveRecord 3.1
# Will add RETURNING clause in case of trigger generated primary keys
def sql_for_insert(sql, pk, id_value, sequence_name, binds)
unless id_value || pk.nil? || (defined?(CompositePrimaryKeys) && pk.kind_of?(CompositePrimaryKeys::CompositeKeys))
sql = "#{sql} RETURNING #{quote_column_name(pk)} INTO :returning_id"
returning_id_col = new_column("returning_id", nil, Type::Value.new, "number", true, "dual", true, true)
(binds = binds.dup) << [returning_id_col, nil]
end
[sql, binds]
end
這兩個方法很明顯就是生成insert的sql的方法,應該就是倒推過來最接近數據庫操作的步驟,于是我在兩個方法中加上了輸出:
puts [sql, id_value, sequence_name]
然后返回控制臺中執行TrialApplication.create操作,結果返回的是:
["INSERT INTO \"TRIAL_APPLICATIONS\" (...)") VALUES (:a1, :a2, :a3, :a4, :a5, :a6, :a7)", 123456, nil]
很明顯,這個時候id_value早就已經取好值了,很明顯,還要繼續追溯這個id_value是在哪取出來的。
繼續深挖
activerecord-oracle_enhanced-adapter 的源代碼挖了一遍,沒有收獲,于是開始搜索active_record,還是用sequence關鍵詞,發現這個方法(activerecord-4.2.5/lib/active_record/connection_adapters/abstract/database_statements.rb ):
# Returns the last auto-generated ID from the affected table.
#
# +id_value+ will be returned unless the value is nil, in
# which case the database will attempt to calculate the last inserted
# id and return that value.
#
# If the next id was calculated in advance (as in Oracle), it should be
# passed in as +id_value+.
def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
puts 'insert' #這個就是我加入的調試的代碼了
puts [name, pk, id_value, sequence_name, binds].inspect
sql, binds = sql_for_insert(to_sql(arel, binds), pk, id_value, sequence_name, binds)
value = exec_insert(sql, name, binds, pk, sequence_name)
id_value || last_inserted_id(value)
end
注釋里面寫的很清楚了,If the next id was calculated in advance (as in Oracle),在Oracle中,id是會預先計算出來的了,加上調試的代碼,驗證,果然在這一步id_value也早就計算出來,還是錯誤的!
最后的希望
繼續檢索,終于找到了這個方法(activerecord-4.2.5/lib/active_record/relation.rb),
def insert(values) # :nodoc:
primary_key_value = nil
if primary_key && Hash === values
primary_key_value = values[values.keys.find { |k|
k.name == primary_key
}]
if !primary_key_value && connection.prefetch_primary_key?(klass.table_name)
puts klass.sequence_name
primary_key_value = connection.next_sequence_value(klass.sequence_name)
puts "primary_key_value=#{primary_key_value}"
values[klass.arel_table[klass.primary_key]] = primary_key_value
end
end
....
connection.next_sequence_value(klass.sequence_name),獲取序列的下一個值的方法就是它了,說明id_value就是在這里取出來的。
加上調試的輸出,結果一看:
klass.sequence_name居然不是trial_applications_seq,而是employees_seq!問題終于找到了!
解決
原來在ProjectTable中,新版的activerecord會預先加載模型對應的table的信息,避免報錯我加上了self.table_name = 'employees',指定了一個table。
而TrialApplication,就悲催的延續了ProjectTable的設置,雖然self.table_name = 'trial_applications'修改了表名,序列sequence_name沒有修改!解決起來就很簡單了:
class TrialApplication < ProjectTable
self.table_name = 'trial_applications'
self.sequence_name = 'trial_applications_seq'
end
加上 self.sequence_name = 'trial_applications_seq'搞定!