|| SELECT, UPDATE on AUCTION_ITEMS || INSERT on BIDS || SELECT on HIGH_BIDS || || Execution Requirements: || */ AS /* || exceptions raised and handled in PLACE_BID || procedure
Trang 1|| SELECT, UPDATE on AUCTION_ITEMS
|| INSERT on BIDS
|| SELECT on HIGH_BIDS
||
|| Execution Requirements:
||
*/
AS
/*
|| exceptions raised and handled in PLACE_BID
|| procedure
*/
invalid_item EXCEPTION;
bid_too_low EXCEPTION;
item_is_closed EXCEPTION;
/*
|| place a bid on an item, the bid must exceed any
|| other bids on the item (and the minimum bid)
||
|| bidding on an item registers interest in the
|| item using DBMS_ALERT.REGISTER
||
|| only this procedure should be used to add rows
|| to the bids table, since it also updates
|| auction_items.curr_bid column
*/
PROCEDURE place_bid
(item_id_IN IN VARCHAR2
,bid_IN IN NUMBER);
/*
|| close bidding on an item
*/
PROCEDURE close_item(item_id_IN IN VARCHAR2);
/*
|| watch for any alerts on items bid by the user
|| indicating other users have raised the bid
*/
PROCEDURE watch_bidding(timeout_secs_IN IN NUMBER:=300);
END auction;
/
3.2.3.5.1 Place_bid procedure
The place_bid procedure is intended to be used by the GUI application to place all bids in the auction No INSERTS or UPDATES to the BIDS table should be allowed except through this procedure, as it maintains the complex integrity constraint on the table, updates the curr_bid column of AUCTION_ITEMS, and
registers the session for receiving alerts on the item The body of place_bid looks like this:
/* Filename on companion disk: auction1.sql */*
PROCEDURE place_bid
(item_id_IN IN VARCHAR2
,bid_IN IN NUMBER)
IS
temp_curr_bid auction_items.curr_bid%TYPE;
temp_statusauction_items.status%TYPE;
CURSOR auction_item_cur
IS
SELECT NVL(curr_bid,min_bid), status
FROM auction_items
WHERE id = item_id_IN
Trang 2FOR UPDATE OF curr_bid;
BEGIN
/*
|| lock row in auction_items
*/
OPEN auction_item_cur;
FETCH auction_item_cur INTO temp_curr_bid, temp_status;
/*
|| do some validity checks
*/
IF auction_item_cur%NOTFOUND
THEN
RAISE invalid_item;
ELSIF temp_status = 'CLOSED'
THEN
RAISE item_is_closed;
ELSIF bid_IN <= temp_curr_bid
THEN
RAISE bid_too_low;
ELSE
/*
|| insert to bids AND update auction_items,
|| bidders identified by session username
*/
INSERT INTO bids (bidder, item_id, bid)
VALUES (USER, item_id_IN, bid_IN);
UPDATE auction_items
SET curr_bid = bid_IN
WHERE CURRENT OF auction_item_cur;
/*
|| commit is important because it will send
|| the alert notifications out on the item
*/
COMMIT;
/*
|| register for alerts on item since bidding,
|| register after commit to avoid ORU−10002
*/
DBMS_ALERT.REGISTER(item_id_IN);
END IF;
CLOSE auction_item_cur;
EXCEPTION
WHEN invalid_item
THEN
ROLLBACK WORK;
RAISE_APPLICATION_ERROR
(−20002,'PLACE_BID ERR: invalid item');
WHEN bid_too_low
THEN
ROLLBACK WORK;
RAISE_APPLICATION_ERROR
(−20003,'PLACE_BID ERR: bid too low');
WHEN item_is_closed
THEN
ROLLBACK WORK;
RAISE_APPLICATION_ERROR
Trang 3(−20004,'PLACE_BID ERR: item is closed');
WHEN OTHERS
THEN
ROLLBACK WORK;
RAISE;
END place_bid;
There are a few things to notice about place_bid First, the row in AUCTION_ITEMS is locked FOR
UPDATE to begin the transaction I chose not to use NOWAIT in the cursor, because the transaction is small and should be quite fast, minimizing contention problems The COMMIT immediately follows the INSERT and UPDATE and precedes the call to DBMS_ALERT.REGISTER Originally I had it the other way around, but kept getting ORU−10002 errors when calling DBMS_ALERT.WAITANY immediately after place_bid What was happening was that the call to DBMS_ALERT.REGISTER was holding a user lock that the insert trigger to BIDS was also trying to get By doing the COMMIT first, the trigger is able to acquire and release the lock, which can then be acquired by DBMS_ALERT.REGISTER.
NOTE: To avoid the locking problems mentioned, be careful to code applications in such a
way that a COMMIT will occur between calls to SIGNAL and REGISTER.
3.2.3.5.2 Exception handling
Exception handling in place_bid is inelegant but useful The package defines named exceptions that place_bid detects, raises, and then handles using RAISE_APPLICATION_ERROR In practice, it may be better to pass these out from the procedure to the calling application and let it handle them Since I was prototyping in SQL*Plus and wanted to see the exception and an error message immediately, I used
RAISE_APPLICATION_ERROR When using DBMS_ALERT, note also that it is very important to
terminate transactions to avoid the locking problems mentioned earlier, so the EXCEPTION section makes sure to include ROLLBACK WORK statements.
3.2.3.5.3 The watch_bidding procedure
With the triggers and the place_bid procedure in place, the online auction system is basically ready to go Since a real application would involve a GUI, but I was prototyping in SQL*Plus, I needed a way to simulate what the GUI should do to receive DBMS_ALERT signals and inform the user of auction activity This is basically what the watch_bidding procedure does It could be modified and called directly from the GUI or its logic could be adapted and embedded into the GUI The watch_bidding procedure uses DBMS_OUTPUT to display bidding alerts received It also demonstrates the use of the alert message to implement a
context−sensitive response to alerts.
Here is the source code for watch_bidding:
/* Filename on companion disk:auction1.sql */*
PROCEDURE watch_bidding(timeout_secs_IN IN NUMBER:=300)
IS
temp_nameVARCHAR2(30);
temp_message VARCHAR2(1800);
temp_statusINTEGER;
BEGIN
/*
|| enter a loop which will be exited explicitly
|| when a new bid from another user received or
|| DBMS_ALERT.WAITANY call times out
*/
LOOP
/*
|| wait for up to timeout_secs_IN for any alert
*/
DBMS_ALERT.WAITANY
(temp_name, temp_message, temp_status, timeout_secs_IN);
Trang 4IF temp_status = 1
THEN
/*
|| timed out, return control to application
|| so it can do something here if necessary
*/
EXIT;
ELSIF temp_message = 'CLOSED'
THEN
/*
|| unregister closed item, re−enter loop
*/
DBMS_ALERT.REMOVE(temp_name);
DBMS_OUTPUT.PUT_LINE('Item '||temp_name||
' has been closed.');
ELSIF temp_message = USER OR temp_message = 'OPEN'
THEN
/*
|| bid was posted by this user (no need to alert)
|| re−enter loop and wait for another
*/
NULL;
ELSE
/*
|| someone raised the bid on an item this user is bidding || on, application should refresh user's display with a || query on the high_bids view and/or alert visually
|| (we will just display a message)
||
|| exit loop and return control to user so they can bid */
DBMS_OUTPUT.PUT_LINE
('Item '||temp_name||' has new bid: '||
TO_CHAR(curr_bid(temp_name),'$999,999.00')||
' placed by: '||temp_message);
EXIT;
END IF;
END LOOP;
END watch_bidding;
The watch_bidding procedure uses DBMS_ALERT.WAITANY to wait for any alerts for which the session has registered In the auction system, registering for alerts is done when a bid is placed using place_bid When
an alert is received, the name of the alert is the auction item that has been updated The alert message is used
to respond intelligently to the alert as follows:
•
If the alert signals that bidding is closed on the item, the procedure unregisters the alert using
DBMS_ALERT.REMOVE and waits for another alert.
•
If the alert was raised by the current user placing a bid or indicates that bidding has been opened on the item, the procedure waits for another alert.
•
If the DBMS_ALERT.WAITANY call times out, control is passed back to the caller.
•
Trang 5If the alert is raised by another user, a message is displayed and control is passed back to the caller (so the user can make a new bid).
3.2.3.6 Testing the system
So the question is, does it work? I inserted some rows into the AUCTION_ITEMS table as follows:
/* Filename on companion disk: auction3.sql */*
INSERT INTO auction_items
VALUES ('GB123','Antique gold bracelet',350.00,NULL,'OPEN');
INSERT INTO auction_items
VALUES ('PS447','Paul Stankard paperweight',550.00,NULL,'OPEN');
INSERT INTO auction_items
VALUES ('SC993','Schimmel print',750.00,NULL,'OPEN');
COMMIT;
I granted EXECUTE privilege on the AUCTION package to two users, USER01 and USER02, and connected two SQL*Plus sessions, one for each user Then I initiated a bidding war using the following PL/SQL block:
/* Filename on companion disk: auction4.sql */*
set serveroutput on size 100000
set verify off
BEGIN
opbip.auction.place_bid('GB123',&bid);
opbip.auction.watch_bidding(300);
END;
/
On each execution of the previous block in each session, I raised the bid Here are the results from the
USER01 session:
SQL> @auction4
Enter value for bid: 1000
Item GB123 has new bid: $1,100.00 placed by: USER02
PL/SQL procedure successfully completed
SQL> /
Enter value for bid: 1200
Item GB123 has new bid: $1,300.00 placed by: USER02
PL/SQL procedure successfully completed
USER01 opened the bidding on GB123 with a $1000 bid, which was quickly upped by USER02 to $1100, so USER01 came back with $1200 only to be topped finally by the winning $1300 bid by USER02 USER02's log of events confirms this exciting back and forth bidding war:
SQL> @auction4
Enter value for bid: 1100
Item PS447 has been closed
Item GB123 has new bid: $1,200.00 placed by: USER01
PL/SQL procedure successfully completed
SQL> /
Enter value for bid: 1300
PL/SQL procedure successfully completed