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

时间:2012-06-19 23:11:55

标签: .net oracle c#-4.0 plsql odp.net

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

我正在使用ODP.NET与Oracle 11g进行通信,以便与使用SQL连接检索/修改数据的现有C#.NET 4.0代码库进行通信。现有的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中生成,以容纳不同数量的worker 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命令的其余部分组合成一个名为&#39; sql&#39;的字符串,然后在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();
}

我已尝试过,无论是否按名称绑定参数,并确保在没有参数名称的情况下绑定时顺序正确。我一直得到的是一个&#34; ORA-01008:并非所有变量都受到约束&#34;错误。

我还尝试在SQL Developer中运行Merge命令,并获得&#34; Bind Variable&#39; Id0&#39;没有声明。&#34;通常当我在SQL Developer中使用未声明的绑定变量运行命令时,它会打开一个对话框来输入值,但是不能使用这个SQL命令,因此可以理解它在SQL Developer中是未声明的,但我并没有&#39 ;了解为什么ODP.NET / C#实现的情况,因为我将参数添加到OracleCommand对象。

如果有人能指出我做错了什么,或者告诉我如何达到同样的效果,我们将不胜感激。此外,如果有人知道将值列表传递给Merge的Using子句的更好方法,那么在它们之间使用UNION ALLs做一堆SELECT FROM FROM时,也会受到赞赏。

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

1 个答案:

答案 0 :(得分: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 Associative Array 这样做你将在SGA中总是有一个SQL语句而不是所有可能数量的参数的许多语句(这可能更重要)你将能够一次合并1000个元素。

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