Sei sulla pagina 1di 58

Triggers

Exercises
01 - Authorship
Consider the following relational schema:

BOOK (Isbn, Title, SoldCopies)


WRITING (Isbn, Name)
AUTHOR (Name, SoldCopies)

Define a set of triggers for keeping SoldCopies in AUTHOR


updated with respect to:
updates on SoldCopies in BOOK
insertion of new tuples in the WRITING relation
create trigger UpdateSalesAfterNewSaleOfExistingBook
after update of SoldCopies on BOOK
for each row
update AUTHOR
set SoldCopies = SoldCopies +
+ new.SoldCopies old.SoldCopies
where Name in ( select Name
from WRITING
where Isbn = new.Isbn)

WARNING!! Author.SoldCopies is NOT equal to


Book.SoldCopies.
create trigger UpdateSalesAfterDiscoveringNewAuthorship
after insert on WRITING
for each row
update AUTHOR
set SoldCopies = SoldCopies +
( select SoldCopies
from BOOK
where Isbn = new.Isbn )
where Name = new.Name
It may seem that we should also react to the insertion of new books.
However, for the authorship to be granted, a tuple in Writing is required
too, and we already have a trigger to update the counter in that case. It
would therefore be wrong to add a trigger like the one below.

create trigger UpdateSalesForNewBook


after insert on BOOK
for each row
update AUTHOR
set SoldCopies = SoldCopies + new.SoldCopies
where Name in ( select Name
from WRITING
where Isbn = new.Isbn)
02 - Exams
Consider the following relational schema:
EXAMINATION (StudentID, Course, Date, Grade)
PRECEDENCERULE (PrecedingCourse, FollowingCourse)
With the following integrity constraint: it is forbidden to
sit in for an examination if there is a preceding course
that has not been passed yet.

a. Find the operations that can violate the constraint.


b. Based on the common sense experience, choose the
operation that will actually violate the constraint.
c. Write a trigger for that operation, that rolls back the
effects when the constraints are violated.
5 operations can possibly violate the integrity constraint:

INSERT of a new EXAMINATION, when there are


preceding courses that are not sustained.
UPDATE of the Date or Course on EXAMINATION.
DELETE of an EXAMINATION.
UPDATE of an existing PRECEDENCERULE.
INSERT of a new PRECEDENCERULE.

The operation that most typically will violate the rule is the first one.

Updates and deletions on examinations should occur exceptionally,


and only to handle specific errors. Changes to precedence rules,
instead, may affect older exams. One solution to this issue is to
extend the schema with a date from which the precedence rule is
activated and a date in which the rule is to be considered invalidated.
create trigger MissingPrecedence
after insert on EXAMINATION
for each row
when exists
( select *
from PRECEDENCERULE
where FollowingCourse = new.Course AND
PrecedingCourse not in ( select Course
from EXAMINATION
where StudentID= new.StudentID
AND Date < new.Date ) )
rollback;

Insertion is forbidden if there is a precedenceRule [FollowingCourse


= new.Course] and the corresponding preceding course is
not registered yet [Date < new.Date] for the current student
[StudentID = new.StudentID]
03 - Products
The following schema describes a hierarchy of products:
Product( Code, Name, Description, SuperProduct, Level )
Level describes the depth of the product positioning in the
hierarchy tree. Products that are not included into any
other product are considered as Level=0 products, with
SuperProduct = NULL.
P1
Hierarchy example:
P2 P3 P4 P5

P6 P7 P8 P9
1
Define a trigger for the following task:
When a product is deleted, all the sub-products
(at any level) must be deleted as well.
create trigger DeleteProduct
after delete on Product
for each row
delete from Product
where SuperProduct = old.Code
Deleting a product activates the trigger that deletes all the
directly connected subproducts (that is, the direct children
of the product). In turn, each of these activates again the
trigger for deleting the sub-subproduct (grandchildren) of
the originally deleted product. At the end, all the descendant
are deleted.
2
Write a trigger that calculates the value of the
attribute Level when a new Product is created. Note
that the new product may be child of an existing
one.
create trigger ProductLevel
after insert on Product
for each row
begin
update Product
set Level = 1 + ( select Level
from Product
where Code = new.SuperProduct )
where Code = new.Code and new.SuperProduct is not null;
update Product
set Level = 0
where Code = new.Code and new.SuperProduct is null;
end;
Some comments:
The calculation for a root product (level 0) must be
managed separately from a product positioned within
higher levels
The condition Code=new.Code is used for identifying
the tuple that has been inserted and needs to be modified.
WARNING!! Writing set new.Level = is a terribly
wrong solution! new is not a pointer to the newly
created element.
This rule calculates automatically the level of a new
product. If a product is inserted with a value for the
attribute Level, this is overwritten.
If we allow updates of the suprProduct attribute (changing the
hierarchy over time) then we should also maintain the Level

