Как решить это упражнение о триггерах Oracle

#oracle #triggers #business-rules

#Oracle #триггеры #бизнес-правила

Вопрос:

Я должен решить это упражнение о триггерах:

Рассмотрим следующую схему реляционной базы данных, используемую для представления информации о проекте:

Человек (идентификатор, фамилия, имя, национальность)

Проект (название, менеджер, начальный год, количество участников, международный)

Персонал (Project, PersonID)

Укажите триггеры, необходимые в Oracle для поддержания следующих ограничений целостности:

a) Количество людей, участвующих в проекте (атрибут NumPeopleInvolved), должно соответствовать количеству кортежей, введенных в персонал для этого проекта

б) Если проект международный (международный атрибут принимает только два значения), то в проекте должны участвовать как минимум два человека разных национальностей

У меня проблема с частью b).

Я не знаю, как справиться со случаем, когда в данном проекте не задействованы люди. Если я попытаюсь вставить первых людей, у меня не может быть двух людей разных национальностей, поскольку у меня есть только один человек.

Как следует обращаться с этой ситуацией?

Должен ли я использовать триггер на уровне оператора? У меня нет опыта работы с триггерами, поэтому я до сих пор не совсем понял, что я могу / не могу сделать с одним типом триггера.

Я пробовал этот способ, но он явно работает не так, как должен:

 CREATE TRIGGER InsertPersonnelInternational
AFTER INSERT ON Personnel
FOR EACH ROW
BEGIN
    SELECT ProjectName
    FROM Personnel INNER JOIN Project
    WHERE PersonID = :new.ID Project = Name

    SELECT International
    FROM Personnel INNER JOIN Project
      ON Project = Name

    SELECT COUNT(*) AS NumPersonnel
    FROM Personnel
    WHERE Project = :new.Project

    IF NumPersonnel >= 1 THEN
    BEGIN
        SELECT COUNT(*) AS NumNationalities
        FROM Personnel INNER JOIN Person
        ON Project = ProjectName
        GROUP BY Nationality

        IF International THEN
            IF NumNationalities = 1 Then
            BEGIN
                raise_application_error(-1)
            END
        ELSE
            IF NumNationalities <> 1 THEN
            BEGIN
                raise_application_error(-1)
            END
        END
    END
END
  

Комментарии:

1. @WernfriedDomscheit — Действительно. Я всегда удивляюсь, когда инструкторы, пытаясь научить технике, предоставляют сценарий, в котором эта техника не должна использоваться.

Ответ №1:

Когда у вас есть триггер на уровне строки в таблице Personnel , вы не можете выполнить какой-либо выбор в таблице Personnel внутри триггера — вы получите ORA-04091: table PERSONEL is mutating ... сообщение об ошибке.

Я думаю, ваш учитель ожидает чего-то подобного:

 CREATE TRIGGER ProjectConsistency
    BEFORE INSERT OR UPDATE ON PROJECT
    FOR EACH ROW
    
    p_count INTEGER;
    n_count INTEGER;

BEGIN

    SELECT COUNT(*)
    INTO p_count
    FROM Personnel
    WHERE PROJECT = :new.NAME;
        
    IF :new.NumPeopleInvolved <> p_count THEN
        RAISE_APPLICATION_ERROR(-20010, 'The number of people involved in a project must be consistent with the number of tuples entered in Personnel for that project');
    END IF;

    IF :new.International = 'YES' THEN
        SELECT COUNT(DISTINCT Nationality)
        INTO n_count
        FROM Personnel
        WHERE PROJECT = :new.NAME;
        
        IF n_count < 2 THEN
            RAISE_APPLICATION_ERROR(-20010, 'The project must involve at least two people of different nationalities')
        END IF;    
    END IF;

END;
  

На самом деле вы бы не реализовали такое требование с помощью триггера, вы бы использовали процедуру PL / SQL.

Атрибут NumPeopleInvolved бесполезен, т. Е. Избыточен. Обычно вы решаете это с помощью

 UPDATE PROJECT proj 
SET NumPeopleInvolved = 
    (SELECT COUNT(*)
    FROM Personnel p
    WHERE PROJECT = :new.NAME)
WHERE NAME = :new.NAME;
  

Например, такое обновление может быть выполнено с помощью триггера.

На самом деле вам понадобятся аналогичные триггеры также для таблицы Personnel и Person , поскольку personel / persons могут измениться, и проект станет непоследовательным. Я не знаю, следует ли это учитывать в упражнении.

Представьте, что on person освобождается, т. Е. Удаляется из таблицы Person:

  • вызовет ли приложение сообщение об ошибке — человек не может быть освобожден (что произойдет, если человек умрет от короны :-))?
  • будет ли проект недействительным?
  • будет ли проект автоматически обновляться?

Затем вы никогда не должны вызывать такие ошибки, как raise_application_error(-1) — всегда сообщайте пользователю, что пошло не так!

Комментарии:

1. Триггер в ПРОЕКТЕ не поможет, потому что это действие DML для ПЕРСОНАЛА, которое нам нужно отслеживать. Мы можем использовать составной триггер для обеспечения соблюдения правил.

Ответ №2:

Лучший способ сделать это — с помощью составного триггера. С помощью составного триггера мы избегаем проблемы изменения таблиц, которые мы получили бы от триггера уровня строк для ПЕРСОНАЛА.

Мы отслеживаем каждый проект, на который ссылается каждая затронутая строка в инструкции DML (insert, update, delete) в массиве. В конце инструкции мы запрашиваем эти проекты, чтобы выяснить, является ли проект международным, и если это так, чтобы проверить национальность назначенного ему персонала.

Это может выглядеть так:

 CREATE OR REPLACE TRIGGER international_project_trg
  FOR insert or update or delete ON personnel
    COMPOUND TRIGGER

  -- Global declaration
  type project_t is table of number index by personnel.project%type;
  g_project project_t; 

  BEFORE EACH ROW IS
  BEGIN
    CASE
      -- we don't care about the value here, we just what a set of distinct projects
      WHEN INSERTING THEN
        g_project(:new.project) := 1;
      WHEN UPDATING THEN
        g_project(:new.project) := 1;
      WHEN DELETING THEN
        g_project(:old.project) := 1;
    END CASE;
  END BEFORE EACH ROW;

  AFTER STATEMENT IS
    l_project personnel.project%type;
    l_country_cnt pls_integer;
    l_people_cnt pls_integer; 
  BEGIN
    l_project := g_project.first();
    
    while l_project is not null loop
      select count(distinct ppl.nationality)
             ,count(*) 
       into l_country_cnt
            ,l_people_cnt
       from personnel per
            join project prj on per.project  = prj.name
            join person  ppl on per.personid = ppl.id     
        where per.project = l_project
        and   prj.international = 'Y';
        
        if l_people_cnt <= 1 then
          -- either not international project or only one assigned person
          -- so we don't care
          null;
        elsif l_country_cnt <= 1 then
          raise_application_error(-20999, l_project ||' must have multi-national team membership');  
        end if;
        
        l_project := g_project.next(l_project);
        
    end loop;    
    
  END AFTER STATEMENT;

END international_project_trg;
  

Вот рабочая демонстрация на db<>fiddle. Вы можете видеть, что, хотя триггер позволяет международному проекту иметь только одного назначенного сотрудника, он выдает ошибку, когда мы добавляем второго человека той же национальности. Мы можем решить эту проблему, вставив строки в специальном порядке или, что лучше, вставив набор строк. Это проблема с применением таких бизнес-правил.

Вы можете использовать тот же подход (в том же триггере), чтобы проверить, соответствует ли количество назначенного персонала Project.NumPeopleInvolved правилу.


Примечание: составные триггеры поступили в Oracle 11gR1.

Комментарии:

1. Спасибо за ваш ответ, я до сих пор не знаю о составных триггерах, поэтому я думаю, что упражнение должно быть выполнено без него, но все равно спасибо!!

Ответ №3:

Я думаю, что следующее должно работать со вставками, удалениями и обновлениями для табличного персонала. Он просто проверяет и обновляет международную согласованность для каждого проекта, изменен ли персонал таблицы.

 CREATE TRIGGER UpdateInternationalProject
AFTER INSERT OR UPDATE OR DELETE ON Personnel
BEGIN
    SELECT name, international
    FROM Project
    AS ProjectInternational;

    FOR projectInfo IN ProjectInternational
    LOOP
        SELECT COUNT(DISTINCT nationality)
            AS numNationalities
        FROM Personnel INNER JOIN Person
        ON personId = id
        WHERE project = projectInfo.name;

        IF numNationalities = 1 THEN
            IF projectInfo.international THEN
                UPDATE Project
                SET international = 0
                WHERE name = projectInfo.name;
            END IF;
        ELIF numNationalities > 1 THEN
            IF NOT projectInfo.international THEN
                UPDATE Project
                SET international = 1
                WHERE name = projectInfo.name;
            END IF;
        END IF;
    END LOOP;
END;
  

Комментарии:

1. Это устанавливает значения для ПРОЕКТА в зависимости от количества строк в ПЕРСОНАЛЕ. Но если вы прочтете вопрос более внимательно, вы увидите, что искатель хочет обеспечить соблюдение ограничений целостности, то есть строки в PERSONAL должны идентифицировать записи PERSON, которые соответствуют правилам, определенным в PROJECT .