如何创建一个触发器,允许sql开发人员使用oracle 11g手动和自动主键

时间:2017-04-24 02:35:22

标签: sql oracle oracle11g triggers

我正在尝试创建一个触发器(使用带有Oracle 11g的SQL Developer),允许手动插入主键,如果创建的记录没有指定的主键,它将从序列中分配一个。首先,我尝试在触发器中使用select语句,该语句检查序列生成的id是否因为手动插入而已在表中:

DROP TABLE testing;
DROP SEQUENCE testing_seq;

CREATE TABLE testing (
id_number NUMBER PRIMARY KEY,
test_data VARCHAR(50)
);

CREATE SEQUENCE testing_seq
MINVALUE 1
MAXVALUE 10000
START WITH 1
INCREMENT BY 1
NOORDER
NOCYCLE;

CREATE OR REPLACE TRIGGER auto_testing_id BEFORE
  INSERT ON testing 
  FOR EACH ROW 
  DECLARE
    tmp NUMBER;
    seq NUMBER;
  BEGIN
    IF :NEW.id_number IS NULL THEN
      seq := testing_seq.nextval;
      SELECT
        (SELECT 1 FROM testing WHERE id_number = seq
        ) INTO tmp FROM dual;
      while (tmp = 1)
      loop
        seq := testing_seq.nextval;
        SELECT
          (SELECT 1 FROM testing WHERE id_number = seq
          ) INTO tmp FROM dual;
      END loop;
      :NEW.id_number := seq;
    END IF;
  END;
  /

INSERT INTO testing VALUES(1,'test1');
INSERT INTO testing (test_data) VALUES('test2');

SELECT * FROM testing;

Table TESTING dropped.


Sequence TESTING_SEQ dropped.

Table TESTING created.


Sequence TESTING_SEQ created.

Trigger AUTO_TESTING_ID compiled

1 row inserted.


1 row inserted.


 ID_NUMBER TEST_DATA                                        
---------- --------------------------------------------------
         1 test1                                             
         2 test2                                             

这适用于手动创建的插入,但如果我尝试使用select语句插入则不行。我相信这是因为我引用了插入触发器内部的表格。

我尝试了一个没有检查的触发器,但正如预期的那样,如果触发器创建了一个已经在表中的id,它就会抛出一个唯一的约束错误

CREATE OR REPLACE TRIGGER auto_testing_id2 BEFORE
  INSERT ON testing 
  FOR EACH ROW 
  DECLARE
  BEGIN
      IF :NEW.id_number is null
      then 
        :NEW.id_number := testing_seq.nextval;
      end if;
  end;
/


Trigger AUTO_TESTING_ID2 compiled


1 row inserted.


Error starting at line : 59 in command -
INSERT INTO testing (test_data) VALUES('test2')
Error report -
SQL Error: ORA-00001: unique constraint (KATRINA_LEARNING.SYS_C001190313) violated
00001. 00000 -  "unique constraint (%s.%s) violated"
*Cause:    An UPDATE or INSERT statement attempted to insert a duplicate key.
           For Trusted Oracle configured in DBMS MAC mode, you may see
           this message if a duplicate entry exists at a different level.
*Action:   Either remove the unique restriction or do not insert the key.

 ID_NUMBER TEST_DATA                                        
---------- --------------------------------------------------
         1 test1    

我试图捕获此错误(使用错误名称DUP_VAL_ON_INDEX),然后循环它,直到找到序列中不在表中的下一个数字(有和没有错误捕获),但它甚至不会发送测试错误消息,当我添加循环时,它将无法编译...

任何人都可以帮我创建一个触发器,无需使用select语句即可查看序列nextval是否已被使用?

1 个答案:

答案 0 :(得分:0)

我在这里添加的例子很笨重,这里的表现很差,但我想提出一个可能是起点的想法。

您提到您不希望SELECT检查NEXTVAL是否已被使用。如果你的意思是你根本不想要执行任何SELECT,那么这个答案就是作弊,因为它包含SELECT语句。但如果你只想避免变异表问题,那么它可能是一种可能性。

