0.結論
ODP.NETのデフォルトではバインド変数は名前解決ではなく、宣言した順序に依存して解決されます。
名前解決するためにはOracleCommand.BindByName = True
を指定する必要があります。
はい。いきなり結論です。
・・・なんでそんなことするんだよ!
処理速度でメリットがあるんでしょうか???
さっぱり原因がわからなくて1日以上ハマったので書き残しておきます。
願わくば同じことでハマった人が解決できますように。
1.事象
こんなテーブルがあります。
CD NVARCHAR2(20) Primary Key
,VAL NUMBER(1)
で、こんな更新処理を実行します。
' SQL
_command.CommandText="UPDATE T SET VAL = :VAL WHERE CD = :CD"
' パラメータその1
dim p1 as DbParameter = _command.CreateParameter()
p1.DbType = DbType.String
p1.Value = "AX-0001"
p1.ParameterName = "CD"
_command.Parameters.Add(p1)
'パラメータその2
dim p2 as DbParameter = _command.CreateParameter()
p2.DbType = DbType.Decimal
p2.Value = 3
p2.ParameterName = "VAL"
_command.Parameters.Add(p2)
'実行
_command.ExecuteNonQuery() ->Exception発生!
このとき、数値の型変換エラーということでORA-01722が発生します。
次のSELECT処理は正常に実行され、期待通りのデータを取得できます。
' SQL
_command.CommandText="SELECT CD, VAL FROM T WHERE CD = :CD AND VAL = :VAL"
' パラメータその1
dim p1 as DbParameter = _command.CreateParameter()
p1.DbType = DbType.String
p1.Value = "AX-0001"
p1.ParameterName = "CD"
_command.Parameters.Add(p1)
'パラメータその2
dim p2 as DbParameter = _command.CreateParameter()
p2.DbType = DbType.Decimal
p2.Value = 3
p2.ParameterName = "VAL"
_command.Parameters.Add(p2)
_adapter.SelectCommand = _command
'実行
_command.ExecuteNonQuery() ' ->成功!
値も型も一見違いはなさそうなのですが何故なんでしょうか?
2.原因
冒頭に書きましたのでもうおわかりかと思いますが、updateの方はSQLに記載されるバインド変数の順序とCreateParameterで宣言されるバインド変数の順序が違っています。
これを正常に実行するためにはこちらも冒頭に書いたとおりOracleCommand.BindByName = True
を指定します。
If TypeOf _command Is OracleCommand Then
DirectCast(_command, OracleCommand).BindByName = True
End If
_command.CommandText="UPDATE T SET VAL = :VAL WHERE CD = :CD"
dim p1 as DbParameter = _command.CreateParameter()
p1.DbType = DbType.String
p1.Value = "AX-0001"
p1.ParameterName = "CD"
_command.Parameters.Add(p1)
dim p2 as DbParameter = _command.CreateParameter()
p2.DbType = DbType.Decimal
p2.Value = 3
p2.ParameterName = "VAL"
_command.Parameters.Add(p2)
_command.ExecuteNonQuery() ->Exception発生!
ここではSystem.Data.Common.DbProviderFactories
を使ってOracle以外のデータベースに変更した際の影響が抑えられるようにしていますので、If文でOracleの場合だけキャストしてBindByName=True
を指定するようにしています。
もちろん実際のコードでは_comandの生成、BindByNameの設定は共通部品内でやっていますが。
Oracle® Data Provider for .NET開発者ガイド - OracleCommandクラス - BindByName
デフォルト = false
次の条件の場合、BindByNameは無視されます。
・XmlCommandTypeプロパティの値が、Insert、UpdateまたはDeleteの場合
・XmlCommandTypeプロパティの値がQueryであるが、OracleCommandにパラメータが設定されていない場合
3.他のエラー
上述の原因・事象ですので、ORA-01722(数値が無効です)以外にも
- ORA-01861(リテラルが書式文字列と一致しません)
- ORA-06502(文字列バッファが小さすぎます)
- ORA-01438(この列に許容される指定精度より大きな値です)
などなどが発生し得ると思います。ハマってる人が辿りつけるようできるだけ書きたいのですがあまり思いつきません。。。
答えがわかってキーワード「BindByName」や「名前付きパラメータ」がわかれば公式含めていくらでも参考サイトがあるのですが、私自身がなかなか辿りつけなかったので他のキーワードから辿りつけるように意識して書いたつもりです。
4.(おまけ)他の原因
「SQLをsqlplusやその他のツールでそのまま実行するとちゃんと動くのにODP.NETで実行するとエラーになる!」
あるいは「結果が違う!」「条件がマッチしない!」という相談がままあります。
大抵はツールで実行するときはバインド変数を使っていないのが原因です。
バインド変数の型は適切ですか?
桁は適切ですか?
VARCHARは後続の空白も含めて判定しますが考慮していますか?(CHARの項目にVARCHARの変数を使っていませんか?)
どなたかの一助になれば幸いです。