2

如果我在存储过程中使用 FOR UPDATE 子句,我应该什么时候“提交”?在关闭打开的光标之后还是在关闭打开的光标之前?以下是我正在使用的程序,我是否以正确的方式进行操作?

CREATE OR REPLACE PROCEDURE Proc_UpdateCSClientCount(inMerid     IN  VARCHAR2,
                                                     outCliCount  OUT NUMBER,
                                                     outretvalue  OUT NUMBER)
AS
   CURSOR c1 IS
      SELECT CLIENT_COUNT
        FROM OP_TMER_CONF_PARENT
       WHERE MER_ID = inMerid
      FOR UPDATE OF CLIENT_COUNT;
BEGIN
   OPEN c1;
   IF SQL%ROWCOUNT = 1 THEN
      FETCH c1 INTO outCliCount;
      outCliCount := outCliCount + 1;
      UPDATE OP_TMER_CONF_PARENT
         SET CLIENT_COUNT = outCliCount
       WHERE CURRENT OF c1;
   END IF;
   outretvalue := 0;
   CLOSE c1;
   COMMIT;
EXCEPTION
   WHEN no_data_found THEN
      outretvalue := -1;
END;
4

3 回答 3

6

您应该在事务结束时提交。我怀疑您能否找到一个合理的案例,即交易结束处于FOR UPDATE循环中间。

也许您听说过经常提交是一件好事。这是一个错误的神话,这是完全错误的。在 Oracle 中则相反:提交涉及额外的工作,因此您应该只在所有工作完成后才提交,而不是以前。

此外,从逻辑的角度来看,如果您可以从头开始而不是完成一半的工作,那么从错误中恢复会变得难以想象。

IMO,在程序中提交应该是非常罕见的。调用应用程序应该是进行必要检查并最终决定是否应提交数据的应用程序。

总之,您不能跨FOR UPDATE循环提交(它会产生一个ORA-01002: fetch out of sequence),这是一件好事。每当你发现自己在一个正常的循环中提交时,你应该问问自己提交是否真的有必要——很可能不是。

如果您确实需要提交并且只获取一次,那么在关闭游标之前或之后提交都没有关系。


在您的代码摘录之后更新:您的代码中有很多地方需要更正(我想它不是直接的生产代码,但仍然是):

  • 永远不会引发异常:只有隐式SELECT INTO可以产生NO_DATA_FOUND.
  • SQL%ROWCOUNT如果前面的语句是 NULL,则为 NULL SELECT
  • 您可以使用c1%ROWCOUNT,但这只会返回获取的行数:0在初始open.
  • 我主要使用FOR UPDATE NOWAIT这样两个会话永远不会互相阻塞。如果你只使用FOR UPDATE,你还不如使用一个UPDATE,而不是SELECT预先使用。
  • 这是一个偏好问题,但返回码容易出错,通常首选异常。让错误传播。为什么有人会在id不存在的情况下调用此函数?这可能是调用应用程序/过程中的一个错误,所以你不应该抓住它。

所以你可以像这样重写你的程序:

CREATE OR REPLACE PROCEDURE Proc_UpdateCSClientCount(inMerid     IN  VARCHAR2, 
                                                     outCliCount OUT NUMBER) AS
BEGIN
   -- lock the row, an exception will be raised if this row is locked
   SELECT CLIENT_COUNT + 1
     INTO outCliCount
     FROM OP_TMER_CONF_PARENT
    WHERE MER_ID = inMerid
   FOR UPDATE OF CLIENT_COUNT NOWAIT;
   -- update the row
   UPDATE OP_TMER_CONF_PARENT
      SET CLIENT_COUNT = CLIENT_COUNT + 1
    WHERE MER_ID = inMerid;
END;
于 2012-11-07T09:24:21.907 回答
2

来自Oracle 文档

当您打开游标时,所有行都被锁定,而不是在获取它们时。 当您提交或回滚事务时,这些行被解锁。 由于不再锁定行,因此您无法在提交后从 FOR UPDATE 游标中获取。

这很重要。如果您在关闭游标之前或之后提交,则完成任务(完成获取)并不重要。

但是,如果需要在 fetch 之间提交,作为一种解决方法,请使用 update 和 rowid,而不是where current of. 来自文档的示例:

DECLARE
   CURSOR c1 IS SELECT last_name, job_id, rowid FROM employees;
   my_lastname   employees.last_name%TYPE;
   my_jobid      employees.job_id%TYPE;
   my_rowid      UROWID;
BEGIN
   OPEN c1;
   LOOP
      FETCH c1 INTO my_lastname, my_jobid, my_rowid;
      EXIT WHEN c1%NOTFOUND;
      UPDATE employees SET salary = salary * 1.02 WHERE rowid = my_rowid;
      -- this mimics WHERE CURRENT OF c1
      COMMIT;
   END LOOP;
   CLOSE c1;
END;
/

更新(编辑问题后):您可以在单个 sql 中执行此操作,无需光标。

UPDATE OP_TMER_CONF_PARENT 
set CLIENT_COUNT = CLIENT_COUNT +1 
where MER_ID = inMerid;

更新2。为了工作,代码应该更新如下:

...
open C1;
FETCH C1 into OUTCLICOUNT;
--dbms_output.put_line(' count:'||c1%rowcount);
IF c1%rowcount = 1 THEN
      outCliCount := outCliCount + 1;
...

也就是说: fetch 应该在计算受影响的行之前完成,并且受影响的行是c1%rowcount,不是sql%rowcount。如果您想知道某行是否更新,您应该在 if 中添加一个 else 并为 outretvalue 参数分配一个特殊值。

于 2012-11-07T08:35:47.600 回答
0

如果您在关闭游标之前提交,然后尝试再次获取,您会收到 INVALID_CURSOR 异常。我建议在关闭光标后提交。

于 2012-11-07T08:33:03.550 回答