create trigger TrigSupPro_A


after update of SuperProduct on Product
when new.SuperProduct is null
update Product set Level = 0
where Code = old.Code

create trigger TrigSupPro_B


after update of SuperProduct on Product
when new.SuperProduct is not null
update Product set Level = 1 + ( select Level
from Product
where Code = new.SuperProduct)
where Code = old.Code
Alternatively, with the approach of DB2 conditioners, with before
semantics:

create trigger ProductLevel_Before_childProduct


before insert into Product
for each row
when new.SuperProduct is not null
set new.Level = 1 + ( select Level
from Product
where Code = new.SuperProduct )

create trigger ProductLevel_Before_rootProduct


before insert into Product
for each row
when new.SuperProduct is null
set new.Level = 0
And therefore propagate the new value for the level

create trigger TrigSupPro_A


after update of Level on Product
do
update Product set Level = 1 + new.Level
where SuperProduct = old.Code

This trigger recursively activates itself and cascades the update to


all sub-products
(N.B. the action and the event are the same)
The computation terminates if and only if there are no cyclic
references in the data (i.e., the tree is a properly structured tree)
3
Use a set of trigger to implement a foreign key
(pseudo-)constraint on the attribute SuperProduct.
The only acceptable values for this attribute are:
Null
the value of Code of another product (a product
cannot be superproduct of itself)
create trigger PseudoCostraint
after insert on Product
for each row
when new.SuperProduct is not null and
new.SuperProduct not in ( select Code
from Product )
rollback;
This solution still allows a product to be super-product of itself.
Indeed, when you insert the product, the tuple is added in the table
and then the trigger is activated (because the trigger starts after
insert). The nested query will then extract the newly added product
code too.
A new rule can be added for this case:
create trigger PseudoConstraintSpecial
after insert on Product
for each row
when new.SuperProduct = new.Code
rollback;
Other options:
Adding to the nested query the clause:
where Code <> new.Code,
Adding to the when clause of the first rule the condition:
new.SuperProduct = new.Code
Defining the trigger with "before" semantics: the action
is executed before the actual insertion and the nested
query does not extract the newly added Code.
As a last remark, please note that the rules introduced
so far do not prevent us from the creation of circular
references, unless we disable updates.
However, if the database is consistent (the products are
shaped in well-formed hierarchies) when the triggers
are defined, and only insertions and deletions are
allowed (no updates), then there is no way to introduce
cycles.
04 - Campus

Consider the following relational schema:


STUDENT ( ID, Name, Address, Phone, Faculty, Year,
Campus, EarnedCredits)
ENROLLMENT ( StudID, CourseCode, Year, Date)
COURSEYEAR ( CourseCode, Year, Teacher, Quarter,
#Students, #ExternalStuds)
COURSE (coursecode, Name, Credits, Campus)
1
Write a trigger that rolls back the creation of a new
Enrollment if the referenced Student and/or COURSEYEAR
does not exist in the corresponding table.
create trigger CheckEnrollment
after insert on ENROLLMENT
when not exists ( select *
from STUDENT
where StudID = new.StudID )
OR not exists
( select *
from COURSEYEAR
where CourseCode = new.CourseCode
and Year = new.Year )
Rollback;
2
The #ExternalStuds attribute in COURSEYEAR represents the
number of enrolled students to the course, that are associated
to a different Campus wrt the one where the COURSE is held.
Write a trigger that updates (if needed) the value of
COURSEYEAR.#ExternalStuds when a Student moves from a
Campus to another.
Modifying the Campus of a student has the following
effects:
the student becomes ExternalStudent for all the
courses he is enrolled into that are located in the campus
he is leaving;
the student becomes Internal Student for all his courses
located in the campus where he is moving.
There are two counters to update.
create trigger CheckCampus1
after update of Campus on STUDENT
for each row
begin
update COURSEYEAR
set #OtherCampusStuds = #OtherCampusStuds + 1
where (CourseCode,Year) in (select CourseCode, Year
from ENROLLMENT
where StudID = old.StudID)
and CourseCode in ( select CourseCode
from COURSE
where Campus = old.Campus);
end
create trigger CheckCampus2
after update of Campus on STUDENT
for each row
begin
update COURSEYEAR
set #OtherCampusStuds = #OtherCampusStuds 1
where (CourseCode,Year) in (select CourseCode, Year
from ENROLLMENT
where StudID = old.StudID)
and CourseCode in ( select CourseCode
from COURSE
where Campus = new.Campus);
end
Some comments:
old and new are fundamental for distinguishing the
values of the Campus attribute, while they are not
critical for the StudID attribute (that does not change).
if the update re-assigns to the Campus attribute the same
value, the counters remain correct.
05 - Play with me
Consider the following schema describing a system for hiring
rehearsal rooms to musical groups. Start and end time of reservations
contain only hours (minutes are always 00). Use of rooms can only
happen if there is a corresponding reservation, but can start later or
end earlier. All rooms open at 7:00 and close at 24:00.

User (SSN, Name, Email, Type)


Reservation (UserSSN, RoomCode, Date, StartTime, EndTime)
Room (RoomId, Type, CostPerHour)

1) Write a trigger that prevents the reservation of already booked


