I hesitate to add an answer to this, esp as Justin has already answered with an approach that meets your specific question. But I suspect that you probably have other business logic spread around (in triggers or on middleware/app side), so the following may help you or someone else.
One possible approach is to use Transactional APIs (xapis). Note that this is not the same as Table APIs (tapis), where even select access is hidden in pl/sql layers. Xapis would encapsulate just the transactional (ins/upd/del) requirements of the system, and end users would call a procedure to do something like "enroll a student", for example. See this Ask Tom article for more on Xapi approach.
How much business logic is used in the xapi would depend on a lot of factors, but I'd keep it simple. For your specific question (to serialize inserts to the enrollment table), you can do this fairly easily in pl/sql, something like:
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;
/
And to enroll a user, you would call:
exec enroll_pkg.enroll_user('Joe Smith','Biology');
If you went this route, you would typically remove insert/update/delete privs from users directly, and grant them execute on the xapi instead. Also note that I only briefly tested the above code, but it should illustrate the approach.