とある戦国時代の武将がこう言ってました。
鳴かぬのなら、殺してしまおう鳴かせて見せよう、とほほぎす
色々と引用文に間違いがありますが、前回と前々回の続きです。
まず、他のバージョンの組み合わせに関してですが、前回のコメントでやじゅさんがおっしゃった通り、どうもODP.NET 10.2でも発生するみたいです。
Merge文使われてないのかな~なんて思いつつも、Oracleの対応を待つのも何なので、回避策を書いておこうと思います。
回避策:大人しくSystem.Data.OracleClientを使用する
半分冗談です^^
次が本当の回避策です。
本当の回避策:無名ブロックとRAISE_APPLICATION_ERRORを使用する
無名ブロックというのは、ストアドされないただのプロシージャで、BEGIN~ENDで囲まれているPL/SQLです。
考え方として、次のような感じになります。
- 無名ブロック内でMerge文を発行
- SQL%ROWCOUNTが0であれば、楽観ロック発生としRAISE_APPLICATION_ERRORで例外を挙げる
最後にある「RAISE_APPLICATION_ERROR」は、開発者が任意のSQLCODE(いわゆるORA-XXXXXのXXXXX部分)と任意のメッセージを返すことができるものです。
RAISE_APPLICATION_ERRORについては「SHIFT the Oracle」内の「RAISE_APPLICATION_ERROR」に分かり易く解説されています。
ということで、SQL
BEGIN
--MERGE実行
MERGE INTO MergeTable Target
USING (SELECT :key1 Key1, :col1 Col1, :col2 Col2, :optLock OptLock
FROM DUAL) InputSrc
ON (Target.Key1 = InputSrc.Key1)
WHEN MATCHED THEN
UPDATE SET
Col1 = InputSrc.Col1
, Col2 = InputSrc.Col2
WHERE Target.OptLock = InputSrc.OptLock
WHEN NOT MATCHED THEN
INSERT (Key1, Col1, Col2, OptLock)
VALUES (InputSrc.Key1, InputSrc.Col1, InputSrc.Col2, systimestamp);
--件数チェック(楽観ロックに引っかかった場合は0)
IF SQL%ROWCOUNT = 0 THEN
RAISE_APPLICATION_ERROR(-20801, '楽観的過ぎて更新できませんでした><');
END IF;
END;
18行目の-20801ですが、この部分がOracleException.Numberに入ってきます。ただし、.NET側では正の数で入ってくるので、OracleException.Numberが20801のときに、OptimisticConcurrencyExceptionにラップするなりなんなりして下さい。
ちなみに、ORA-20000~ORA-20999がユーザ定義のSQLCODEとしてOracleが許可している範囲ですが、他のOracleのアプリケーションでも使用されています。実際、ORA-20000あたりがOracleTextを使用していたときに発生した記憶があります。
SHIFT the Oracleの説明にもありますが、ORA-20000~ORA-20999がユーザ定義のSQLCODEとして許可されているものの、被らないという保証は一切ありません。なので、仕事で使用する場合はMessageの部分に独自のエラーコードを付与し、.NET側ではOracleException.Numberと共にOracleException.Messageも同様に調べ、適切に処理する方が良いと思います。
コード
const string sql
= @"BEGIN
--MERGE実行
MERGE INTO MergeTable Target
USING (SELECT :key1 Key1, :col1 Col1, :col2 Col2, :optLock OptLock
FROM DUAL) InputSrc
ON (Target.Key1 = InputSrc.Key1)
WHEN MATCHED THEN
UPDATE SET
Col1 = InputSrc.Col1
, Col2 = InputSrc.Col2
WHERE Target.OptLock = InputSrc.OptLock
WHEN NOT MATCHED THEN
INSERT (Key1, Col1, Col2, OptLock)
VALUES (InputSrc.Key1, InputSrc.Col1, InputSrc.Col2, systimestamp);
--件数チェック(楽観ロックに引っかかった場合は0)
IF SQL%ROWCOUNT = 0 THEN
RAISE_APPLICATION_ERROR(-20801, 'ERR-001:楽観的過ぎて更新できませんでした><');
END IF;
END;";
private void ExecuteMergeSql()
{
OracleConnection conn = new OracleConnection("user id=xxx;password=yyy;data source=zzz");
try
{
conn.Open();
OracleCommand command = conn.CreateCommand();
command.BindByName = true;
command.CommandText = sql.Replace("\r", ""); //CRLFをLFだけに修正
command.Parameters.Add(new OracleParameter("key1", 4));
command.Parameters.Add(new OracleParameter("col1", "aaa"));
command.Parameters.Add(new OracleParameter("col2", "bbb"));
command.Parameters.Add(new OracleParameter("optLock", DateTime.Now));
command.ExecuteNonQuery();
}
catch (OracleException oex)
{
if (oex.Number == 20801)
{
// ここで楽観ロックエラーを処理
}
else
{
throw;
}
}
finally
{
conn.Close();
}
}
最後に、Oracleで無名ブロックを使用する場合の注意点ですが、環境によってBEGIN~ENDの間の改行コードをLFだけにしなければならない場合があります。(31行目の部分)
これを行わないと、次のようなエラーが表示されます。
Oracle.DataAccess.Client.OracleException ORA-06550: 行1、列6:
PLS-00103: 記号""が見つかりました。 次のうちの1つが入るとき:
begin case declare exit for goto if loop mod null pragma
raise return select update while with <an identifier>
<a double-quoted delimited-identifier> <a bind variable> <<
close current delete fetch lock insert open rollback
savepoint set sql execute commit forall merge pipe
場所 Oracle.DataAccess.Client.OracleException.HandleErrorHelper(Int32 errCode, OracleConnection conn, IntPtr opsErrCtx, OpoSqlValCtx* pOpoSqlValCtx, Object src, String procedure)
場所 Oracle.DataAccess.Client.OracleException.HandleError(Int32 errCode, OracleConnection conn, String procedure, IntPtr opsErrCtx, OpoSqlValCtx* pOpoSqlValCtx, Object src)
場所 Oracle.DataAccess.Client.OracleCommand.ExecuteNonQuery()
場所 WindowsFormsApplication3.Form1.button1_Click(Object sender, EventArgs e)
場所 C:\Users\ganfield\Documents\Visual Studio 2008\Projects\WindowsFormsApplication3\WindowsFormsApplication3\Form1.cs:行 58
英語の内容だけでは何を言っているのか分かりませんw
検証が面倒で、何故CRが許可されたりされなかったりするのか調べていません。なので、未だに原因不明。
無名ブロックを書くときだけの注意点なので、上記のエラーが発生したCRを削ればいい、ぐらいに考えても良いと思います。
と、以上が回避策となります。これでMerge文もOracleで楽観ロック形式の同時実行制御付きで実行することができます。
ま、例外があがったりするので、DBのファクトリクラスなどを使用して抽象的に処理することはできませんので、若干とほほな感じもしますが…