简体   繁体   中英

Adding a between date constraint in Oracle SQL with parent/child tables

I have two tables Study and Case. The Study is the parent table and Case is the child table. I have to add a constraint such that the CASE_DATE in the Case table is within it's parent table Study's START_DATE and END_DATE.

Study
-------
ID
START_DATE
END_DATE

Case
-----
ID
STUDY_ID
CASE_DATE

The solution is much simpler:

CREATE OR REPLACE TRIGGER check_date
  BEFORE UPDATE OR INSERT ON CASE
  FOR EACH ROW 
DECLARE

  StartDate STUDY.START_DATE%TYPE;
  EndDate STUDY.END_DATE%TYPE;

BEGIN

SELECT START_DATE, END_DATE
INTO StartDate, EndDate 
FROM STUDY
WHERE ID = :NEW.STUDY_ID; -- Assuming ID is the primary key, i.e. unique

IF NEW.STUDY_ID NOT BETWEEN StartDate AND EndDate THEN
   raise_application_error(-20001, 'Study date not in valid range');
END IF;

END;
/

However, there are some pre-requisites:

  • ID in table STUDY is a unique key
  • START_DATE and END_DATE in table STUDY must not change after insert into table CASE, otherwise you have to write another trigger also for table STUDY
  • You have a foreign key constraint from STUDY.ID to CASE.STUDY_ID

As long as these pre-requisites are present the trigger should work.

In case you have to do this with a constraint you can do it also. Create a function that checks the date, eg

create or replace function IsDateValid(StudyId in number, CaseDate in date) 
return boolean is
declare
   StartDate STUDY.START_DATE%TYPE;
   EndDate STUDY.END_DATE%TYPE;        
BEGIN        
    SELECT START_DATE, END_DATE
    INTO StartDate, EndDate 
    FROM STUDY
    WHERE ID = StudyId; 

    return CaseDate BETWEEN StartDate AND EndDate;
END;
/

Then create the constraint:

ALTER TABLE case ADD CONSTRAINT check_date CHECK (IsDateValid(STUDY_ID, CASE_DATE));

Assuming that the basic referential integrity is being enforced through a standard foreign key constraint, and that no columns are allowed to be NULL.

In order to properly create this validation using a trigger a procedure should be created to obtain user-specified locks so the validation can be correctly serialized in a multi-user environment.

PROCEDURE request_lock
  (p_lockname                     IN     VARCHAR2
  ,p_lockmode                     IN     INTEGER  DEFAULT dbms_lock.x_mode
  ,p_timeout                      IN     INTEGER  DEFAULT 60
  ,p_release_on_commit            IN     BOOLEAN  DEFAULT TRUE
  ,p_expiration_secs              IN     INTEGER  DEFAULT 600)
IS
  -- dbms_lock.allocate_unique issues implicit commit, so place in its own
  -- transaction so it does not affect the caller
  PRAGMA AUTONOMOUS_TRANSACTION;
  l_lockhandle                   VARCHAR2(128);
  l_return                       NUMBER;
BEGIN
  dbms_lock.allocate_unique
    (lockname                       => p_lockname
    ,lockhandle                     => p_lockhandle
    ,expiration_secs                => p_expiration_secs);
  l_return := dbms_lock.request
    (lockhandle                     => l_lockhandle
    ,lockmode                       => p_lockmode
    ,timeout                        => p_timeout
    ,release_on_commit              => p_release_on_commit);
  IF (l_return not in (0,4)) THEN
    raise_application_error(-20001, 'dbms_lock.request Return Value ' || l_return);
  END IF;
  -- Must COMMIT an autonomous transaction
  COMMIT;
END request_lock;

This procedure can then be used in two compound triggers (assuming at least Oracle 11, this will need to be split into individual triggers in earlier versions)

CREATE OR REPLACE TRIGGER cases_exist
  FOR UPDATE ON study
  COMPOUND TRIGGER

  -- Table to hold identifiers of updated studies (assuming numeric)
  g_ids sys.odcinumberlist;

