Sql Oracle阻止重复插入
考虑一个有多个课程注册请求的系统。我们需要一种方法来阻止系统中的重复注册。我创建了一个触发器,如下所示,但是当我同时从不同的连接(相隔毫秒)收到两个请求时,它们都被插入。我做错了什么Sql Oracle阻止重复插入,sql,oracle,race-condition,Sql,Oracle,Race Condition,考虑一个有多个课程注册请求的系统。我们需要一种方法来阻止系统中的重复注册。我创建了一个触发器,如下所示,但是当我同时从不同的连接(相隔毫秒)收到两个请求时,它们都被插入。我做错了什么 create trigger enrollment_duplicates before insert on enrollment for each row begin select count(*) into cnt from enrollment where use
create trigger enrollment_duplicates
before insert
on enrollment
for each row
begin
select count(*)
into cnt
from enrollment
where user = :new.user
and course = :new.course
and status = 'Enrolled';
if cnt > 0 then
raise_application_error(-20001, 'User already enrolled in course');
end if;
end;
编辑:
如果我们将用户/课程设置为一个唯一的约束,那么这很容易,但事实并非如此。他们可以根据状态重新注册。您需要一个唯一的索引。如果您说只能有一个
已注册的
行,但有许多行具有其他状态,则可以创建基于函数的索引
CREATE UNIQUE INDEX idx_stop_multiple_enrolls
ON enrollment( (case when status = 'Enrolled'
then user
else null
end),
(case when status = 'Enrolled'
then course
else null
end) );
这利用了这样一个事实,即当所有列都为NULL
时,Oracle不在索引中包含值,因此索引中只有状态为已注册的行的条目
请注意,USER
是一个保留字(有一个内置函数USER
),因此我假设您的实际列的名称有所不同。我犹豫是否要添加一个答案,尤其是因为Justin已经用一种满足您特定问题的方法回答了。但我怀疑您可能有其他业务逻辑(在触发器或中间件/应用程序端),因此以下内容可能对您或其他人有所帮助
一种可能的方法是使用事务API(XAPI)。请注意,这与表API(TAPI)不同,在表API中,甚至select访问都隐藏在pl/sql层中。Xapis将只封装系统的事务(ins/upd/del)需求,最终用户将调用一个过程来执行类似“注册学生”之类的操作。有关Xapi方法的更多信息,请参见本文
xapi中使用了多少业务逻辑取决于很多因素,但我会保持简单。对于您的特定问题(要将插入序列化到注册表),您可以在pl/sql中相当轻松地执行此操作,例如:
create table enrollment
(
id number,
username varchar2(50),
course varchar2(50),
status varchar2(50),
created_date date default sysdate not null
);
create index enrollment_idx
on enrollment(username, course)
logging
noparallel;
create or replace package enroll_pkg as
err_already_enrolled constant number := -20101;
err_already_enrolled_msg constant varchar2(50) := 'User is already enrolled';
err_lock_request constant number := -20102;
err_lock_request_msg constant varchar2(50) := 'Unable to obtain lock';
enroll_lock_id constant number := 42;
function is_enrolled(i_username varchar2, i_course varchar2) return number;
procedure enroll_user(i_username varchar2, i_course varchar2);
end;
/
create or replace package body enroll_pkg as
-- returns 1=true, 0=false
function is_enrolled(i_username varchar2, i_course varchar2) return number is
l_cnt number := 0;
begin
-- run test if user is enrolled in this course
select decode(count(1),0,0,1)
into l_cnt
from enrollment
where username=i_username
and course=i_course
and status = 'ENROLLED';
-- testing locks here
--dbms_lock.sleep(5);
return l_cnt;
end;
procedure enroll_user(i_username varchar2, i_course varchar2)
is
l_lock_result number;
l_username enrollment.username%type;
l_course enrollment.course%type;
begin
-- try to get lock (serialize access)
l_lock_result := dbms_lock.request(enroll_lock_id, dbms_lock.x_mode, 10, true);
if (l_lock_result <> 0) then
raise_application_error(err_lock_request,err_lock_request_msg || ' (' || l_lock_result || ')');
end if;
-- simple business rule: uppercase names & course
l_username := upper(trim(i_username));
l_course := upper(trim(i_course));
if (is_enrolled(l_username, l_course) > 0) then
raise_application_error(err_already_enrolled,err_already_enrolled_msg);
end if;
-- do other business logic checks, update other tables, logging, etc...
-- add enrollment
insert into enrollment(id,username,course,status) values
(enroll_seq.nextval,l_username,l_course,'ENROLLED');
commit;
-- release lock
l_lock_result := dbms_lock.release(enroll_lock_id);
end;
end;
/
如果采用这种方法,通常会直接从用户中删除insert/update/delete PRIV,并授予它们在xapi上执行的权限。还要注意的是,我只是简单地测试了上述代码,但它应该说明了方法。谢谢Justin。列的名称不同。我将使用这个解决方案。如果我将隔离级别设置为SERIALIZABLE而不是READ_Committed,我就能够让我的代码(使用我问题中的触发器)在其中一个插入上爆炸。这两个隔离级别不都应该导致第二次插入在提交时失败吗?@DMoses-触发器肯定不会阻止在读提交隔离级别的不同会话中重复插入。这就是在触发器中实现这类事情如此困难的原因之一。您可以执行类似于对学生
表中的行执行选择更新
的操作,以便两个会话不能在同一时间点修改关于同一学生的数据。这将序列化访问,但可能会产生许多其他问题。有趣且信息丰富。这对我来说不太合适,但我喜欢学习,并且花了一些时间在dbms_lock对象上。
exec enroll_pkg.enroll_user('Joe Smith','Biology');