4

考虑一个有多个课程注册请求的系统。我们需要一种方法来阻止系统中的重复注册。我按如下方式创建了一个触发器,但是当我同时收到来自不同连接的两个请求时(相隔毫秒),它们都被插入。我究竟做错了什么

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;

编辑:

如果我们将用户/课程设置为唯一约束,这很容易,但事实并非如此。他们可以根据状态重新注册。

4

2 回答 2

6

你需要一个唯一的索引。如果您说只能有Enrolled一行但有很多行具有其他状态,则可以创建基于函数的索引

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) );

这利用了这样一个事实,即当所有列都在索引中时,Oracle 不会在索引中包含值,因此索引仅具有isNULL的行的条目。statusEnrolled

请注意,这USER是一个保留字(有一个内置函数USER),所以我假设您的实际列被命名为不同的名称。

于 2013-10-02T21:48:00.740 回答
1

我不愿为此添加答案,尤其是贾斯汀已经用满足您特定问题的方法进行了回答。但我怀疑您可能有其他业务逻辑散布在(在触发器或中间件/应用程序端),因此以下内容可能会对您或其他人有所帮助。

一种可能的方法是使用事务性 API (xapis)。请注意,这与 Table API (tapis) 不同,其中甚至选择访问都隐藏在 pl/sql 层中。Xapis 将仅封装系统的事务性(ins/upd/del)需求,并且最终用户将调用一个过程来执行诸如“注册学生”之类的操作。有关 Xapi 方法的更多信息,请参阅这篇Ask Tom 文章

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;
/

要注册用户,您可以调用:

exec enroll_pkg.enroll_user('Joe Smith','Biology');

如果你走这条路,你通常会直接从用户中删除插入/更新/删除权限,并授权他们在 xapi 上执行。另请注意,我只是简单地测试了上面的代码,但它应该说明该方法。

于 2013-10-03T20:28:15.277 回答