BEFORE STATEMENT 
IS
BEGIN
  -- Reset the internal study table
  g_ids := sys.odcinumberlist();
END BEFORE STATEMENT; 

AFTER EACH ROW
IS
BEGIN
  -- Store the updated studies
  IF (  :new.start_date <> :old.start_date
     OR :new.end_date <> :old.end_date)
  THEN           
    g_ids.EXTEND;
    g_ids(g_ids.LAST) := :new.id;
  END IF;
END AFTER EACH ROW;

AFTER STATEMENT
IS
  CURSOR csr_studies
  IS
    SELECT DISTINCT
           sty.column_value id
    FROM TABLE(g_ids) sty
    ORDER BY sty.column_value;
  CURSOR csr_constraint_violations
    (p_id study.id%TYPE)
  IS
    SELECT NULL
    FROM study sty
         INNER JOIN case cse
           ON (   cse.study_id = sty.id
              AND cse.case_date NOT BETWEEN sty.start_date AND sty.end_date)
    WHERE sty.id = p_id;
  r_constraint_violation csr_constraint_violations%ROWTYPE;
BEGIN
  -- Check if for any updated study there exists a case outside the start and
  -- end dates. Serialise the constraint for each study id so concurrent
  -- transactions do not affect each other
  FOR r_study IN csr_studies LOOP
    request_lock('STUDY_CASES_' || r_study.id);
    OPEN csr_constraint_violations(r_study.id);
    FETCH csr_constraint_violations INTO r_constraint_violation;
    IF csr_constraint_violations%FOUND THEN
      CLOSE csr_constraint_violations;
      raise_application_error(-20001, 'Study ' || r_study.id || ' has cases outside its dates');
    ELSE
      CLOSE csr_constraint_violations;
    END IF;
  END LOOP;
END AFTER STATEMENT;

END;
/

CREATE OR REPLACE TRIGGER study_dates
  FOR INSERT OR UPDATE ON case
  COMPOUND TRIGGER

  -- Table to hold identifiers of studies (assuming numeric)
  g_study_ids sys.odcinumberlist;

BEFORE STATEMENT 
IS
BEGIN
  -- Reset the internal study table
  g_study_ids := sys.odcinumberlist();
END BEFORE STATEMENT; 

AFTER EACH ROW
IS
BEGIN
  -- Store the updated studies
  IF (  INSERTING
     OR :new.study_id <> :old.study_id
     OR :new.case_date <> :old.case_date)
  THEN           
    g_study_ids.EXTEND;
    g_study_ids(g_study_ids.LAST) := :new.study_id;
  END IF;
END AFTER EACH ROW;

AFTER STATEMENT
IS
  CURSOR csr_studies
  IS
    SELECT DISTINCT
           sty.column_value id
    FROM TABLE(g_study_ids) sty
    ORDER BY sty.column_value;
  CURSOR csr_constraint_violations
    (p_id study.id%TYPE)
  IS
    SELECT NULL
    FROM study sty
         INNER JOIN case cse
           ON (   cse.study_id = sty.id
              AND cse.case_date NOT BETWEEN sty.start_date AND sty.end_date)
    WHERE sty.id = p_id;
  r_constraint_violation csr_constraint_violations%ROWTYPE;
BEGIN
  -- Check if for any updated case it is now outside the start and end dates of
  -- the study. Serialise the constraint for each study id so concurrent
  -- transactions do not affect each other
  FOR r_study IN csr_studies LOOP
    request_lock('STUDY_CASES_' || r_study.id);
    OPEN csr_constraint_violations(r_study.id);
    FETCH csr_constraint_violations INTO r_constraint_violation;
    IF csr_constraint_violations%FOUND THEN
      CLOSE csr_constraint_violations;
      raise_application_error(-20001, 'Study ' || r_study.id || ' has cases outside its dates');
    ELSE
      CLOSE csr_constraint_violations;
    END IF;
  END LOOP;
END AFTER STATEMENT;

END;
/

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM