I have to solve this exercise about triggers:
Consider the following relational database schema used to represent
project information:
Person (ID, Surname, Name, Nationality)
Project (Name, Manager,
StartingYear, NumPeopleInvolved, International)
Personnel (Project, PersonID)
Specify the triggers required in Oracle to maintain the following
integrity constraints:
a) The number of people involved in a project (attribute
NumPeopleInvolved) must be consistent with the number of tuples
entered in Personnel for that project
b) If the project is international (the International attribute
assumes only two values) then the project must involve at least two
people of different nationalities
I have a problem with the b) part.
I don't know how to handle the case in which a given Project has no people involved. If I try to insert the first people, I can not have two people of different nationalities since I have only one people.
How should this situation be handled?
Should I use a statement level trigger? I have not experience with triggers so I still haven't understood well what I can / I can't do with one kind of trigger.
I tried this way but it's clearly not working as it should:
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
When you have a row-level trigger on table Personnel then you cannot run any SELECT on table Personnel within the trigger - you will get an ORA-04091: table PERSONEL is mutating ... error.
I think your teacher is expecting something like this:
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;
In reality you would not implement such requirement with a trigger, you would use a PL/SQL procedure.
Attribute NumPeopleInvolved is useless, i.e. redundant. Typically you would solve it by
UPDATE PROJECT proj
SET NumPeopleInvolved =
(SELECT COUNT(*)
FROM Personnel p
WHERE PROJECT = :new.NAME)
WHERE NAME = :new.NAME;
Such an update could be done by a trigger, for example.
Actually you would need similar triggers also on table Personnel and Person, because the personel/persons may change and project would become inconsistent. I don't know whether this should be considered by the exercise.
Imagine, on person gets released, i.e. deleted from table Person:
would the application raise an error - person cannot be released (what happens if the person dies by Corona :-))?
would be project be invalid?
would be project be automatically updated?
Then, you should never raise errors like raise_application_error(-1) - always let the user know what went wrong!
The best way to do this is with a compound trigger. With a compound trigger we avoid the problem of mutating tables which we would get from a row level trigger on PERSONNEL.
We keep track of each project which is referenced by each affected row in a DML statement (insert, update, delete) in an array. At the end of the statement we query those projects to find whether the project is international, and if it is to check the nationalities of its assigned personnel.
It might look like this:
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;
Here is a working demo on db<>fiddle. You can see that although the trigger allows for an international project to have only one assigned person it throws an error when we add a second person of the same nationality. We can solve this by inserting rows in a special order, or better by inserting a set of rows. This is a problem with apply such business rules.
You can use the same approach (in the same trigger) to check that whether the number of assigned personnel meets the Project.NumPeopleInvolved rule.
Note: compound triggers arrived in Oracle 11gR1.
I think the following should work with insertions, deletions and updates on table Personnel. It simply check and update the international consistency for each project whether the table Personnel is altered.
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;
Related
I want to update certain rows if the status of a device is changed. Here is the plain update statement:
update REMINDER set FLAG_LIST = 0 where ID in
(select r.id from REMINDER r
join DEVICE d
on d.id = (regexp_replace(r.origin_values, '[^0-9]', ''))
and d.status <> 0
and r.context = 'DEVICE'
and r.flag_list <> 0);
I wrote a trigger like below but I get an ORA-04091 (table DEVICE is mutating, trigger/function may not see ist) when i change the status of a device:
create or replace TRIGGER DEACTIVATE_REMINDER
AFTER UPDATE OF STATUS ON DEVICE
for each row
BEGIN
if updating then
if (:old.STATUS = 0 and :new.STATUS != 0) then
update REMINDER set FLAG_LIST = 0 where ID in
(select r.ID from REMINDER r
join DEVICE d
on d.id = (regexp_replace(r.ORIGIN_VALUES, '[^0-9]', ''))
and d.STATUS <> 0
and d.ID = :new.ID
and r.CONTEXT = 'DEVICE'
and r.FLAG_LIST <> 0);
end if;
end if;
END;
Maybe i can not use a trigger here, because the same object whose modification triggered the trigger must not be modified or read in a row-level trigger? Is it better to use a scheduled job somehow?
What I also want to do is to update the FLAG_LIST back to 1, if the STATUS of a device is changed back to 0. The only way i can imagine to do this is also in the trigger, but as i wrote I`m not sure if this is basically possible.
Thank you in advance!
A row-level trigger on device can't query the device table. My wager, though, is that you don't need to and can simply use the :new pseudorecord (note that the :new.status predicate in the subquery is redundant here given the if statement but doesn't hurt anything)
create or replace TRIGGER DEACTIVATE_REMINDER
AFTER UPDATE OF STATUS ON DEVICE
for each row
BEGIN
if updating then
if (:old.STATUS = 0 and :new.STATUS != 0) then
update REMINDER set FLAG_LIST = 0 where ID in
(select r.ID
from REMINDER r
where :new.id = (regexp_replace(r.ORIGIN_VALUES, '[^0-9]', ''))
and :new.STATUS <> 0
and r.CONTEXT = 'DEVICE'
and r.FLAG_LIST <> 0);
end if;
end if;
END;
If that's not what you want, it would be helpful to provide a full test case that we can run to see the behavior you want. You could create a compound trigger with a row-level component that stores the modified id values in a collection and then a statement-level component that updates the table using that collection. But that seems like overkill in this situation.
Architecturally, however, I am always really hesitant about designs that put this sort of logic in triggers. It will virtually always make more sense to have a stored procedure that updates the device table and then runs whatever update you want on the reminder table. You won't need to be concerned about a mutating table exception, all the logic will be in one place rather than partially in an application and partially in a trigger, you won't encounter problems if two months from now you want to create a trigger on the reminder table that involves queries against device, etc.
I'am just exploring Trigger in Oracle.
I have table like this
Note : WT_ID Id is FK of Water Summary but not have constraint(not directly connected)
I want to make trigger in Temp_tank table, if there are update in Table Temp_tank, it will sum all temp_tank volume with same WT_ID then updated it to Water_summary.Water_Use. Because of bussiness requirement, not all water_summary data will update. in this example only Home A will be affected
This is MyCode
CREATE OR REPLACE TRIGGER UPD_WaterUse
AFTER UPDATE ON Temp_tank
DECLARE
temp_wat number;
homeA_id= 1;
BEGIN
IF (WT_ID = homeA_id) THEN
SELECT SUM(ss.Volume) INTO temp_wat
from Temp_tank ss WHERE ss.Daytime = DAYTIME and ss.WT_ID =homeA_id;
-- functionUpdate(homeA_id,Daytime,temp_wat) ;
ELSE
NULL;
END IF;
END;
/
The question is, in line
IF (WT_ID = homeA_id) THEN
when i compiled, the line is ignored because WT_ID is not identifier.
is trigger cannot accept this style of code?
I will try to present my problem as simplified as possible.
Assume that we have 3 tables in Oracle 11g.
Persons (person_id, name, surname, status, etc )
Actions (action_id, person_id, action_value, action_date, calculated_flag)
Calculations (calculation_id, person_id,computed_value,computed_date)
What I want is for each person that meets certain criteria (let's say status=3)
I should get the sum of action_values from the Actions table where calculated_flag=0. (something like this select sum(action_value) from Actions where calculated_flag=0 and person_id=current_id).
Then I shall use that sum in a some kind of formula and update the Calculations table for that specific person_id.
update Calculations set computed_value=newvalue, computed_date=sysdate
where person_id=current_id
After that calculated_flag for participated rows will be set to 1.
update Actions set calculated_flag=1
where calculated_flag=0 and person_id=current_id
Now this can be easily done sequentially, by creating a cursor that will run through Persons table and then execute each action needed for the specific person.
(I don't provide the code for the sequential solution as the above is just an example that resembles my real-world setup.)
The problem is that we are talking about quite big amount of data and sequential approach seems like a waste of computational time.
It seems to me that this task could be performed in parallel for number of person_ids.
So the question is:
Can this kind of task be performed using parallelization in PL/SQL?
What would the solution look like? That is, what special packages (e.g. DBMS_PARALLEL_EXECUTE), keywords (e.g. bulk collect), methods should be used and in what manner?
Also, should I have any concerns about partial failure of parallel updates?
Note that I am not quite familiar with parallel programming with PL/SQL.
Thanks.
Edit 1.
Here my pseudo code for my sequential solution
procedure sequential_solution is
cursor persons_of_interest is
select person_id from persons
where status = 3;
tempvalue number;
newvalue number;
begin
for person in persons_of_interest
loop
begin
savepoint personsp;
--step 1
select sum(action_value) into tempvalue
from actions
where calculated_flag = 0
and person_id = person.person_id;
newvalue := dosomemorecalculations(tempvalue);
--step 2
update calculations set computed_value = newvalue, computed_date = sysdate
where person_id = person.person_id;
--step 3
update actions set calculated_flag = 1;
where calculated_flag = 0 and person_id = person.person_id;
--step 4 (didn't mention this step before - sorry)
insert into actions
( person_id, action_value, action_date, calculated_flag )
values
( person.person_id, 100, sysdate, 0 );
exception
when others then
rollback to personsp;
-- this call is defined with pragma AUTONOMOUS_TRANSACTION:
log_failure(person_id);
end;
end loop;
end;
Now, how would I speed up the above either with forall and bulk colletct or with parallel programming Under the following constrains:
proper memory management (taking into consideration large amount of data)
For a single person if one part of the step sequence fails - all steps should be rolled back and the failure logged.
I can propose the following. Let's say you have 1 000 000 rows in persons table, and you want to process 10 000 persons per iteration. So you can do it in this way:
declare
id_from persons.person_id%type;
id_to persons.person_id%type;
calc_date date := sysdate;
begin
for i in 1 .. 100 loop
id_from := (i - 1) * 10000;
id_to := i * 10000;
-- Updating Calculations table, errors are logged into err$_calculations table
merge into Calculations c
using (select p.person_id, sum(action_value) newvalue
from Actions a join persons p on p.person_id = a.person_id
where a.calculated_flag = 0
and p.status = 3
and p.person_id between id_from and id_to
group by p.person_id) s
on (s.person_id = c.person_id)
when matched then update
set c.computed_value = s.newvalue,
c.computed_date = calc_date
log errors into err$_calculations reject limit unlimited;
-- updating actions table only for those person_id which had no errors:
merge into actions a
using (select distinct p.person_id
from persons p join Calculations c on p.person_id = c.person_id
where c.computed_date = calc_date
and p.person_id between id_from and id_to)
on (c.person_id = p.person_id)
when matched then update
set a.calculated_flag = 1;
-- inserting list of persons for who calculations were successful
insert into actions (person_id, action_value, action_date, calculated_flag)
select distinct p.person_id, 100, calc_date, 0
from persons p join Calculations c on p.person_id = c.person_id
where c.computed_date = calc_date
and p.person_id between id_from and id_to;
commit;
end loop;
end;
How it works:
You split the data in persons table into chunks about 10000 rows (depends on gaps in numbers of ID's, max value of i * 10000 should be knowingly more than maximal person_id)
You make a calculation in the MERGE statement and update the Calculations table
LOG ERRORS clause prevents exceptions. If an error occurs, the row with the error will not be updated, but it will be inserted into a table for errors logging. The execution will not be interrupted. To create this table, execute:
begin
DBMS_ERRLOG.CREATE_ERROR_LOG('CALCULATIONS');
end;
The table err$_calculations will be created. More information about DBMS_ERRLOG package see in the documentation.
The second MERGE statement sets calculated_flag = 1 only for rows, where no errors occured. INSERT statement inserts the these rows into actions table. These rows could be found just with the select from Calculations table.
Also, I added variables id_from and id_to to calculate ID's range to update, and the variable calc_date to make sure that all rows updated in first MERGE statement could be found later by date.
I am relatively new to PL/SQL and i am trying to create a trigger that will alert me after an UPDATE on a table Review. When it is updated I want to ge the username(User table), score(Review Table), and product name (Product Table) and print them out:
This is what I have so far:
three tables:
Review: score, userid,pid, rid
Users: userid,uname
Product: pid,pname
So Review can reference the other tables with forigen keys.
create or replace trigger userNameTrigger
after insert on review
for each row
declare
x varchar(256);
y varchar(256);
z varchar(256);
begin
select uname into x , pname into y , score into z
from review r , product p , users u
where r.pid = p.pid and r.userid = u.userid and r.rid =new.rid;
dbms_output.put_line('user: '|| X||'entered a new review for Product: '|| Y || 'with a review score of: '|| Z);
end;
The problem I am having is I cannot seem to figure out how to store the selected fields into the variables and output it correctly.
DDL:
Create Table Review
(
score varchar2(100)
, userid varchar2(100)
, pid varchar2(100)
, rid varchar2(100)
);
Create Table Users
(
userid varchar2(100)
, uname varchar2(100)
);
Create Table Product
(
pid varchar2(100)
, pname varchar2(100)
);
The first problem I can see is that you're missing a colon when you refer to new.rid. The second is that you're accessing the review table inside a row-level trigger on that same table, which will give you a mutating table error at some point; but you don't need to as all the data from the inserted row is in the new pseudorow.
create or replace trigger userNameTrigger
after insert on review
for each row
declare
l_uname users.uname%type;
l_pname product.pname%type;
begin
select u.uname into l_uname
from users u
where u.userid = :new.userid;
select p.pname
into l_pname
from product
where p.pid = :new.pid;
dbms_output.put_line('user '|| l_uname
|| ' entered a new review for product ' || l_pname
|| ' with a review score of '|| :new.score);
end;
The bigger problem is that the only person who could see the message is the user inserting tow row, which seems a bit pointless; and they would have to have output enabled in their session to see it.
If you're trying to log that so someone else can see it then store it in a table or write it to a file. As the review table can be queried anyway it seems a bit redundant though.
Having all your table columns as strings is also not good - don't store numeric values (e.g. scores, and probably the ID fields) or dates as strings, use the correct data types. It will save you a lot of pain later. You also don't seem to have any referential integrity (primary/foreign key) constraints - so you can review a product that doesn't exist, for instance, which will cause a no-data-found exception in the trigger.
It makes really no sense to use a trigger to notify themselves about changed rows. If you insert new rows into the table, then you have all info about them. Why not something like the block below instead a trigger:
create table reviews as select 0 as rid, 0 as userid, 0 as score, 0 as pid from dual where 1=0;
create table users as select 101 as userid, cast('nobody' as varchar2(100)) as uname from dual;
create table products as select 1001 as pid, cast('prod 1001' as varchar2(100)) as pname from dual;
<<my>>declare newreview reviews%rowtype; uname users.uname%type; pname products.pname%type; begin
insert into reviews values(1,101,10,1001) returning rid,userid,score,pid into newreview;
select uname, pname into my.uname, my.pname
from users u natural join products p
where u.userid = newreview.userid and p.pid = newreview.pid
;
dbms_output.put_line('user: '||my.uname||' entered a new review for Product: '||my.pname||' with a review score of: '||newreview.score);
end;
/
output: user: nobody entered a new review for Product: prod 1001 with a review score of: 10
In order to inform another session about an event you should use dbms_alert (transactional) or dbms_pipe (non transactional) packages. An example of dbms_alert:
create or replace trigger new_review_trig after insert on reviews for each row
begin
dbms_alert.signal('new_review_alert', 'signal on last rid='||:new.rid);
end;
/
Run the following block in another session (new window, worksheet, sqlplus or whatever else). It will be blocked until the registered signal is arrived:
<<observer>>declare message varchar2(400); status integer; uname users.uname%type; pname products.pname%type; score reviews.score%type;
begin
dbms_alert.register('new_review_alert');
dbms_alert.waitone('new_review_alert', observer.message, observer.status);
if status != 0 then raise_application_error(-20001, 'observer: wait on new_review_alert error'); end if;
select uname, pname, score into observer.uname, observer.pname, observer.score
from reviews join users using(userid) join products using (pid)
where rid = regexp_substr(observer.message, '\w+\s?rid=(\d+)', 1,1,null,1)
;
dbms_output.put_line('observer: new_review_alert for user='||observer.uname||',product='||observer.pname||': score='||observer.score);
end;
/
Now in your session:
insert into reviews values(2, 101,7,1001);
commit; --no alerting before commit
The another (observer) session will be finished with the output:
observer: new_review_alert for user=nobody,product=prod 1001: score=7
P.S. There was no RID in the Table REVIEW, so i'll just assume it was supposed to be PID.
create or replace trigger userNameTrigger
after insert on review
for each row
declare
x varchar2(256);
y varchar2(256);
z varchar2(256);
BEGIN
select uname
, pname
, score
INTO x
, y
, z
from review r
, product p
, users u
where r.pid = p.pid
and r.userid = u.userid
and r.PID = :new.pid;
dbms_output.put_line('user: '|| X ||'entered a new review for Product: '|| Y || 'with a review score of: '|| Z);
end userNameTrigger;
You just made a mistake on the INTO statement, you can just clump them together in one INTO.
Is there a way to merge customer accounts in Magento? I have a customer who's email got corrupted (or so they say) so they created a new account. I'd like to be able to merge the history of the old customer into the new. Is this possible?
Assuming you have permission to create a MySQL stored routine and you have the information_schema database at your disposal, the following code will merge customers. Note that after creating the routine, you'll need to run it with the entity_id's of the customers as the parameters.
drop procedure if exists merge_customers;
delimiter //
create procedure merge_customers(
id1 int unsigned, -- the customer to keep
id2 int unsigned) -- the record to delete
begin
declare done int default false;
declare tbl_name, col_name varchar(255);
declare c cursor for
select table_name, column_name
from information_schema.key_column_usage
where table_schema = database()
and referenced_table_name = 'customer_entity'
and referenced_column_name = 'entity_id';
declare continue handler for not found set done = true;
open c;
read_loop: loop
fetch c into tbl_name, col_name;
if done then
leave read_loop;
end if;
set #str = concat(
'UPDATE IGNORE ', tbl_name,
' SET ', col_name, ' = ', id1,
' WHERE ', col_name, ' = ', id2
);
prepare stmt from #str;
execute stmt;
deallocate prepare stmt;
end loop;
close c;
delete from customer_entity where entity_id = id2;
end//
delimiter;
You could refund the orders of one account (which doesn't actually refund any charges) and submit identical ones under the other account using, say, the Check payment method (which doesn't actually make any charges). That way you have a paper trail of activity which wouldn't happen if you altered the database directly.
Also, ask yourself could this person be up to no good? How certain are you they are who they say they are? Simply knowing their pet's name or whichever secret question only proves they have seen the first person's facebook page. What does it even mean for an email to get "corrupted"?
I'm looking for the same thing. Definitely a needed extension.
I currently change the Customer_ID manually in phpMyAdmin in both tables sales_flat_order & sales_flat_order_grid. You can find the customer ID in your magento Admin, second column in the customers section.
But it is VERY time consuming, it would be so great if someone could write a module, I'd pay for it.