rooms
Play with me
We need to capture reservations for the same room with overlapping
periods. We can use abefore semantics to impose an early rollback
of the transaction. (N.B.: two intervals are overlapped if the first
begins before the end and ends before the beginning of the other)
create trigger ThouShallNotBook
before insert into Reservation
for each row
when exists ( select *
from Reservation
where RoomCode = new.RoomCode
and Date = new.Date and StartTime < new.EndTime
and EndTime > new.StartTime )
rollback
A side thought
Overlapping intervals... how-to?

A, B

A.start <= B.end and B.start <= A.end

33
A side thought
non overlapping intervals... How to?

A, B

A.start > B.end or B.start > A.end


De Morgan

not (A.start <= B.end and B.start <= A.end )


34
Play with me
Suppose that usage data are inserted into a table Usage only after the room
has been actually used. Enrich the schema to track the number of hours that
have been reserved but not used by each user, and write a (set of) trigger(s)
that set the type of a user to unreliable when he totalizes 50 hours of
unused reservations.
We track actual usage times and costs in the Usage table:
Usage (UserSSN, RoomCode, Date, StartTime, EndTime, Cost)
Unused hours can be counted via queries, without further schema
modifications. For efficiency reasons, however, we may want to calculate the
number incrementally, e.g., by adding a WastedHours field to table User.
How do we matche reservations and usages?
We assume that (1) the previous trigger guarantees reservations to be correct
and not overlapping and that (2) actual usages of each room only and always
happen within the limits of a reserved interval, and by those who actually
reserved the room.
Play with me
create trigger UpdateWastedHours
after insert into Usage
for each row
update User
set WastedHours = WastedHours +
( select EndTime StartTime ( new.EndTime new.StartTime )
from Reservation
where RoomCode=new.RoomCode and Date = new.Date
and StartTime>= new.StartTime and EndTime <= new.EndTime )
where SSN = new.UserSSN

This is the simplest option: the field is updated at each insertion, possibly with a zero-
increment when users are on time.
Capturing zero-increments may be done in the when clause (but is as heavy as the update)
Play with me
create trigger UpdateWastedHours
after insert into Usage
for each row
declare X integer;
when 0 < ( select EndTime StartTime ( new.EndTime new.StartTime ) into X
from Reservation
where RoomCode=new.RoomCode and Date = new.Date
and StartTime>= new.StartTime and EndTime <= new.EndTime )
update User
set WastedHours = WastedHours + X
where SSN = new.UserSSN

Now it is potentially more efficient


Play with me
The only missing part is the monitoring of the threshold:

create trigger UpdateType


after update of WastedHours on User
for each row
when old.WastedHours < 50 and new.WastedHours >= 50
do
update User
set Type = Unreliable
where SSN = old.SSN

ATTENTION:
were not considering the case in which bands do not show up at all !
Play with me
How to deal with users who dont show up at all?
- We may not accept new any new reservation for users who didnt
show up in the past (but this would be a new business rule we
should simply try to count the hours as wasted hours... What we
miss, in this case, is the triggering event)
- We may consider a new reservation a triggering event and, before
reserving, check for previous dangling reservations
- And delete them, once dealt with (in order not to count them again in
the future)
- Or, more likely, in order not to delete potentially useful data, mark
them with a flag that needs to be added to the schema
- Most likely (and simply), periodically check for these situations
we need a triggering event that is not a data modification, but rather
a system event (example: check all reservations daily, after the
change of date), as in the following trigger
Play with me
create trigger InelegantNonPortable_GhostMusicians
after change-date() // each vendor has its own extended event language
do
select * into PENDING
from Reservation R
where R.date = today()1 and not exists( select * from Usage U
where U.Date = R.Date
and R.StartTime <= U.StartTime
and U.EndTime >= R.EndTime )
for-each X in PENDING
do
update User
set WastedHours = WastedHours + X.EndTime X.StartTime
end;