我将在此处添加的方法需要几个步骤来避免冲突,并在非NULL值作为键提供时随时运行它们。它目前设置为每行触发,但如果整个语句将全为NULL或全非NULL,则可以将其更改为语句或复合触发器以提高效率。无论如何,在这个例子中,避免碰撞是低效的。

一般步骤:
- 如果NULL,请使用NEXTVAL
- 如果不是NULL,请针对提供的值检查LAST_VALUECACHE SEQUENCE
- 如果提供的值在CACHE内(或超出缓存)并且可能导致冲突,则跳过SEQUENCE超出提供的值并丢弃缓存中的值。

创建测试表/序列:

CREATE TABLE MY_TABLE (
  MY_TABLE_ID NUMBER NOT NULL PRIMARY KEY
); 

--Default cache here 20
CREATE SEQUENCE MY_SEQUENCE;

创建自主同步器。请注意,这根本不是有效的,并且并发可能是一个真正的问题(序列化替代方案如下)。
它假定CACHE至少为1,可能与NOCACHE不兼容。 (实际上整个情况可能更简单,只有NOCACHE SEQUENCE

CREATE OR REPLACE PROCEDURE SYNC_MY_SEQUENCE(P_CANDIDATE_MAX_VALUE IN NUMBER)
IS
PRAGMA AUTONOMOUS_TRANSACTION;
  V_LAST_NUMBER NUMBER;
  V_CACHE_SIZE  NUMBER;
  V_SEQUENCE_GAP NUMBER;
  V_SEQUENCE_DELTA NUMBER;
  V_NEXTVAL NUMBER;
  BEGIN

    SELECT
      LAST_NUMBER,
      CACHE_SIZE
    INTO V_LAST_NUMBER, V_CACHE_SIZE
    FROM USER_SEQUENCES
    WHERE SEQUENCE_NAME = 'MY_SEQUENCE';

    --Only do anything if the provided value could cause a collision.
    IF P_CANDIDATE_MAX_VALUE >= (V_LAST_NUMBER - V_CACHE_SIZE)
      THEN
        -- Get the delta, in case the provided value is way way higher than the SEQUENCE
        V_SEQUENCE_DELTA := P_CANDIDATE_MAX_VALUE + V_CACHE_SIZE - V_LAST_NUMBER ;

        -- Use the biggest gap to get a safe zone when resetting the SEQUENCE
        V_SEQUENCE_GAP := GREATEST(V_SEQUENCE_DELTA, V_CACHE_SIZE);

        -- Set the increment so the distance between the last_value and the safe zone can be moved in one jump
      EXECUTE IMMEDIATE 'ALTER SEQUENCE MY_SEQUENCE INCREMENT BY '||V_SEQUENCE_GAP;
        -- Jump to the safe zone.
        V_NEXTVAL := MY_SEQUENCE.NEXTVAL;
        -- Reset increment.  Note there is a space here that other sessions could get big NEXTVALs from concurrent access
      EXECUTE IMMEDIATE 'ALTER SEQUENCE MY_SEQUENCE INCREMENT BY 1';

        --Chew through the rest of at least one cache cycle.
        FOR CACHE_POINTER IN 1..V_CACHE_SIZE LOOP
          V_NEXTVAL := MY_SEQUENCE.NEXTVAL;
        END LOOP;

    END IF;

  COMMIT;

  END;
/

编辑:它的成本会更高,但是可以通过下面的替代方案来序列化访问以管理并发:

CREATE OR REPLACE PROCEDURE SYNC_MY_SEQUENCE(P_CANDIDATE_MAX_VALUE IN NUMBER)
IS
PRAGMA AUTONOMOUS_TRANSACTION;
  V_LAST_NUMBER NUMBER;
  V_CACHE_SIZE  NUMBER;
  V_SEQUENCE_GAP NUMBER;
  V_SEQUENCE_DELTA NUMBER;
  V_NEXTVAL NUMBER;
  V_LOCK_STATUS NUMBER;
  V_LOCK_HANDLE VARCHAR2(64);
  C_LOCK_KEY CONSTANT VARCHAR2(20) := 'SYNC_MY_SEQUENCE';

  BEGIN
  DBMS_LOCK.ALLOCATE_UNIQUE (C_LOCK_KEY,V_LOCK_HANDLE,10);
  --Serialize access
  V_LOCK_STATUS := DBMS_LOCK.REQUEST(
  LOCKHANDLE => V_LOCK_HANDLE,
  LOCKMODE => DBMS_LOCK.X_MODE,
  TIMEOUT => 10,
  RELEASE_ON_COMMIT => TRUE);

    SELECT
      LAST_NUMBER,
      CACHE_SIZE
    INTO V_LAST_NUMBER, V_CACHE_SIZE
    FROM USER_SEQUENCES
    WHERE SEQUENCE_NAME = 'MY_SEQUENCE';

    IF P_CANDIDATE_MAX_VALUE >= (V_LAST_NUMBER - V_CACHE_SIZE)
    THEN
      V_SEQUENCE_DELTA := P_CANDIDATE_MAX_VALUE + V_CACHE_SIZE - V_LAST_NUMBER ;

      V_SEQUENCE_GAP := GREATEST(V_SEQUENCE_DELTA, V_CACHE_SIZE);

      EXECUTE IMMEDIATE 'ALTER SEQUENCE MY_SEQUENCE INCREMENT BY '||V_SEQUENCE_GAP;

      V_NEXTVAL := MY_SEQUENCE.NEXTVAL;
      EXECUTE IMMEDIATE 'ALTER SEQUENCE MY_SEQUENCE INCREMENT BY 1';

      FOR CACHE_POINTER IN 1..V_CACHE_SIZE LOOP
        V_NEXTVAL := MY_SEQUENCE.NEXTVAL;
      END LOOP;

    END IF;

    COMMIT;

  END;
/

创建触发器:

CREATE OR REPLACE TRIGGER MAYBE_SET
BEFORE INSERT ON MY_TABLE
FOR EACH ROW
  BEGIN
    IF :NEW.MY_TABLE_ID IS NULL
    THEN
      :NEW.MY_TABLE_ID := MY_SEQUENCE.NEXTVAL;
      ELSE
      SYNC_MY_SEQUENCE(:NEW.MY_TABLE_ID);
    END IF;

  END;
/

然后测试一下:

INSERT INTO MY_TABLE SELECT LEVEL FROM DUAL CONNECT BY LEVEL < 5;
SELECT * FROM MY_TABLE ORDER BY 1 ASC;

MY_TABLE_ID  
1            
2            
3            
4          

每次只使用NEXTVAL

然后添加可碰撞的值。添加它将激活sproc并进行额外的工作以将SEQUENCE推入安全区域。

INSERT INTO MY_TABLE VALUES(5);
SELECT * FROM MY_TABLE ORDER BY 1 ASC;

MY_TABLE_ID  
1            
2            
3            
4            
5       

然后再次使用NULL:

INSERT INTO MY_TABLE VALUES(NULL);
SELECT * FROM MY_TABLE ORDER BY 1 ASC;

MY_TABLE_ID  
1            
2            
3            
4            
5            
41           

SEQUENCE到达那里需要付出昂贵的代价,但已经解决并且没有发生碰撞。

如果其他提供的值低于SEQUENCE可见度,则会自由添加并且不会更改NEXTVAL:

INSERT INTO MY_TABLE VALUES(7);
INSERT INTO MY_TABLE VALUES(19);
INSERT INTO MY_TABLE VALUES(-9999);
INSERT INTO MY_TABLE VALUES(NULL);
SELECT * FROM MY_TABLE ORDER BY 1 ASC;

MY_TABLE_ID  
-9999        
1            
2            
3            
4            
5            
7            
19           
41           
42  

如果差距很大,那就会跳出来:

INSERT INTO MY_TABLE VALUES(50000);
INSERT INTO MY_TABLE VALUES(NULL);
SELECT * FROM MY_TABLE ORDER BY 1 ASC;

MY_TABLE_ID  
-9999        
1            
2            
3            
4            
5            
7            
19           
41           
42           
50000        
50022        

这可能对您的用例来说太昂贵了,而且我没有在RAC中测试过,但是想要抛出一个可以避免冲突的想法。