Theory, Practice & Methodology of Relational Database Design and Programming Copyright © Ellis Cohen 2002-2008 Embedded Database Programming Using PL/SQL These slides are licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 2.5 License. For more information on how you may use them, please see http://www.openlineconsult.com/db 1 Overview of Lecture Basic PL/SQL Conditional Statements CASE Expressions & Statements Loops Stored Functions Stored Procedures Stored Packages Sequential Values Exceptions Record Types Query-Based FOR Loops Dynamic SQL © Ellis Cohen 2001-2008 2 Basic PL/SQL © Ellis Cohen 2001-2008 3 PL/SQL Block DECLARE <declaration statements> BEGIN <executable statements> EXCEPTION <exception handling code> END; © Ellis Cohen 2001-2008 4 Simplest Block BEGIN Do Nothing NULL; END; This block is anonymous (or unnamed) © Ellis Cohen 2001-2008 5 Hello World Anonymous (unnamed) PL/SQL block invoked directly from SQL*Plus Built-in package Procedure in package SQL> BEGIN dbms_output.put_line( 'Hello World' ); END; / SQL*Plus knows how to parse pure SQL, but not PL/SQL, so it doesn't know when a PL/SQL block actually ends. Use / to tell it. © Ellis Cohen 2001-2008 6 Hello World via Stored Procedure SQL> CREATE PROCEDURE pl( str varchar ) IS BEGIN dbms_output.put_line( str ); END; / SQL> BEGIN pl( 'Hello World' ); END; / © Ellis Cohen 2001-2008 7 SQL*Plus EXECUTE SQL> EXECUTE pl('Hello World') automatically translated into SQL> BEGIN pl( 'Hello World' ); END; / © Ellis Cohen 2001-2008 8 Declaring & Using Variables Adding 100 to salary of employee 7876 SQL> PL/SQL variable DECLARE declaration & mgrno int := 7876; initialization BEGIN UPDATE Emps SET sal = sal + 100 WHERE empno = mgrno; END; / Using PL/SQL variable in SQL command PL/SQL does not use a : prefix to identify variables (e.g. :mgrno) so there is no obvious way to distinguish attributes and variables © Ellis Cohen 2001-2008 9 Naming Conflicts Adding 100 to salary of employee 7876 DECLARE mgrno int := 7876; BEGIN UPDATE Emps SET sal = sal + 100 WHERE empno = mgrno; END; Adding 100 to salary of all employees who are their own manager DECLARE mgr int := 7876; BEGIN UPDATE Emps SET sal = sal + 100 WHERE empno = mgr; END; The mgr attribute of Emps overrides the mgr variable declaration © Ellis Cohen 2001-2008 10 Named/Labelled Blocks Adding 100 to salary of employee 7876 This block is named myBlock <<myBlock>> DECLARE mgr int := 7876; BEGIN UPDATE Emps SET sal = sal + 100 WHERE empno = myBlock.mgr; END; Indicates that the variable mgr declared in myBlock should be used instead of emp's mgr attribute. © Ellis Cohen 2001-2008 11 Using PL/SQL Variables PL/SQL variable UPDATE Emps SET sal = (sal + max_sal) / 2 WHERE empno = 7876; PL/SQL variable DELETE FROM Projs WHERE pname = badPname; INSERT INTO Projs( pno, pname, pmgr ) VALUES( aPno, aPname, aPmgr ); You can use a PL/SQL variable wherever you can use a constant in SQL PL/SQL variable © Ellis Cohen 2001-2008 12 PL/SQL is a Typed Language SQL Data Types: – CHAR(n), VARCHAR(n), LONG – NUMBER(n1,n2), INT/INTEGER, DEC/DECIMAL, SMALLINT, FLOAT/REAL – DATE Added PL/SQL-only Data Types – BOOLEAN – PLS_INTEGER -- fast internal integer (can't be NULL) – structured types (defined later) © Ellis Cohen 2001-2008 13 Variable Declarations job_title varchar(80) := 'Salesman'; the_date date := NULL; is_cool boolean; /* init to NULL */; counter int := 7; Anchored incr CONSTANT int := 1; declaration: Whatever the type is anEname varchar(30) of the attribute NOT NULL := 'NONE'; Emps.empno aDeptno number(5); anEmpno Emps.empno%TYPE; xEmpno anEmpno%TYPE; © Ellis Cohen 2001-2008 14 Assignment Statements aDeptno := 10; counter := counter + 1; SELECT ename, deptno INTO anEname, aDeptno FROM Emps WHERE empno = 7876; SELECT count(*) INTO counter FROM Emps WHERE deptno = 40; Raises exception • if > 1 row selected, or • if 0 rows selected Useful Idiom! Succeeds no matter how many rows match the WHERE clause © Ellis Cohen 2001-2008 15 Using Single Row Results DECLARE anEname varchar(30); aDeptno int; BEGIN SELECT ename, deptno INTO anEname, aDeptno FROM Emps WHERE empno = 7876; pl( anEname || ' ' || aDeptno ); END; © Ellis Cohen 2001-2008 16 Variables vs Simple Scalar Subqueries UPDATE Emps SET sal = sal + 100 WHERE job = (SELECT job FROM Emps WHERE ename = 'BLAKE') Only slight performance penalty Choice is primarily aesthetic DECLARE blakejob varchar(20); BEGIN SELECT job INTO blakejob FROM Emps WHERE ename = 'BLAKE'; UPDATE Emps SET sal = sal + 100 WHERE job = blakejob; END; © Ellis Cohen 2001-2008 17 RETURNING INTO Delete & Update DELETE FROM CopyEmps WHERE empno = 7876 RETURNING ename, deptno INTO anEname, aDeptno; Returns information about deleted row • Raises exception if > 1 rows deleted • No effect if no rows are deleted UPDATE CopyEmps SET sal = sal + 100 WHERE empno = 7876 Return value of sal RETURNING ename, sal after update INTO anEname, aSal; Returns information about updated row • Raises exception if > 1 rows updated • No effect if no rows are updated © Ellis Cohen 2001-2008 18 RETURNING INTO Insert INSERT INTO CopyEmps( empno, ename, deptno, sal ) VALUES( 6144, 'SONI', 40, getDeptSal( 40 ) ) RETURNING sal INTO theSal; Returns information about inserted values INSERT INTO CopyEmps SELECT * FROM Emps WHERE empno = 7876 RETURNING ename, deptno INTO anEname, aDeptno; Not legal, sigh …. © Ellis Cohen 2001-2008 19 Conditional Statements © Ellis Cohen 2001-2008 20 Conditional Statements IF profit > 50000 THEN UPDATE Emps SET sal = sal + 200; UPDATE Bonus SET amt = .004 WHERE job = 'DEPTMGR'; END IF; IF profit > 50000 THEN UPDATE Emps SET sal = sal + 200; UPDATE Bonus SET amt = .004 WHERE job = 'DEPTMGR'; ELSE UPDATE Emps SET sal = sal + 50 WHERE job = 'DEPTMGR'; END IF; © Ellis Cohen 2001-2008 This is a PL/SQL variable. It does not identify data in a DB table 21 Using NULLs in Conditional Statements IF profit > 50000 THEN UPDATE Emps SET sal = sal + 200; UPDATE Bonus SET amt = .004 WHERE job = 'DEPTMGR'; END IF; IF profit <= 50000 THEN NULL; ELSE UPDATE Emps SET sal = sal + 200; UPDATE Bonus SET amt = .004 WHERE job = 'DEPTMGR'; END IF; Equivalent if profit is not NULL © Ellis Cohen 2001-2008 22 Using NULL Statements to Avoid NULL Checks IF a = b THEN NULL; ELSE DoSomething(); END IF; Suppose both a and b can be NULL Rewrite the above statement with only a THEN clause. © Ellis Cohen 2001-2008 23 Doing NULL Checks IF a = b THEN NULL; ELSE DoSomething(); END IF; Equivalent IF (a != b) OR (a IS NULL) OR (b IS NULL) THEN DoSomething(); END IF; © Ellis Cohen 2001-2008 24 Conditionals with ELSIF IF profit > 50000 THEN UPDATE Emps SET sal = sal + 200; UPDATE Bonus SET pct = .004 WHERE job = 'DEPTMGR'; ELSIF profit > 20000 THEN UPDATE Emps SET sal = sal + 50 WHERE job = 'DEPTMGR'; ELSIF profit > 0 THEN NULL; ELSE DELETE * FROM Emps WHERE job = 'CLERK'; END IF; © Ellis Cohen 2001-2008 25 Insert/Update Exercise Write an anonymous PL/SQL block that does the following to the Projs table: if project #30420 is not in the table, INSERT project #30420 with pname: 'My Project', and pmgr: 2020 else if it already exists, UPDATE project #30420 with pname: 'Your Project', and pmgr: 2020 © Ellis Cohen 2001-2008 26 Can’t Coerce Scalar Result Sets DECLARE thePno int := (SELECT pno FROM Projs WHERE pno = 30420); BEGIN IF thePno IS NULL THEN INSERT INTO Projs( pno, pname, pmgr ) VALUES( 30420, 'My Project', 2020 ); ELSE UPDATE Projs SET pname = 'Your Project', pmgr = 2020 WHERE pno = 30420; END IF; END; DOESN'T WORK! The SELECT statement produces a result set, not an integer © Ellis Cohen 2001-2008 27 Scalar Coercion Doesn’t Work BEGIN IF (SELECT count(*) FROM Projs WHERE pno = 30420) = 0 THEN INSERT INTO Projs( pno, pname, pmgr ) VALUES( 30420, 'My Project', 2020 ); ELSE UPDATE Projs SET pname = 'Your Project', pmgr = 2020 WHERE pno = 30420; END IF; END; DOESN'T WORK! Coercion of Scalar Result Sets to scalar values only works in a SQL Query © Ellis Cohen 2001-2008 28 Exists Doesn't Work BEGIN IF exists( SELECT * FROM Projs WHERE pno = 30420 ) THEN UPDATE Projs SET pname = 'Your Project', pmgr = 2020 WHERE pno = 30420; ELSE INSERT INTO Projs( pno, pname, pmgr ) VALUES( 30420, 'My Project', 2020 ); END IF; END; DOESN'T WORK! exists can ONLY be used in a SQL QUERY © Ellis Cohen 2001-2008 29 Empty SELECTs Fail DECLARE thePno int; BEGIN SELECT pno INTO thePno FROM Projs WHERE pno = 30420; IF thePno IS NULL THEN INSERT INTO Projs( pno, pname, pmgr ) VALUES( 30420, 'My Project', 2020 ); ELSE UPDATE Projs SET pname = 'Your Project', pmgr = 2020 WHERE pno = 30420; END IF; END; DOESN'T WORK! The SELECT statement will cause an error if Projs doesn't have project 30420 © Ellis Cohen 2001-2008 30 Insert/Update Exercise Answer DECLARE knt number(5); BEGIN SELECT count(*) INTO knt FROM Projs WHERE pno = 30420; IF knt = 0 THEN INSERT INTO Projs( pno, pname, pmgr ) VALUES( 30420, 'My Project', 2020 ); ELSE UPDATE Projs SET pname = 'Your Project', pmgr = 2020 WHERE pno = 30420; END IF; END; © Ellis Cohen 2001-2008 31 Using RETURNING INTO DECLARE thePno int; BEGIN UPDATE Projs SET pname = 'Your Project', pmgr = 2020 WHERE pno = 30420 RETURNING pno INTO thePno; IF thePno IS NULL THEN INSERT INTO Projs( pno, pname, pmgr ) VALUES( 30420, 'My Project', 2020 ); END IF; END; thePno will remain NULL if nothing is updated © Ellis Cohen 2001-2008 32 Using SQL%ROWCOUNT BEGIN UPDATE Projs SET pname = 'Your Project', pmgr = 2020 WHERE pno = 30420; IF SQL%ROWCOUNT = 0 THEN INSERT INTO Projs( pno, pname, pmgr ) VALUES( 30420, 'My Project', 2020 ); END IF; END; Better approach! SQL%ROWCOUNT returns the number of rows affected by the previous SQL command – in this case, the number of rows updated. © Ellis Cohen 2001-2008 33 Other Insert/Update Approaches Oracle 10g: MERGE INTO Projs p USING DUAL ON (p.pno = 30420) WHEN MATCHED THEN UPDATE SET pname = 'Your Project', pmgr = 2020 WHEN NOT MATCHED THEN INSERT ( pno, pname, pmgr ) VALUES( 30420, 'My Project', 2020 ) MySQL 5.0: INSERT INTO Projs( pno, pname, pmgr ) VALUES( 30420, 'My Project', 2020 ) ON DUPLICATE KEY UPDATE SET pname = 'Your Project', pmgr = 2020 © Ellis Cohen 2001-2008 34 Case Expressions & Statements © Ellis Cohen 2001-2008 35 Simple CASE Expression salincr := CASE WHEN profit > 50000 THEN 200 WHEN profit > 20000 THEN 50 WHEN profit > 0 THEN 0 ELSE –profit/200 END; Returns the value associated with the first expression which evaluates to TRUE Similar to SQL case expressions © Ellis Cohen 2001-2008 36 ELSE NULL Implied salincr := CASE WHEN profit > 50000 THEN 200 WHEN profit > 20000 THEN 50 WHEN profit > 0 THEN 0 END; Equivalent ELSE NULL implied if no explicit ELSE clause salincr := CASE WHEN profit > 50000 THEN 200 WHEN profit > 20000 THEN 50 WHEN profit > 0 THEN 0 ELSE NULL END; © Ellis Cohen 2001-2008 37 Searched CASE Expression salincr := CASE cooljob WHEN 'DEPTMGR' THEN 500 WHEN 'ANALYST' THEN 300 ELSE 150 END; Equivalent salincr := CASE WHEN cooljob = 'DEPTMGR' THEN 500 WHEN cooljob = 'ANALYST' THEN 300 ELSE 150 END; © Ellis Cohen 2001-2008 38 Simple CASE Statement SELECT job INTO cooljob FROM Emps WHERE empno = coolemp; CASE WHEN cooljob = 'DEPTMGR' THEN bonuspct := .004; salincr := 500; WHEN cooljob = 'ANALYST' THEN salincr := 300; Only the statements ELSE matching the first expression which salincr := 150; evaluates to TRUE END CASE; are executed ELSE NULL implied here as well if no explicit ELSE clause © Ellis Cohen 2001-2008 39 Searched CASE Statement SELECT job INTO cooljob FROM Emps WHERE empno = coolemp; CASE cooljob WHEN 'DEPTMGR' THEN bonuspct := .004; salincr := 500; WHEN 'ANALYST' THEN salincr := 300; ELSE Only the statements matching the first value salincr := 150; found are executed END CASE; © Ellis Cohen 2001-2008 40 Conditional vs. Simple CASE Statements CASE WHEN profit > 50000 THEN UPDATE Emps SET sal = sal + 200; UPDATE Bonus SET pct = .004 WHERE job = 'DEPTMGR'; WHEN profit > 20000 THEN UPDATE Emps SET sal = sal + 50 WHERE job = 'DEPTMGR'; WHEN profit > 0 NULL; ELSE DELETE * FROM Emps WHERE job = 'CLERK'; END CASE; © Ellis Cohen 2001-2008 41 Loops © Ellis Cohen 2001-2008 42 For Loops FOR LOOP variables are automatically declared These don't need to be constants; they can be arbitrary expression BEGIN FOR i IN 1 .. 10 LOOP INSERT INTO Projs ( pno, pname, pmgr ) VALUES( i, 'Base Project ' || i, 7789 ); END LOOP; END; ALWAYS incr by 1, except can also write FOR i IN REVERSE 1 .. 10 © Ellis Cohen 2001-2008 43 While Loops Get the first employee tracked by mypkg DECLARE anEmpno NUMBER(5) := mypkg.getFirstEmp(); BEGIN WHILE anEmpno IS NOT NULL LOOP DELETE FROM Asns WHERE empno = anEmpno; anEmpno := mypkg.getNextEmp(); END LOOP; END; Get the next employee tracked by mypkg, return NULL when none left © Ellis Cohen 2001-2008 44 Loops with Exit LOOP Elided code that computes credit rating ... IF credit_rating < 3 THEN EXIT; END IF; ... END LOOP; LOOP EXITs can also be used with FOR & WHILE loops, though some authors frown on this practice ... EXIT WHEN credit_rating < 3; ... END LOOP; © Ellis Cohen 2001-2008 45 Labelled Loops with Exit outer loop, labelled this_one <<this_one>> LOOP ... LOOP inner loop ... EXIT this_one WHEN credit_rating < 3; ... END LOOP; ... END LOOP this_one; Optional, but recommended © Ellis Cohen 2001-2008 46 Loop Exercise lst is a list of employee numbers (all of which are guaranteed to identify employees in the Emps table) [don't be concerned, for now, with how lst is represented] IntLists.length( lst ) returns the length of lst (i.e. how many employee's numbers are in lst) IntLists.get( lst, nth ) returns the nth employee number in lst Find the first employee listed in lst whose commission is larger than 2000, and add that employee's number and name to the Winners table © Ellis Cohen 2001-2008 47 Loop Exercise Answer DECLARE anEmpno Emps.empno%type; anEname Emps.ename%type; aComm Emps.comm%type; BEGIN FOR knt in 1..IntLists.length(lst) LOOP anEmpno := IntLists.nth( lst, knt ); SELECT ename, comm INTO anEname, aComm FROM Emps WHERE empno = anEmpno; IF (aComm > 2000) THEN INSERT INTO winners VALUES( anEmpno, anEname ); EXIT; END IF; END LOOP; END; © Ellis Cohen 2001-2008 48 Stored Functions © Ellis Cohen 2001-2008 49 Defining & Using Stored Functions SQL> CREATE FUNCTION sqr( num int ) RETURN int IS BEGIN RETURN num * num; END; / SQL> EXECUTE pl( sqr( 7 ) ) -- displays 49 SQL> DECLARE sqrval int; BEGIN sqrval := sqr( 7 ); pl( sqrval ); END; / Functions always return a value. The RETURN clause declares the type of value returned © Ellis Cohen 2001-2008 50 CREATE or REPLACE optional SQL> CREATE OR REPLACE FUNCTION sqr( num int ) RETURN int IS BEGIN RETURN num * num; END; / First drops the function before redefining it. If just CREATE is used, and the function is already defined, the CREATE will fail. © Ellis Cohen 2001-2008 51 Using Stored Functions from SQL SQL> CREATE FUNCTION sqr( num int ) RETURN int IS BEGIN RETURN num * num; END; / SQL> SELECT empno, sal, sqr( sal ) FROM Emps; © Ellis Cohen 2001-2008 52 The DUAL Table SELECT ename, sysdate FROM Emps Lists the employee name of all 14 employees, along with today's date SELECT sysdate FROM Emps Lists today's date 14 times sysdate is a built-in parameterless function which returns the current date SELECT sysdate FROM DUAL DUAL is a built-in unmodifiable table which has one column (DUMMY) and one row Lists today's date ONCE! © Ellis Cohen 2001-2008 53 Parameterless Functions SQL> CREATE OR REPLACE FUNCTION getDate RETURN date IS dat date; Parameterless BEGIN SELECT sysdate INTO dat FROM DUAL; RETURN dat; END; / Built-in functions can ONLY be SQL> execute pl( getDate ) used in SQL SQL> execute pl( getDate() ) commands! User-defined parameterless functions can be called with or without parentheses from PL/SQL © Ellis Cohen 2001-2008 54 Parameterless Functions from SQL SQL> CREATE OR REPLACE FUNCTION getDate RETURN date IS dat date; Parameterless BEGIN SELECT sysdate INTO dat FROM DUAL; RETURN dat; END; / SQL> SELECT * FROM Projs WHERE pstart > getDate User-defined parameterless functions MUST be called without parentheses from SQL © Ellis Cohen 2001-2008 55 Stored Procedures © Ellis Cohen 2001-2008 56 Functions & Procedures Functions – can return a value – should not have side effects (this allows calls using them to be optimized) Procedures – do not have a return value, but can return results through OUT parameters – may have arbitrary side effects © Ellis Cohen 2001-2008 57 Defining & Using Stored Procedures SQL> CREATE OR REPLACE PROCEDURE DeleteEmployee( anEmpno int ) IS BEGIN DELETE FROM Emps WHERE empno = anEmpno; END; / SQL> EXECUTE DeleteEmployee( 3142 ) © Ellis Cohen 2001-2008 58 OUT Parameters SQL> CREATE OR REPLACE PROCEDURE DeleteEmployee( anEmpno int, theDeptno OUT int ) IS BEGIN DELETE FROM Emps WHERE empno = anEmpno RETURNING deptno INTO theDeptno; END; An OUT parameter can be used / like a variable, but also passes a value back to the caller when the procedure finishes SQL> DECLARE deldept int; BEGIN DeleteEmployee( 3142, deldept ); pl( 'Employee deleted from dept ' || deldept ); END; / There can be any number of OUT parameters © Ellis Cohen 2001-2008 59 Named Parameter Notation SQL> CREATE OR REPLACE PROCEDURE DeleteEmployee( anEmpno int, theDeptno OUT int ) IS BEGIN DELETE FROM Emps WHERE empno = anEmpno RETURNING deptno INTO theDeptno; END; / SQL> DECLARE deldept int; BEGIN DeleteEmployee( theDeptno => deldept, anEmpno => 3142 ); pl( 'Employee deleted from dept ' || deldept ); END; / © Ellis Cohen 2001-2008 60 IN OUT Parameters SQL> CREATE OR REPLACE PROCEDURE SwapSal( anEmpno int, theSal IN OUT number ) IS oldsal number; BEGIN IN OUT SELECT sal INTO oldsal FROM Emps parameters WHERE empno = anEmpno; UPDATE Emps SET sal = theSal • pass values WHERE empno = anEmpno; to the procedure theSal := oldsal; when it is END; called / • pass values back to the SQL> DECLARE caller when mysal number := 1000; the procedure BEGIN finishes SwapSal( 3142, mysal ); pl( 'Previous sal was ' || mysal ); END; / © Ellis Cohen 2001-2008 61 Stored Packages © Ellis Cohen 2001-2008 62 Role of Packages A package is used to group together a cohesive set of stored database operations (procedures and functions) • They all provide related functionality, and/or • They use the same set of variables/tables/views © Ellis Cohen 2001-2008 63 Package Descriptions SQL> CREATE OR REPLACE PACKAGE MyUtil AS FUNCTION getDate RETURN date; FUNCTION getNumColumns( tblnam varchar ) RETURN int; END MyUtil; This only describes the operations in the package; not their implementation. That's done separately in the package body. Invoking packaged functions SQL> execute pl( MyUtil.getDate() ) SQL> execute pl( MyUtil.getNumColumns( 'Emps' ) ) SQL> SELECT * FROM Projs WHERE pstart > MyUtil.getDate; © Ellis Cohen 2001-2008 64 Package Body SQL> CREATE OR REPLACE PACKAGE BODY MyUtil AS FUNCTION getDate RETURN date IS dat date; BEGIN SELECT sysdate INTO dat FROM dual; RETURN dat; END; FUNCTION getNumColumns( tblnam varchar ) RETURN int IS numcols int; BEGIN SELECT count(*) INTO numcols FROM user_tab_columns WHERE table_name = upper(tblnam); RETURN numcols; END; END MyUtil; © Ellis Cohen 2001-2008 65 Table-Based Packages A package of operations dealing with the Emps table PACKAGE EmpPkg AS PROCEDURE ChangeSal( anEmpno int, aSal number ); -- Changes the salary of an employee PROCEDURE ChangeJob( anEmpno int, aJob varchar ); -- Changes the job of some other employee PROCEDURE ChangeMgr( anEmpno int, aMgr int ); -- Changes mgr of an employee PROCEDURE ChangePosition( anEmpno int, aMgr int, aDeptno int, aJob varchar ); -- Changes one or more of job/dept/mgr of employee PROCEDURE AddEmp( anEmpno int, anEname varchar, aSal number, aJob varchar, aMgr int, aDeptno int ); -- Adds an employee to a department PROCEDURE TerminateEmp( anEmpno int ); -- Terminates an employee END EmpPkg; It can also be useful for a package to encapsulate the procedures & functions for a group of related tables © Ellis Cohen 2001-2008 66 Table-Based Package Body PACKAGE BODY EmpPkg AS … PROCEDURE ChangePosition( anEmpno int, aMgr int, aDeptno int, aJob varchar ) IS BEGIN UPDATE Emps SET mgr = aMgr, deptno = aDeptno, job = aJob WHERE empno = anEmpno; END; -------------------PROCEDURE AddEmp( anEmpno int, anEname varchar, aSal number, aJob varchar, aMgr int, aDeptno int ) IS BEGIN INSERT INTO Emps( empno, ename, sal, job, mgr, deptno ) VALUES ( anEmpno, anEname, aSal, aJob, aMgr, aDeptno ); END; -------------------PROCEDURE TerminateEmp( anEmpno int ) IS BEGIN DELETE FROM Emps WHERE empno = anEmpno; END; END EmpPkg; © Ellis Cohen 2001-2008 67 Sequential Values © Ellis Cohen 2001-2008 68 Hiding Implementation Details Given a relational model with Asns asnid empno pno hrs int primary key int references Emps int references Projs int We would like to write a procedure AddAssignment( empno, pno, hrs ) which would add a new tuple to Asns but automatically fill in asnid © Ellis Cohen 2001-2008 69 Hiding Id Assignment CREATE SEQUENCE asnseq START WITH 1000; -- Defines a sequence used to generate -- sequential values. -- Can specify start value and increment CREATE OR REPLACE PROCEDURE AddAssignment( anEmpno int, aPno int, theHrs number ) IS BEGIN The first value generated will be 1001 INSERT INTO Asns VALUES( asnseq.nextval, anEmpno, aPno, theHrs ); END; nextval is a parameterless function which gets the next value from the sequence Suppose you want to return the value as well as insert it? © Ellis Cohen 2001-2008 70 Returning Assigned Id's CREATE SEQUENCE asnseq START WITH 1001; -- Defines a sequence used to generate -- sequential values. Can specify start value and increment CREATE OR REPLACE PROCEDURE AddAssignment( anEmpno int, aPno int, theHrs number, OUT theId int ) IS BEGIN SELECT asnseq.nextval INTO theId FROM DUAL; -- nextval is an Oracle built-in parameter-less -- function which gets the next value from sequence, -- but can ONLY be called from SQL code -- DUAL is a convenient, -- built-in single row/column table INSERT INTO Asns VALUES( theId, anEmpno, aPno, theHrs ); END; theId := asnseq.nextval DOESN'T WORK! asnseq.nextval must be used in a SQL statement © Ellis Cohen 2001-2008 71 Using RETURNING INTO CREATE SEQUENCE asnseq START WITH 1001; CREATE OR REPLACE PROCEDURE AddAssignment( anEmpno int, aPno int, theHrs number, OUT theId int ) IS Associates this value with the asnid attribute BEGIN INSERT INTO Asns VALUES( asnseq.nextval, anEmpno, aPno, theHrs ) RETURNING asnid INTO theId; END; Also, asnseq.currval gets the current value of the sequence, without incrementing it! © Ellis Cohen 2001-2008 72 Data-Specific Sequences Given a relational model with Entries invid linenum prodid qty primary key int references Invoices int int int ( invid, linenum ) We would like to write a procedure AddEntry which would add a new tuple to Entries but automatically fill in the next linenum © Ellis Cohen 2001-2008 73 Computing Next Sequence Number SELECT 1 + nvl(max(linenum),0) INTO nxtlin FROM Entries WHERE invid = anInvid; nxtlin will get 1 more than the highest linenum for anInvid Because this uses an aggregate function (MAX), this SELECT always succeeds and returns a result, even if there are no current entries for anInvid In that case, MAX returns NULL, and because of nvl, nxtlin will get 1 © Ellis Cohen 2001-2008 74 Inserting Next Sequence Number CREATE OR REPLACE PROCEDURE AddEntry( anInvid int, aProdid int, aQty int ) IS nxtlin int; BEGIN SELECT 1 + nvl(max(linenum),0) INTO nxtlin FROM Entries WHERE invid = anInvid; INSERT INTO Entries VALUES( anInvid, nxtlin, aProdid, aQty); END; Later, we'll see how to use Dynamic SQL to define a sequence for each invoice ID. But assuming the number of invoices is large, and the number of lines per invoice is small, it's reasonable to just compute the next linenum © Ellis Cohen 2001-2008 75 Exceptions © Ellis Cohen 2001-2008 76 Exceptions An anonymous block ZERO_DIVIDE exception raised if earnings is 0 DECLARE pe_ratio number(5,1); BEGIN SELECT price / earnings INTO pe_ratio FROM Stocks WHERE symbol = :sym; INSERT INTO Stats( symbol, ratio ) VALUES( :sym, pe_ratio ); END; If an exception is raised • the INSERT is not executed • the exception is reported to the user © Ellis Cohen 2001-2008 :sym is a middle-tier variable, not a declared local variable 77 Catching ZERO_DIVIDE DECLARE pe_ratio number(5,1); BEGIN SELECT price / earnings INTO pe_ratio FROM Stocks WHERE symbol = :sym; INSERT INTO Stats( symbol, ratio ) VALUES( :sym, pe_ratio ); EXCEPTION WHEN ZERO_DIVIDE THEN INSERT INTO Stats( symbol, ratio ) VALUES( :sym, NULL ); END; If the exception is raised, control jumps to the EXCEPTION clause, where an exception handler for ZERO_DIVIDE has code for an alternative INSERT statement which is executed instead © Ellis Cohen 2001-2008 78 Discarding All Other Exceptions Suppose some other kind of DECLARE error is raised while executing this code pe_ratio number(3,1); BEGIN SELECT price / earnings INTO pe_ratio FROM Stocks WHERE symbol = :sym; INSERT INTO Stats( symbol, ratio ) VALUES( :sym, pe_ratio ); EXCEPTION WHEN ZERO_DIVIDE THEN INSERT INTO Stats( symbol, ratio ) VALUES( :sym, NULL ); WHEN OTHERS THEN NULL; END; WHEN OTHERS catches all other exceptions NULL does nothing (in response) © Ellis Cohen 2001-2008 79 Raise Application Error DECLARE pe_ratio number(3,1); BEGIN SELECT price / earnings INTO pe_ratio FROM Stocks WHERE symbol = :sym; INSERT INTO Stats( symbol, ratio ) VALUES( :sym, pe_ratio ); EXCEPTION WHEN ZERO_DIVIDE THEN INSERT INTO Stats( symbol, ratio ) VALUES( :sym, NULL ); WHEN OTHERS THEN RAISE_APPLICATION_ERROR( -20069, 'Couldn''t Calculate PE for ' || :sym ); END; Reports a more descriptive error Note: the error number must be in the range -20000 to -20999 © Ellis Cohen 2001-2008 80 Propagating Exceptions BEGIN DoSomething(); EXCEPTION WHEN OTHERS THEN pl( 'Problem doing something' ); END; PROCEDURE DoSomething() IS BEGIN … RAISE_APPLICATION_ERROR( -20069, 'Some problem encountered' ); … END Exceptions not caught within a procedure/function are propagated to the site where it was called © Ellis Cohen 2001-2008 81 Exceptional Insert/Update Attempting to insert a row into a table will raise an exception if another tuple in that table has the same primary key value Use that information to write an anonymous PL/SQL block that does the following without using SELECT, MERGE, or SQL%ROWCOUNT or RETURNING: if a project does not exist with project # 30420, INSERT it with pname: 'My Project', and pmgr: 2020 else if it already exists, UPDATE it with pname: 'Your Project', and pmgr: 2020 © Ellis Cohen 2001-2008 82 Exceptional Insert/Update Answer BEGIN INSERT INTO Projs( pno, pname, pmgr ) VALUES( 30420, 'My Project', 2020 ) EXCEPTION WHEN OTHERS THEN UPDATE Projs SET pname = 'Your Project', pmgr = 2020 WHERE pno = 30420; END; Although using SQL%ROWCOUNT is generally more efficient © Ellis Cohen 2001-2008 83 Reraising Exceptions PROCEDURE DoSomething IS BEGIN -- Do something complicated -- which could raise an exception EXCEPTION WHEN OTHERS THEN IF … THEN -- under some circumstances, -- we can simply -- do something else ELSE -- but in other circumstances -- we propagate the exception -- back to the caller RAISE; END IF; END; © Ellis Cohen 2001-2008 84 Non-Existence Exceptions If there is no employee with empno anEmpno, raise an exception BEGIN SELECT count(*) INTO knt FROM Emps WHERE empno = anEmpno; IF knt = 0 THEN RAISE_APPLICATION_ERROR( … ); END IF; Equivalent result; END; same performance (since at most one such employee) BEGIN SELECT empno INTO theEmpno FROM Emps WHERE empno = anEmpno; EXCEPTION WHEN OTHERS THEN RAISE_APPLICATION_ERROR( … ); END; © Ellis Cohen 2001-2008 85 Record Types © Ellis Cohen 2001-2008 86 Record Types DECLARE TYPE NameDeptRec IS RECORD ( ename varchar(30), deptno int ); aRec NameDeptRec; BEGIN SELECT ename, deptno INTO aRec.ename, aRec.deptno FROM Emps WHERE empno = 7876; INSERT INTO CoolEmps( ename, deptno ) VALUES( aRec.ename, aRec.deptno ); END; © Ellis Cohen 2001-2008 87 Record Based Select/Insert DECLARE TYPE NameDeptRec IS RECORD ( ename varchar(30), deptno int ); aRec NameDeptRec; BEGIN SELECT ename, deptno INTO aRec FROM Emps WHERE empno = 7876; INSERT INTO CoolEmps( ename, deptno ) VALUES aRec; END; match based on position, not name © Ellis Cohen 2001-2008 88 Using ROWTYPE You can declare a record whose type corresponds to a row of a table or view DECLARE aRec CoolEmps%ROWTYPE; BEGIN SELECT ename, deptno INTO aRec FROM Emps WHERE empno = 7876; INSERT INTO CoolEmps( ename, deptno ) VALUES aRec; END; © Ellis Cohen 2001-2008 89 Pretty Printing Tuples SELECT ename, street, city, state, zip FROM Emps WHERE empno = 7876; DECLARE aRec Emps%ROWTYPE; BEGIN SELECT * INTO aRec FROM Emps WHERE empno = 7876; pl( aRec.ename ); pl( aRec.street ); pl( aRec.city || ', ' || aRec.state || ', ' || aRec.zip ); pl( '' ); END; © Ellis Cohen 2001-2008 90 Record Based Update PROCEDURE IncludeProject( pRec Projs%ROWTYPE ) IS BEGIN INSERT INTO Projs VALUES pRec; EXCEPTION Built-in; a record with that WHEN DUP_VAL_ON_INDEX index is already in the table THEN UPDATE Projs SET ROW = pRec WHERE pno = pRec.pno; END; © Ellis Cohen 2001-2008 91 Query-Based FOR Loops © Ellis Cohen 2001-2008 92 Query-Based FOR Loops FOR i IN 1 .. 10 LOOP … END LOOP loops through the numbers from 1..10 FOR rec IN (SELECT …) LOOP … END LOOP loops through the tuples resulting from the SELECT BEGIN pl( ' Name Dept#'); Query-Based FOR pl('-----------------------------'); Loops automatically declare the loop FOR erec IN variable to be (SELECT ename, deptno consistent with the FROM Emps tuples in the result set WHERE job = 'ANALYST') LOOP pl( rpad( erec.ename,15) || erec.deptno ); END LOOP; END; Very similar to SQL*Plus display of the bare SELECT © Ellis Cohen 2001-2008 93 Queries as Loops CREATE TABLE Results AS SELECT empno, ename FROM Emps e WHERE sal > 1500 CREATE TABLE Results (empno int, ename varchar(30)); BEGIN FOR erec IN (SELECT * FROM Emps WHERE sal > 1500) LOOP INSERT INTO Results VALUES ( erec.empno, erec.ename ); END LOOP; END; Don't fill a table this way! We're just using PL/SQL as a way of describing how queries work © Ellis Cohen 2001-2008 94 Joins as Nested Loops CREATE TABLE Results AS SELECT ename, dname FROM Emps e, Depts d WHERE e.deptno = d.deptno AND e.sal > 1500 CREATE TABLE Results (ename varchar(30), dname varchar(16)); BEGIN FOR drec IN (SELECT * FROM Depts) LOOP FOR erec IN (SELECT * FROM Emps WHERE deptno = drec.deptno AND sal > 1500) LOOP INSERT INTO Results VALUES( erec.ename, drec.dname ); END LOOP; END LOOP; Don't fill a table this way! END; We're just using PL/SQL as a way of describing how queries work © Ellis Cohen 2001-2008 95 Bare SELECT Problem BEGIN DoSomething(); SELECT empno, ename FROM Emps WHERE job = 'CLERK'; END; This anonymous block is illegal Why? Write code that works, still written as a single anonymous block © Ellis Cohen 2001-2008 96 Eliminate Bare SELECTs BEGIN DoSomething(); FOR erec IN (SELECT empno, ename FROM Emps WHERE job = 'CLERK') LOOP pl( erec.empno || ': ' || erec.ename ); END LOOP; END; Can't use a bare SELECT as a PL/SQL statement. Can use a Query For Loop instead © Ellis Cohen 2001-2008 97 List Employees in Each Dept ACCOUNTING: CLARK, KING, MILLER OPERATIONS RESEARCH: SMITH, JONES, SCOTT, ADAMS, FORD SALES: ALLEN, WARD, MARTIN, BLAKE, TURNER, JAMES DECLARE fstr varchar(200); sep varchar(5); BEGIN FOR drec IN (SELECT deptno, dname FROM Depts ORDER BY dname) LOOP fstr := drec.dname; sep := ': '; FOR erec IN (SELECT ename FROM Emps WHERE deptno = drec.deptno) LOOP fstr := fstr || sep || erec.ename; sep := ', '; END LOOP; pl( fstr ); END LOOP; END; © Ellis Cohen 2001-2008 98 Existence Exceptions Raise an error if dept aDeptno has any employees BEGIN SELECT count(*) INTO knt FROM Emps WHERE deptno = aDeptno; IF knt > 0 THEN RAISE_APPLICATION_ERROR( … ); END IF; END; FOR erec IN ( SELECT empno FROM Emps WHERE deptno = aDeptno) LOOP RAISE_APPLICATION_ERROR( … ); END LOOP; This code is more efficient if there are many such employees, and there is no index on deptno, since it can quit when the first such employee is found, rather than after counting all of them © Ellis Cohen 2001-2008 99 Unnecessary Looped UPDATEs Increase salary of all project managers FOR prec IN (SELECT DISTINCT pmgr FROM Projects) LOOP UPDATE Emps SET sal = sal + 100 WHERE empno = prec.pmgr; END LOOP; UPDATE Emps SET sal = sal + 100 WHERE empno IN (SELECT pmgr FROM Projects) This code is much more efficient In general, avoid loops when equivalent code can be written without loops © Ellis Cohen 2001-2008 100 Unnecessary Looped SELECTs Raise an error if some dept has no employees DECLARE knt int; BEGIN FOR drec IN ( SELECT deptno FROM Depts) LOOP SELECT count(*) INTO knt FROM Emps WHERE deptno = drec.deptno; IF knt = 0 THEN RAISE_APPLICATION_ERROR( -20023, 'There is a dept with no employees' ); END LOOP; END; The inner SELECT statement can be avoided by using a more complicated outer SELECT © Ellis Cohen 2001-2008 101 Handling Multiple Errors Raise an error if some dept has no employees CREATE VIEW DeptKntsView AS SELECT deptno, count(empno) AS eknt FROM Depts NATURAL LEFT JOIN Emps GROUP BY deptno; BEGIN FOR drec IN ( SELECT deptno FROM DeptKntsView WHERE eknt = 0) LOOP RAISE_APPLICATION_ERROR( -20023, 'There is a dept with no employees' ); END LOOP; END; This code works, but suppose we want to indicate exactly which departments don't have employees. What can we do? © Ellis Cohen 2001-2008 102 Identifying the First Error Raise an error if some dept has no employees CREATE VIEW DeptKntsView AS SELECT deptno, count(empno) AS eknt FROM Depts NATURAL LEFT JOIN Emps GROUP BY deptno; BEGIN FOR drec IN ( SELECT deptno FROM DeptKntsView WHERE eknt = 0) LOOP RAISE_APPLICATION_ERROR( -20023, 'Dept ' || drec.deptno || ' has no employees' ); END LOOP; END; This only identifies the first such department. Suppose we want to identify all of them? © Ellis Cohen 2001-2008 103 Building Informative Error Messages Identify all depts which have no employees DECLARE empties varchar(200) := ''; sep varchar(2) := ''; BEGIN FOR drec IN ( SELECT deptno FROM DeptKntsView WHERE eknt = 0) LOOP empties := empties || sep || drec.deptno; sep := ', '; END LOOP; IF length(empties) > 0 THEN RAISE_APPLICATION_ERROR( -20023, 'Depts without employees: ' || empties ); END IF; END; © Ellis Cohen 2001-2008 104 Dynamic SQL © Ellis Cohen 2001-2008 105 Dynamic SQL PROCEDURE InsertProject( aPno number, aPname varchar, aPmgr number ) IS BEGIN INSERT INTO Projs( pno, pname, pmgr ) VALUES( aPno, aPname, aPmgr ); END; PROCEDURE InsertProject( aPno number, aPname varchar, aPmgr number ) IS Dynamically construct & sqlstr varchar(100); execute a SQL command! BEGIN sqlstr := 'INSERT INTO Projs( pno, pname, pmgr ) ' || ' VALUES( ' || aPno || ', ' || quote(aPname) || ', ' || aPmgr || ' )'; EXECUTE IMMEDIATE sqlstr; END; © Ellis Cohen 2001-2008 106 Runtime Table Names & Conditions PROCEDURE DeleteRows( table_name varchar, condition varchar ) IS where_clause varchar(100); BEGIN IF condition IS NOT NULL THEN where_clause := ' WHERE ' || condition; END IF; EXECUTE IMMEDIATE 'DELETE FROM ' || table_name || where_clause; END; EXECUTE IMMEDIATE works with PL/SQL blocks as well as SQL statements! © Ellis Cohen 2001-2008 107 Runtime DDL PROCEDURE DropTable( tbl varchar ) IS sqlstr varchar(99) := 'drop table ' || tbl; BEGIN EXECUTE IMMEDIATE sqlstr; EXCEPTION WHEN OTHERS THEN NULL; END; Prevents error message if table doesn't exist. DDL statements (CREATE, DROP, …) cannot be executed inside of PL/SQL except via Dynamic SQL © Ellis Cohen 2001-2008 108 Creating Sequences Dynamically CREATE OR REPLACE PROCEDURE AddEntry( anInvid int, aProdid int, aQty int ) IS seqstr varchar(20) := 'InvSeq' + anInvid; insstr varchar(100) := 'INSERT INTO Entries VALUES (' || 'anInvid, ' || seqstr || '.nextval, aProdid, aQty)'; BEGIN EXECUTE IMMEDIATE insstr; EXCEPTION WHEN OTHERS THEN EXECUTE IMMEDIATE 'CREATE SEQUENCE ' || seqstr; EXECUTE IMMEDIATE insstr; END; Dynamically creates a sequence for an invoice if the sequence doesn’t already exist (causing an exception). For invoice 347, the sequence will be named InvSeq347 © Ellis Cohen 2001-2008 109 Execute Immediate INTO FUNCTION SelectEmployee( condition in varchar ) RETURN Emps.empno%TYPE IS sqlstr varchar(200); anEmp Emps.empno%TYPE; BEGIN sqlstr := 'SELECT FROM ' || table_name || ' WHERE ' || condition; EXECUTE IMMEDIATE sqlstr INTO anEmp; RETURN anEmp; END; Allows SQL query to be reused with different INTO clauses © Ellis Cohen 2001-2008 110 Parameterized Dynamic SQL PROCEDURE InsertProject( aPno number, aPname varchar, aPmgr number ) IS BEGIN INSERT INTO Projs( pno, pname, pmgr ) VALUES( aPno, aPname, aPmgr ); END; PROCEDURE InsertProject( aPno number, aPname varchar, aPmgr number ) IS sqlstr varchar(100); BEGIN Using numbered bind parameters sqlstr := 'INSERT INTO Projs( pno, pname, pmgr ) ' || ' VALUES( :1, :2, :3 )' EXECUTE IMMEDIATE sqlstr USING aPno, aPname, aPmgr; END; © Ellis Cohen 2001-2008 111