This solution pre-extracts the relevant reservations into a virtual table


(PENDING) and uses a proprietary explicit iteration (for-each) in order to apply
the modifications.
A much more elegant solution, using only SQL-2, is in the next trigger
Play with me
create trigger ElegantAndPortable_GhostMusicians
after change-date() // each vendor has its own extended event language
do
update User U
set WastedHours = WastedHours +
( select sum( P.EndTime P.StartTime )
from Reservation P
where P.Date = today()1 and P.UserSSN = U.SSN
and not exists( select *
from Usage S
where S.Date = P.Date
and P.StartTime <= S.StartTime
and S.EndTime >= P.EndTime ) )
end;

Please note that the bindings on P and U take the place of the iteration, and each
user has its counter updated by the quantity depending on their specific faults.
Also note that this solution, by means of aggregation, also accounts for the case in
which the same user has left more than one pending reservation in the same day.
06 - The social concert hall
A concert hall manages information about the shows using a set of row-level triggers.
Visitors to the Web site can create an account and register a set of keywords matching
their interests. When (a) a new show is inserted into the Website, with a set of
keywords, registers users with a match with their set of keywords will receive an
email. Some of them will buy a ticket for the event. In case of (b) show cancellation or
(c) change of starting time, a notification is sent to users who bought a ticket for the
affected show. Write only the triggers for the management of events (a,b,c). Assume
that a function send-mail(ReceiverEmail, Subject, ... OtherAttributes ...) is available,
which is invoked with all the parameters required for email creation. The database
schema is:

VISITOR( VisId, Name, Email ) INTERESTS( VisId, Keyword )


SHOW( ShowId, Title, Date, StartTime ) DESCRIPTION( ShowId, Keyword )
TICKET( VisId, ShowId, Seats )
We assume that keywords are always inserted together with the show, and not updated

create rule NewShow


after insert into SHOW
for each row
send-mail( ( select Email
from ( VISITOR V join INTERESTS I on V.VisId = I.VisId )
join DESCRIPTION D on D.Keyword = I.Keyword
where D.ShowId = new.ShowId ),
New Show,
new.Title,
new.Date,
new.StartTime )
create rule CanceledShow
after delete from SHOW
for each row
send-mail( ( select Email
from VISITOR V join TICKET T on V.VisId = T.VisId
where T.ShowId = old.ShowId ),
Canceled Show ,
old.Title,
old.Date,
old.StartTime )
create rule NewTime
after update of StartTime on SHOW
for each row
send-mail( ( select Email
from VISITOR V join TICKET T on V.VisId = T.VisId
where T.ShowId = old.ShowId ),
Rescheduled Show,
old.Title,
old.Date,
new.StartTime )
07 - Orders ( Exam, 11 / 02 / 2016 )
ClientOrder ( OrderId, ProductId, Qty, ClientId, TotalSubItems )
ProductionProcess ( ProdProcId, ObtainedProdId, StartingProdId,
Qty, ProcessDuration, ProductionCost )
ProductionPlan ( BatchId, ProdProcId, Qty, OrderId )
PurchaseOrder ( PurchaseId, ProdId, Qty, OrderId )
The relational database above supports the production systems of a factory. Table
ProductionProcess describes how a product can be obtained by (possibly several) other
products, which can be themselves obtained from other products or bought from
outside.
Build a trigger system that reacts to the insertion of orders from clients and creates new
items in ProductionPlan or in PurchaseOrder, depending on the ordered product, so as
to manage the clients order (for the generation of the identifiers, use a function
GenerateId()).
The triggers should also update the value of TotalSubItems (initially always set to 0) to
describe the number of sub-products (internally produced or outsourced) that are used
overall in the production plan deriving from the order.
Also briefly discuss the termination of the trigger system.

sqliteonline: https://goo.gl/Mw4rYB
ProdProc Obtained Starting
Qty
Id ProdId ProdId

1000 1 2 4

1001 1 3 1

1002 2 4 2

1003 2 5 1

sqliteonline: https://goo.gl/QiOS01
We have to define at least the following triggers:

T1 (NewOrder) reacts to the insertion on ClientOrder and:


Adds a record in ProductionPlan if there is a process to build ProductId
Adds a record in PurchaseOrder if there is no process to build ProductId

T2 (UpdateSubItemsAfterPurchase) reacts to insertion on PurchaseOrder


Sum the ordered Qty to the TotalSubItems of the order

T3 (UpdateSubItemsAfterProduction) reacts to insertion on ProductionPlan


Sums the produced Qty to the TotalSubItems of the order

