我正在尝试创建一个触发器(使用带有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是否已被使用?
答案 0 :(得分:0)
我在这里添加的例子很笨重,这里的表现很差,但我想提出一个可能是起点的想法。
您提到您不希望SELECT
检查NEXTVAL
是否已被使用。如果你的意思是你根本不想要执行任何SELECT
,那么这个答案就是作弊,因为它包含SELECT
语句。但如果你只想避免变异表问题,那么它可能是一种可能性。
我将在此处添加的方法需要几个步骤来避免冲突,并在非NULL
值作为键提供时随时运行它们。它目前设置为每行触发,但如果整个语句将全为NULL或全非NULL,则可以将其更改为语句或复合触发器以提高效率。无论如何,在这个例子中,避免碰撞是低效的。
一般步骤:
- 如果NULL
,请使用NEXTVAL
- 如果不是NULL
,请针对提供的值检查LAST_VALUE
和CACHE
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中测试过,但是想要抛出一个可以避免冲突的想法。