Oracle の PL/SQL は私にとってかなり新しいものなので、Merge の Using 句でパラメーターを使用しようとしている方法が可能かどうかを理解する助けが必要です。
SQL接続を使用してデータを取得/変更する既存のC#.NET 4.0コードベースとの通信にODP.NETを使用してOracle 11gを使用しています。既存の SQL ステートメントは次のようになります。
MERGE INTO Worker Target
USING
(
SELECT
:Id0 Id
,:Options0 Options
FROM dual
UNION ALL
SELECT
:Id1 Id
,:Options1 Options
FROM dual
) Source
ON (Target.Id = Source.Id)
WHEN MATCHED THEN
UPDATE SET
Target.StateId = :StateId
,Target.Options = Source.Options
Using 句は C# StringBuilder で生成され、さまざまな数のワーカー Id/Option ペアに対応すると同時に、一致するパラメーターが作成されます。
StringBuilder usingClause = new StringBuilder();
List<OracleParameter> parameters = new List<OracleParameter>();
for (int i = 0; i < workers.Count; ++i)
{
if (i > 0)
usingClause.Append("UNION ALL\n");
usingClause.AppendFormat("SELECT\n :Id{0} Id\n ,:Options{0} Options\n FROM dual\n", i);
parameters.Add(new OracleParameter("Id" + i, workers[i].Id));
parameters.Add(new OracleParameter("Options" + i, workers[i].Options))
}
parameters.Add(new OracleParameter("StateId", pendingStateId));
usingClause StringBuilder は、Merge コマンドの残りの部分と組み合わされて「sql」という文字列になり、OracleCommand オブジェクトで使用されます。SQL Merge ステートメントを実行する C# は次のようになります。
OracleConnection cn = new OracleConnection(
ConfigurationManager.ConnectionStrings["OracleSystemConnection"].ConnectionString
);
using (OracleCommand cmd = new OracleCommand(sql, cn))
{
cmd.BindByName = true;
cn.Open();
foreach (OracleParameter prm in parameters)
cmd.Parameters.Add(prm);
cmd.ExecuteNonQuery();
cn.Close();
}
パラメータを名前でバインドする場合としない場合の両方を試し、パラメータを名前でバインドせずにバインドするときに順序が正しいことを確認しました。私が取得し続けるのは、「ORA-01008: すべての変数がバインドされていません」というエラーです。
また、SQL Developer で Merge コマンドを実行してみましたが、「Bind Variable 'Id0' is NOT DECLARED.」という応答が返されました。通常、宣言されていないバインド変数を使用してSQL Developerでコマンドを実行すると、値を入力するためのダイアログが開きますが、このSQLコマンドではそうではないため、SQL Developerで宣言されていないことは理解できますが、これがなぜなのかわかりませんOracleCommand オブジェクトにパラメータを追加しているため、ODP.NET/C# 実装の場合です。
誰かが私が間違っていることを指摘したり、同じ効果を達成する方法を教えてくれたりしたら、大歓迎です. また、値のリストを Merge の Using 句に渡すためのより良い方法を誰かが知っている場合は、その間に UNION ALL を使用して複数の SELECTs FROM dual を実行するよりも、同様に高く評価されます。
オプション列に Long Raw を使用して回答する
ちょっとした作業の後、これが最終的な解決策でした。正しい方向に向けてくれた tomi44g に感謝します。
DECLARE
TYPE id_array IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
TYPE option_array IS TABLE OF LONG RAW INDEX BY PLS_INTEGER;
t_ids id_array := :ids;
t_options option_array := :options;
BEGIN
FORALL i IN 1..t.ids.count
EXECUTE IMMEDIATE '
MERGE INTO Worker Target
USING (SELECT :1 Id, :2 Options FROM dual) Source
ON (Source.Id = Target.Id)
WHEN MATCHED THEN
UPDATE SET
Target.StateId = :3
,Target.Options = Source.Options' USING t_ids(i), t_options(i), :state_id;
END;
これは、ソリューションに対応するために C# が変更されたものです。
// Gather the values into arrays for binding.
int[] workerIds = new int[workers.Count];
byte[][] workerOptions = new byte[workers.Count][];
BinaryFormatter binaryFormatter = new BinaryFormatter();
for (int i = 0; i < workers.Count; ++i)
{
workerIds[i] = workers[i].Id;
// There's an assumed limit of 4096 bytes here; this is just for testing
MemoryStream memoryStream = new MemoryStream(4096);
binaryFormatter.Serialize(memoryStream, workers[i].Options);
workerOptions[i] = memoryStream.ToArray();
}
// Excute the command.
OracleConnection cn = new OracleConnection(
ConfigurationManager.ConnectionStrings["OracleSystemConnection"].ConnectionString
);
using (OracleCommand cmd = new OracleCommand(sql, cn))
{
cmd.BindByName = true;
cn.Open();
OracleParameter ids = new OracleParameter();
ids.OracleDbType = OracleDbType.Int32;
ids.CollectionType = OracleCollectionType.PLSQLAssociativeArray;
ids.Value = workerIds;
ids.ParameterName = "ids";
OracleParameter options = new OracleParameter();
options.OracleDbType = OracleDbType.LongRaw;
options.CollectionType = OracleCollectionType.PLSQLAssociativeArray;
options.Value = workerOptions;
options.ParameterName = "options";
cmd.Parameters.Add(ids);
cmd.Parameters.Add(options);
cmd.Parameters.Add(new OracleParameter("state_id", pendingStateId));
try
{
cmd.ExecuteNonQuery();
}
catch (OracleException e)
{
foreach (OracleError err in e.Errors)
{
Console.WriteLine("Message:\n{0}\nSource:\n{1}\n", err.Message, err.Source);
System.Diagnostics.Debug.WriteLine("Message:\n{0}\nSource:\n{1}\n", err.Message, err.Source);
}
}
cn.Close();
}