问题
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'搞定!