|| 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 [Appendix A] What's on the Companion Disk? 3.2.3 DBMS_ALERT Examples 196 FOR 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 [Appendix A] What's on the Companion Disk? 3.2.3 DBMS_ALERT Examples 197 (−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); [Appendix A] What's on the Companion Disk? 3.2.3 DBMS_ALERT Examples 198 IF 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. • [Appendix A] What's on the Companion Disk? 3.2.3 DBMS_ALERT Examples 199 If 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. [Appendix A] What's on the Companion Disk? 3.2.3 DBMS_ALERT Examples 200