4

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();
}
4

1 に答える 1

3

IDとオプションのリストを配列にバインドしてから、PL/SQLブロックでFORALLを使用してMERGEを実行することをお勧めします。

DECLARE
  TYPE id_array_type IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
  TYPE options_array_type IS TABLE OF VARCHAR2 (100) INDEX BY PLS_INTEGER;

  t_ids        id_array_type := :ids;
  t_options    options_array_type  := :options;
  v_state_id   NUMBER := :stateId;
BEGIN
  FORALL i IN 1 .. t_ids.count
    EXECUTE IMMEDIATE '
      MERGE INTO worker target
      USING (SELECT :id id, :options options FROM dual) source
      ON (source.id = target.id)
      WHEN MATCHED THEN UPDATE SET target.stateId = :state_id, target.options = source.options'
      USING t_ids (i), t_options (i), v_state_id;
END;

次に、パラメータをPL / SQL連想配列としてバインドできます 。これを行うと、可能なすべてのパラメータ数に対して多数のステートメントではなく、常に1つのSQLステートメントがSGAに含まれ、(おそらくより重要な)数千の要素をマージできるようになります。一度に。

実際、WHENNOTMATCHED句を使用していないことに気づきました。新しいレコードを挿入することに本当に興味がない場合は、MERGEを使用する必要はまったくなく、代わりにUPDATEを使用してください。配列バインディングを使用すると、1回のラウンドトリップでUPDATEステートメントを複数回効率的に実行できます。

于 2012-06-20T15:20:19.480 に答える