在 Oracle Merge 语句的 Using 子句中指定参数

Posted

技术标签:

【中文标题】在 Oracle Merge 语句的 Using 子句中指定参数【英文标题】:Specifying parameters in the Using clause of an Oracle Merge statement 【发布时间】:2012-06-19 23:11:55 【问题描述】:

Oracle 的 PL/SQL 对我来说相当新,所以我需要一些帮助来了解我尝试在 Merge 的 Using 子句中使用参数的方式是否可行。

我正在使用 ODP.NET 使用 Oracle 11g 与现有的 C# .NET 4.0 代码库进行通信,该代码库使用 SQL 连接来检索/修改数据。现有的 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   :Id0  Id\n  ,:Options0  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 命令,并得到“未声明绑定变量 'Id0'”的响应。通常,当我在 SQL Developer 中使用未声明的绑定变量运行命令时,它会打开一个对话框来输入值,但不是使用此 SQL 命令,因此在 SQL Developer 中未声明它是可以理解的,但我不明白为什么会这样ODP.NET/C# 实现的情况,因为我将参数添加到 OracleCommand 对象。

如果有人能指出我做错了什么,或者告诉我如何达到同样的效果,将不胜感激。此外,如果有人知道将值列表传递到 Merge 的 Using 子句中的更好方法,而不是在它们之间执行一堆 SELECTs FROM dual 和 UNION ALL,那么也将不胜感激。

使用 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:\n0\nSource:\n1\n", err.Message, err.Source);
            System.Diagnostics.Debug.WriteLine("Message:\n0\nSource:\n1\n", err.Message, err.Source);
        
    
    cn.Close();

【问题讨论】:

你能发布你尝试在 SQL Developer 中运行的 SQL 吗? 我使用了上面列出的相同 SQL。通常 SQL Developer 会提示我输入绑定变量的值,但不会提示我列出的 SQL,我觉得这很奇怪;这也让我觉得我做错了什么,我只是不知道是什么。 【参考方案1】:

最好将 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 Associative Array 这样做,您将在 SGA 中始终拥有一条 SQL 语句,而不是针对所有可能数量的参数的许多语句,并且(这可能更重要)您将能够一次合并 1000 多个元素。

实际上,我注意到您没有使用 WHEN NOT MATCHED 子句。如果您真的对插入新记录不感兴趣,那么根本不需要使用 MERGE,只需使用 UPDATE 即可。使用Array Binding,您可以在一次往返中高效地多次执行 UPDATE 语句。

【讨论】:

我阅读了链接(顺便说一句,信息量很大),并且我一直在尝试实施您的建议,但是几个小时以来它一直给我一个 ORA-01036 问题。我对它可能是什么有两个想法,但如果它们是问题,我也不知道解决的正确方法。第一个是 :state_id 参数/绑定变量可能不会从 SQL 传递到 EXECUTE IMMEDIATE 内的 PL/SQL,尽管我将它添加到 C# 中的 cmd.Parameters 中。第二个是 Oracle 不知道 :id 和 :option 是什么,因为它们应该来自数组,但没有命名。想法? 我相信我通过将 :state_id 绑定变量作为其 USING 语句的最后一个参数传递给 EXECUTE IMMEDIATE 解决了这个问题(即 EXECUTE IMMEDIATE '...' USING t_ids(i), t_options (i), :state_id;)。但是我现在收到 ORA-06550、PLS-00215、PLS-00382 和 PLS-00320 错误。要查看所有这些,我必须在调用 cmd.ExecuteNonQuery() 时捕获 OracleException,并遍历其中包含的 OracleErrorCollection。 PLS-00215 错误似乎很奇怪,因为我用于选项的测试数据实际上是 "First", "Second", "Third"。 在 VARCHAR2 之后放置一个大小修复了 ORA-06550 和 PLS-00215 错误,并在每个 TYPE 声明的末尾添加了“INDEX BY PLS_INTEGER”修复了 PLS-00382 和 PLS-00320 错误。现在我只需要解决一组 ORA-01745 和 ORA-06512 错误(“第 8 行的主机/绑定变量名无效”)。出于某种原因,它似乎既不喜欢“i”也不喜欢“t_ids”,还不确定哪个。 感谢您通过更改更新答案,并将 :option 更改为 :options。为了让它工作,我还将 EXECUTE IMMEDIATE 语句中的 :id、:option 和 :state_id 更改为 :1、:2、:3。我相信这个问题实际上只是:option,因为'option'是一个保留字。最后一个障碍是未知的,因为我没有指定表定义。 Options 列是一个 Blob,我发现 Oracle 不支持通过 PL/SQL 关联数组 (goo.gl/cRBYvgoo.gl/SXEFSgoo.gl/qt0UT) 传递 Blob 数组;我把它改成了 Long Raw。 对于任何对此类问题同样感到沮丧的人,当前无法通过将列更改为 Long Raw 来正确更新,但我将其标记为答案,因为它在问题范围内有效正如最初定义的那样,以及 tomi44g 让我走上正轨并提供了答案的核心这一事实。如果我找到将字节数组传递给 Long Raw 或任何数据类型恰好正确的解决方案,我也会在这里发布。谢谢 tomi44g。

以上是关于在 Oracle Merge 语句的 Using 子句中指定参数的主要内容,如果未能解决你的问题,请参考以下文章

Oracle merge into 的效率问题

ORACLE MERGE INTO

ora_rowscn 在 USING 子句中时的 Oracle 合并语句行为

merge into using 详解

oracle中merge方法

oraclemergeinto用法及例子