T4 (InsertSubProducts) reacts to insertion on ProductionPlan


Adds a record in ProductionPlan if there is a process to build StartingProdId
Adds a record in PurchaseOrder if there is no process to build StartingProdId
T1 (NewOrder) reacts to the insertion on ClientOrder

CREATE TRIGGER NewOrder


AFTER INSERT ON ClientOrder
FOR EACH ROW
BEGIN
IF (EXISTS (SELECT * FROM ProductionProcess
WHERE ObtainedProdId = new.ProductId))

INSERT INTO ProductionPlan


SELECT GenerateId(), ProdProcId, Qty * new.Qty, new.OrderId
FROM ProductionProcess
WHERE ObtainedProdId = new.ProductId;

ELSE
INSERT INTO PurchaseOrder VALUES
(GenerateId(), new.ProductId, new.Qty, new.OrderId);

END;
END;

sqliteonline: https://goo.gl/9QGmtp
T1 considerations:

When new.ProductId is the ObtainedProdId of a ProductionProcess, we need to


insert the records in ProductionPlan to transform its starting products into the
obtained product;

When new.ProductId isnt an ObtainedProdId of any ProductionProcess, we


need to purchase the ProductId (we are actually re-selling);

The production quantity of each Starting Product is new.Qty (the number of


new.ProductId items to produce for the order) * Qty (the number of Starting
Products needed to produce one Obtained Product).
T2 (UpdateSubItemsAfterPurchase) reacts to insertion on PurchaseOrder

CREATE TRIGGER UpdateSubItemsAfterPurchase


AFTER INSERT ON PurchaseOrder
FOR EACH ROW
BEGIN

UPDATE ClientOrder
SET TotalSubItems = TotalSubItems + new.Qty
WHERE OrderId = new.OrderId;

END;

sqliteonline: https://goo.gl/JXiSXC
T3 (UpdateSubItemsAfterProduction) reacts to insertion on ProductionPlan

CREATE TRIGGER UpdateSubItemsAfterProduction


AFTER INSERT ON ProductionPlan
FOR EACH ROW
BEGIN

UPDATE ClientOrder
SET TotalSubItems = TotalSubItems + new.Qty
WHERE OrderId = new.OrderId;

END;

sqliteonline: https://goo.gl/PKyDlJ
T4 (InsertSubProducts) reacts to insertion on ProductionPlan

CREATE TRIGGER InsertSubProducts


AFTER INSERT ON ProductionPlan
FOR EACH ROW
BEGIN
DEFINE S;
SELECT StartingProdId INTO S
FROM ProductionProcess WHERE ProdProcId = new.ProdProcId;

IF (EXISTS (SELECT * FROM ProductionProcess


WHERE ObtainedProdId = S))

INSERT INTO ProductionPlan


SELECT GenerateId(), ProdProcId, new.Qty * Qty, new.OrderId
FROM ProductionProcess WHERE ObtainedProdId = S;
ELSE
INSERT INTO PurchaseOrder VALUES
(GenerateId(), S, new.Qty, new.OrderId);
END;
END;
sqliteonline: https://goo.gl/ifrAJO
Termination of the trigger system

T4 is the only trigger that could be non-terminating

Nevertheless, if the product hierarchy is well-formed (no cycles), T4 will eventually


terminate reaching the leaves.
We can define other (optional and not required) triggers to improve the system:

T5 (Validate Order)
Validates TotalSubItems = 0
Validates Qty > 0

T6 (Delete Order)
Delete all associated PurchaseOrders
Delete all associated ProductionPlans

T7 (Disable Order Updates)


Permit updates on TotalSubItems
Disable updates on other fields

sqliteonline: https://goo.gl/JwhKT2
T5 (Validate Order)

CREATE TRIGGER NewOrder_validate


BEFORE INSERT ON ClientOrder
FOR EACH ROW
WHEN ((new.TotalSubItems <> 0) OR (new.Qty <= 0))
BEGIN

SELECT RAISE(ABORT, "Invalid Order");

END
T6 (Delete Order)

CREATE TRIGGER DeleteOrder


AFTER DELETE ON ClientOrder
FOR EACH ROW
BEGIN

DELETE FROM ProductionPlan


WHERE OrderId = old.OrderId;

DELETE FROM PurchaseOrder


WHERE OrderId = old.OrderId;

END;
T7 (Disable Order Updates)

CREATE TRIGGER DisableOrderUpdates


BEFORE UPDATE OF OrderId, ProductId, Qty, ClientId ON ClientOrder
FOR EACH ROW
BEGIN

SELECT RAISE(ABORT, "Updates on ClientOrder are disabled");

END;

Potrebbero piacerti anche