Archive for the ‘pl/sql’ Category
Debugging MySQL Procedures
In my second database class we focus on PL/SQL but we’ve begun highlighting the alternatives in MySQL and SQL Server. A student asked how they could debug runtime variable values in a MySQL Stored Procedure (or subroutines according to some documentation). You can see this post for how to create an equivalent solutions for MySQL functions.
In Oracle, we debug with the DBMS_OUTPUT
package. Packages, like DBMS_OUTPUT
hold related functions and procedures, and are a corollary to System.out.println()
in Java.
Before you can see the output at the command-line in Oracle (that is if you’re not using SQL*Developer or Toad), you must set a SQL*Plus environment variable. These variables don’t exist in MySQL or SQL Server command-line tools because they never served the function of a report writer like SQL*Plus.
You enable output display in Oracle by setting this in SQL*Plus:
SQL> SET SERVEROUTPUT ON SIZE 1000000 |
You can test your anonymous or named block. Since MySQL doesn’t support anonymous named block, the examples using a trivial procedure that prints Hello World! (orginal, right ;-)).
1 2 3 4 5 6 7 8 9 10 11 12 | -- Create a procedure in Oracle. CREATE OR REPLACE PROCEDURE hello_world IS BEGIN -- Print a word without a line return. DBMS_OUTPUT.put('Hello '); -- Print the rest of the phrase and a line return. DBMS_OUTPUT.put_line('World!'); END; / -- Call the procedure. EXECUTE hello_world; |
It’s seems useless to print the output because it should be evident. MySQL procedures are a bit different because there’s no OR REPLACE
syntax. The equivalent to calling the DBMS_OUTPUT
package procedures in MySQL is to simply select a string. Now you can do this with or without the FROM dual
clause in MySQL, don’t we wish we could do the same thing in Oracle. 🙂
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | -- Conditionally drop the procedure. SELECT 'DROP PROCEDURE hello_world' AS "Statement"; DROP PROCEDURE IF EXISTS hello_world; -- Reset the delimiter to write a procedure. DELIMITER $$ -- Create a procedure in Oracle. CREATE PROCEDURE hello_world() BEGIN -- Print the phrase and a line return. SELECT 'Hello World!'; END; $$ -- Reset the delimiter back to a semicolon to work again. DELIMITER ; -- Call the procedure. SELECT 'CALL hello_world' AS "Statement"; CALL hello_world(); |
Originally, I tried to keep this short but somebody wanted an example in a loop. Ouch, loops are so verbose in MySQL. Since I was modifying this post, it seemed like a good idea to put down some guidelines for successful development too.
Guidelines for Development of Procedures
Declaration Guidelines
The sequencing of components in MySQL procedures is important. Unlike, PL/SQL, there’s no declaration block, declarations must be at the top of the execution block. They also must appear in the following order:
- Variable declarations must go first, you can assign initial values with the
DEFAULT
keyword. While not required, you should:
- Consider using something like
lv_
to identify them as local variables for clarity and support of your code. - Consider grouping local variables that relate to handlers at the bottom of the list of variables.
- After local variables and before handlers, you put your cursor definitions. You should note that MySQL doesn’t support explicit dynamic cursors, which means you can’t define one with a formal signature. However, you do have prepared statements and they mimic dynamic cursor behaviors.
- Last in your declaration block, you declare your handler events.
Execution Guidelines
- Variable assignments are made one of two ways:
- You should start each execution block with a
START TRANSACTION
and then aSAVEPOINT
, which ensures the procedure acts like a cohesive programming unit. - You assign a
left_operand = right_operand;
as a statement. - You use the
SELECT column INTO variable;
syntax to filter a value through SQL functions and assign the resulting expression to a local variable. - You assign a single row cursor output to variables using a
SELECT column INTO variable FROM ...
.
- You must assign values from cursors called in a loop into local variables when you want to use the results in nested SQL statements or loops.
- You must reset looping variables, like the
fetched
control variable at the end of the loop to reuse the handler variable in subsequent loops. - You must assign values to local variables if you want to use them in the exception handler.
- If you’ve started a transaction, don’t forget to
COMMIT
your work.
Exception Guidelines
- Leave out the exception handler until you’ve tested all outcomes, and make sure you document them and add them as potential handlers.
- When you deploy exception blocks, they’re the last element at the bottom of the exception block.
- You should consider explicit exception handlers for each error unless the action taken is the same.
- You should consider grouping all exception handlers when the action taken is the same.
- You should include a
ROLLBACK
whenever you’ve performed two or more SQL statements that may modify data.
Below is an example for putting debug code inside a loop.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | -- Conditionally drop a sample table. SELECT 'DROP TABLE IF EXISTS sample' AS "Statement"; DROP TABLE IF EXISTS sample; -- Create a table. CREATE TABLE sample ( sample_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT , sample_msg VARCHAR(20)); -- Insert into sample. INSERT INTO sample (sample_msg) VALUES ('Message #1') ,('Message #2') ,('Message #3'); -- Conditionally drop the procedure. SELECT 'DROP PROCEDURE debug_loop' AS "Statement"; DROP PROCEDURE IF EXISTS debug_loop; -- Reset the delimiter to write a procedure. DELIMITER $$ -- Create a procedure in Oracle. CREATE PROCEDURE debug_loop() BEGIN /* Declare a counter variable. */ DECLARE lv_counter INT DEFAULT 1; /* Declare local control loop variables. */ DECLARE lv_sample_id INT; DECLARE lv_sample_msg VARCHAR(20); /* Declare a local variable for a subsequent handler. */ DECLARE duplicate_key INT DEFAULT 0; DECLARE fetched INT DEFAULT 0; /* Declare a SQL cursor fabricated from local variables. */ DECLARE sample_cursor CURSOR FOR SELECT * FROM sample; /* Declare a duplicate key handler */ DECLARE CONTINUE HANDLER FOR 1062 SET duplicate_key = 1; /* Declare a not found record handler to close a cursor loop. */ DECLARE CONTINUE HANDLER FOR NOT FOUND SET fetched = 1; /* Start transaction context. */ START TRANSACTION; /* Set savepoint. */ SAVEPOINT all_or_none; /* Open a sample cursor. */ OPEN sample_cursor; cursor_sample: LOOP /* Fetch a row at a time. */ FETCH sample_cursor INTO lv_sample_id , lv_sample_msg; /* Place the catch handler for no more rows found immediately after the fetch operation. */ IF fetched = 1 THEN LEAVE cursor_sample; END IF; -- Print the cursor values. SELECT CONCAT('Row #',lv_counter,' [',lv_sample_id,'][',lv_sample_msg,']') AS "Rows"; -- Increment counter variable. SET lv_counter = lv_counter + 1; END LOOP cursor_sample; CLOSE sample_cursor; /* This acts as an exception handling block. */ IF duplicate_key = 1 THEN /* This undoes all DML statements to this point in the procedure. */ ROLLBACK TO SAVEPOINT all_or_none; END IF; END; $$ -- Reset the delimiter back to a semicolon to work again. DELIMITER ; -- Call the procedure. SELECT 'CALL debug_loop' AS "Statement"; CALL debug_loop(); |
This post certainly answers the student question. Hopefully, it also helps other who must migrate Oracle skills to MySQL. Since IBM DB2 has introduced a PL/SQL equivalent, wouldn’t it be nice if Oracle did that for MySQL. That is, migrate PL/SQL to MySQL. Don’t tell me if you think that’s a pipe dream, I’d like to hope for that change.
Multi-row Merge in MySQL
After I wrote the post for students on the multiple row MERGE
statement for an upload through an external table in Oracle, I thought to check how it might be done with MySQL. More or less because I try to keep track of how things are done in several databases.
MySQL’s equivalent to a MERGE
statement is an INSERT
statement with an ON DUPLICATE KEY
clause, which I blogged about a while back. You may also use the REPLACE INTO
when you want to merge more than one row. At the time that I wrote this, I thought there wasn’t support for an INSERT ON DUPLICATE KEY
clause statement with a subquery but I found that I was wrong. Fortunately, somebody posted a comment to remind me about this and now both solutions are here for anybody that would like them.
The workaround with a VALUES
clause was to write a stored procedure with two cursor loops, explicitly pass the values from the cursor to local variables, and then put the local variables in the VALUES
clause. I’ll post the other with a subquery soon. On parity, clearly Oracle’s MERGE
statement (shown here) is far superior than MySQL’s approach.
Demonstration
Here are the steps to accomplish an import/upload with the INSERT
statement and ON DUPLICATE KEY
clause. In this example, you upload data from a flat file, or Comma Separated Value (CSV) file to a denormalized table (actually in unnormalized form). This type of file upload transfers information that doesn’t have surrogate key values. You have to create those in the scope of the transformation to the normalized tables.
Step #1 : Position your CSV file in the physical directory
After creating the virtual directory, copy the following contents into a file named kingdom_mysql_import.csv
in the C:\Data\Download
directory or folder. If you have Windows UAC enabled in Windows Vista or 7, you should disable it before performing this step.
Place the following in the kingdom_mysql_import.csv
file. The trailing commas are meaningful in MySQL and avoid problems when reading CSV files.
Narnia, 77600,'Peter the Magnificent',12720320,12920609, Narnia, 77600,'Edmund the Just',12720320,12920609, Narnia, 77600,'Susan the Gentle',12720320,12920609, Narnia, 77600,'Lucy the Valiant',12720320,12920609, Narnia, 42100,'Peter the Magnificent',15310412,15310531, Narnia, 42100,'Edmund the Just',15310412,15310531, Narnia, 42100,'Susan the Gentle',15310412,15310531, Narnia, 42100,'Lucy the Valiant',15310412,15310531, Camelot, 15200,'King Arthur',06310310,06861212, Camelot, 15200,'Sir Lionel',06310310,06861212, Camelot, 15200,'Sir Bors',06310310,06351212, Camelot, 15200,'Sir Bors',06400310,06861212, Camelot, 15200,'Sir Galahad',06310310,06861212, Camelot, 15200,'Sir Gawain',06310310,06861212, Camelot, 15200,'Sir Tristram',06310310,06861212, Camelot, 15200,'Sir Percival',06310310,06861212, Camelot, 15200,'Sir Lancelot',06700930,06821212, |
Step #2 : Connect as the student
user
Disconnect and connect as the student user, or reconnect as the student user. The reconnect syntax that protects your password is:
mysql -ustudent -p |
Connect to the sampledb
database, like so:
mysql> USE sampledb; |
Step #3 : Run the script that creates tables and sequences
Copy the following into a create_mysql_kingdom_upload.sql
file within a directory of your choice. Then, run it as the student
account.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | -- This enables dropping tables with foreign key dependencies. -- It is specific to the InnoDB Engine. SET FOREIGN_KEY_CHECKS = 0; -- Conditionally drop objects. SELECT 'KINGDOM' AS "Drop Table"; DROP TABLE IF EXISTS KINGDOM; SELECT 'KNIGHT' AS "Drop Table"; DROP TABLE IF EXISTS KNIGHT; SELECT 'KINGDOM_KNIGHT_IMPORT' AS "Drop Table"; DROP TABLE IF EXISTS KINGDOM_KNIGHT_IMPORT; -- Create normalized kingdom table. SELECT 'KINGDOM' AS "Create Table"; CREATE TABLE kingdom ( kingdom_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT , kingdom_name VARCHAR(20) , population INT UNSIGNED) ENGINE=INNODB; -- Create normalized knight table. SELECT 'KNIGHT' AS "Create Table"; CREATE TABLE knight ( knight_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT , knight_name VARCHAR(24) , kingdom_allegiance_id INT UNSIGNED , allegiance_start_date DATE , allegiance_end_date DATE , CONSTRAINT fk_kingdom FOREIGN KEY (kingdom_allegiance_id) REFERENCES kingdom (kingdom_id)) ENGINE=INNODB; -- Create external import table in memory only - disappears after rebooting the mysqld service. SELECT 'KINGDOM_KNIGHT_IMPORT' AS "Create Table"; CREATE TABLE kingdom_knight_import ( kingdom_name VARCHAR(20) , population INT UNSIGNED , knight_name VARCHAR(24) , allegiance_start_date DATE , allegiance_end_date DATE) ENGINE=MEMORY; |
Step #4 : Load the data into your target upload table
There a number of things that could go wrong but when you choose LOCAL
there generally aren’t any problems. Run the following query from the student
account while using the sampledb
database, and check whether or not you can access the kingdom_import.csv
file.
1 2 3 4 5 6 | LOAD DATA LOCAL INFILE 'c:/Data/kingdom_mysql_import.csv' INTO TABLE kingdom_knight_import FIELDS TERMINATED BY ',' ENCLOSED BY '"' ESCAPED BY '\\' LINES TERMINATED BY '\r\n'; |
Step #5 : Create the upload procedure
Copy the following into a create_mysql_upload_procedure.sql
file within a directory of your choice. You should note that unlike Oracle’s MERGE
statement, this is done with the ON DUPLICATE KEY
clause and requires actual values not a source query. This presents few options other than a stored routine, known as a stored procedure. As you can see from the code, there’s a great deal of complexity to the syntax and a much more verbose implementation than Oracle’s equivalent PL/SQL.
Then, run it as the student
account. As you look at the structure to achieve this simple thing, the long standing complaint about PL/SQL being a verbose language comes to mind. Clearly, stored procedures are new to MySQL but they’re quite a bit more verbose than PL/SQL.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | -- Conditionally drop the procedure. SELECT 'UPLOAD_KINGDOM' AS "Drop Procedure"; DROP PROCEDURE IF EXISTS upload_kingdom; -- Reset the execution delimiter to create a stored program. DELIMITER $$ -- The parentheses after the procedure name must be there or the MODIFIES SQL DATA raises an compile time exception. CREATE PROCEDURE upload_kingdom() MODIFIES SQL DATA BEGIN /* Declare local variables. */ DECLARE lv_kingdom_id INT UNSIGNED; DECLARE lv_kingdom_name VARCHAR(20); DECLARE lv_population INT UNSIGNED; DECLARE lv_knight_id INT UNSIGNED; DECLARE lv_knight_name VARCHAR(24); DECLARE lv_kingdom_allegiance_id INT UNSIGNED; DECLARE lv_allegiance_start_date DATE; DECLARE lv_allegiance_end_date DATE; /* Declare a handler variables. */ DECLARE duplicate_key INT DEFAULT 0; DECLARE foreign_key INT DEFAULT 0; DECLARE fetched INT DEFAULT 0; /* Cursors must come after variables and before event handlers. */ /* Declare a SQL cursor with a left join on the natural key. */ DECLARE kingdom_cursor CURSOR FOR SELECT DISTINCT k.kingdom_id , kki.kingdom_name , kki.population FROM kingdom_knight_import kki LEFT JOIN kingdom k ON kki.kingdom_name = k.kingdom_name AND kki.population = k.population; /* Declare a SQL cursor with a join on the natural key. */ DECLARE knight_cursor CURSOR FOR SELECT kn.knight_id , kki.knight_name , k.kingdom_id , kki.allegiance_start_date AS start_date , kki.allegiance_end_date AS end_date FROM kingdom_knight_import kki INNER JOIN kingdom k ON kki.kingdom_name = k.kingdom_name AND kki.population = k.population LEFT JOIN knight kn ON k.kingdom_id = kn.kingdom_allegiance_id AND kki.knight_name = kn.knight_name AND kki.allegiance_start_date = kn.allegiance_start_date AND kki.allegiance_end_date = kn.allegiance_end_date; /* Event handlers must always be last in the declaration section. */ /* Declare a duplicate key handler */ DECLARE CONTINUE HANDLER FOR 1062 SET duplicate_key = 1; DECLARE CONTINUE HANDLER FOR 1216 SET foreign_key = 1; /* Declare a not found record handler to close a cursor loop. */ DECLARE CONTINUE HANDLER FOR NOT FOUND SET fetched = 1; /* ---------------------------------------------------------------------- */ /* Start transaction context. */ START TRANSACTION; /* Set savepoint. */ SAVEPOINT both_or_none; /* Open a local cursor. */ OPEN kingdom_cursor; cursor_kingdom: LOOP FETCH kingdom_cursor INTO lv_kingdom_id , lv_kingdom_name , lv_population; /* Place the catch handler for no more rows found immediately after the fetch operation. */ IF fetched = 1 THEN LEAVE cursor_kingdom; END IF; INSERT INTO kingdom VALUES ( lv_kingdom_id , lv_kingdom_name , lv_population ) ON DUPLICATE KEY UPDATE kingdom_name = lv_kingdom_name; END LOOP cursor_kingdom; CLOSE kingdom_cursor; /* Reset the continue handler to zero. */ SET fetched = 0; /* Open a local cursor. */ OPEN knight_cursor; cursor_knight: LOOP /* Fetch records until they're all read, and a NOT FOUND SET is returned. */ FETCH knight_cursor INTO lv_knight_id , lv_knight_name , lv_kingdom_allegiance_id , lv_allegiance_start_date , lv_allegiance_end_date; /* Place the catch handler for no more rows found immediately after the fetch operation. */ IF fetched = 1 THEN LEAVE cursor_knight; END IF; INSERT INTO knight VALUES ( lv_knight_id , lv_knight_name , lv_kingdom_allegiance_id , lv_allegiance_start_date , lv_allegiance_end_date ) ON DUPLICATE KEY UPDATE knight_name = lv_knight_name; END LOOP cursor_knight; CLOSE knight_cursor; /* Reset the continue handler to zero. */ SET fetched = 0; /* ---------------------------------------------------------------------- */ /* This acts as an exception handling block. */ IF duplicate_key = 1 OR foreign_key = 1 THEN /* This undoes all DML statements to this point in the procedure. */ ROLLBACK TO SAVEPOINT both_or_none; ELSE /* This commits the writes. */ COMMIT; END IF; END; $$ -- Reset the delimiter to the default. DELIMITER ; |
Here’s the better option with an embedded query:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | -- Conditionally drop the procedure. SELECT 'UPLOAD_KINGDOM' AS "Drop Procedure"; DROP PROCEDURE IF EXISTS upload_kingdom; -- Reset the execution delimiter to create a stored program. DELIMITER $$ -- The parentheses after the procedure name must be there or the MODIFIES SQL DATA raises an compile time exception. CREATE PROCEDURE upload_kingdom() MODIFIES SQL DATA BEGIN /* Declare a handler variables. */ DECLARE duplicate_key INT DEFAULT 0; DECLARE foreign_key INT DEFAULT 0; /* Declare a duplicate key handler */ DECLARE CONTINUE HANDLER FOR 1062 SET duplicate_key = 1; DECLARE CONTINUE HANDLER FOR 1216 SET foreign_key = 1; /* ---------------------------------------------------------------------- */ /* Start transaction context. */ START TRANSACTION; /* Set savepoint. */ SAVEPOINT both_or_none; /* Using subqueries update the targets. */ INSERT INTO knight ( SELECT kn.knight_id , kki.knight_name , k.kingdom_id , kki.allegiance_start_date AS start_date , kki.allegiance_end_date AS end_date FROM kingdom_knight_import kki INNER JOIN kingdom k ON kki.kingdom_name = k.kingdom_name AND kki.population = k.population LEFT JOIN knight kn ON k.kingdom_id = kn.kingdom_allegiance_id AND kki.knight_name = kn.knight_name AND kki.allegiance_start_date = kn.allegiance_start_date AND kki.allegiance_end_date = kn.allegiance_end_date ) ON DUPLICATE KEY UPDATE knight_id = kn.knight_id; INSERT INTO knight ( SELECT kn.knight_id , kki.knight_name , k.kingdom_id , kki.allegiance_start_date AS start_date , kki.allegiance_end_date AS end_date FROM kingdom_knight_import kki INNER JOIN kingdom k ON kki.kingdom_name = k.kingdom_name AND kki.population = k.population LEFT JOIN knight kn ON k.kingdom_id = kn.kingdom_allegiance_id AND kki.knight_name = kn.knight_name AND kki.allegiance_start_date = kn.allegiance_start_date AND kki.allegiance_end_date = kn.allegiance_end_date ) ON DUPLICATE KEY UPDATE knight_id = kn.knight_id; /* ---------------------------------------------------------------------- */ /* This acts as an exception handling block. */ IF duplicate_key = 1 OR foreign_key = 1 THEN /* This undoes all DML statements to this point in the procedure. */ ROLLBACK TO SAVEPOINT both_or_none; ELSE /* This commits the writes. */ COMMIT; END IF; END; $$ -- Reset the delimiter to the default. DELIMITER ; |
Step #6 : Run the upload procedure
You can run the file by calling the stored procedure built by the script. The procedure ensures that records are inserted or updated into their respective tables.
CALL upload_kingdom; |
Step #7 : Test the results of the upload procedure
You can test whether or not it worked by running the following queries.
-- Check the kingdom table. SELECT * FROM kingdom; SELECT * FROM knight; |
It should display the following information:
+------------+--------------+------------+ | kingdom_id | kingdom_name | population | +------------+--------------+------------+ | 1 | Narnia | 77600 | | 2 | Narnia | 42100 | | 3 | Camelot | 15200 | +------------+--------------+------------+ +-----------+-------------------------+-----------------------+-----------------------+---------------------+ | knight_id | knight_name | kingdom_allegiance_id | allegiance_start_date | allegiance_end_date | +-----------+-------------------------+-----------------------+-----------------------+---------------------+ | 1 | 'Peter the Magnificent' | 1 | 1272-03-20 | 1292-06-09 | | 2 | 'Edmund the Just' | 1 | 1272-03-20 | 1292-06-09 | | 3 | 'Susan the Gentle' | 1 | 1272-03-20 | 1292-06-09 | | 4 | 'Lucy the Valiant' | 1 | 1272-03-20 | 1292-06-09 | | 5 | 'Peter the Magnificent' | 2 | 1531-04-12 | 1531-05-31 | | 6 | 'Edmund the Just' | 2 | 1531-04-12 | 1531-05-31 | | 7 | 'Susan the Gentle' | 2 | 1531-04-12 | 1531-05-31 | | 8 | 'Lucy the Valiant' | 2 | 1531-04-12 | 1531-05-31 | | 9 | 'King Arthur' | 3 | 0631-03-10 | 0686-12-12 | | 10 | 'Sir Lionel' | 3 | 0631-03-10 | 0686-12-12 | | 11 | 'Sir Bors' | 3 | 0631-03-10 | 0635-12-12 | | 12 | 'Sir Bors' | 3 | 0640-03-10 | 0686-12-12 | | 13 | 'Sir Galahad' | 3 | 0631-03-10 | 0686-12-12 | | 14 | 'Sir Gawain' | 3 | 0631-03-10 | 0686-12-12 | | 15 | 'Sir Tristram' | 3 | 0631-03-10 | 0686-12-12 | | 16 | 'Sir Percival' | 3 | 0631-03-10 | 0686-12-12 | | 17 | 'Sir Lancelot' | 3 | 0670-09-30 | 0682-12-12 | +-----------+-------------------------+-----------------------+-----------------------+---------------------+ |
You can rerun the procedure to check that it doesn’t alter any information, then you could add a new knight to test the insertion portion.
Merge Statement for ETL
While working through examples for my students on uploading data, I thought it would be interesting to demonstrate how to create a re-runnable upload. Especially when chatting with a friend who was unaware that you could use joins inside the source element of a MERGE
statement. Naturally, the MERGE
statement seemed like the best approach in an Oracle database because with my criteria:
- The source file would not include any surrogate key values.
- The source file would have denormalized record sets with data that should belong to parent and child tables, technically unnormalized form (UNF).
- Primary and foreign key values would be determined on load to the tables.
- There could be a one-to-many relationship between the parent and child tables in the original source.
- Subsequent data sets may replicate data already seeded or not in the tables.
- Avoid any complex PL/SQL structures.
Step #1 : Create a Virtual Directory
You can create a virtual directory without a physical directory but it won’t work when you try to access it. Therefore, you should create the physical directory first. Assuming you’ve created a C:\Data\Download
file directory on the Windows platform, you can then create a virtual directory and grant permissions to the student
user as the SYS
privileged user. The syntax for these steps is:
CREATE DIRECTORY download AS 'C:\Data\Download'; GRANT READ, WRITE ON DIRECTORY download TO student; |
If you want more detail on these steps, check this older post on the blog.
Step #2 : Create the Target and External Tables
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | -- Conditionally drop tables and sequences. BEGIN FOR i IN (SELECT TABLE_NAME FROM user_tables WHERE TABLE_NAME IN ('KINGDOM','KNIGHT','KINGDOM_KNIGHT_IMPORT')) LOOP EXECUTE IMMEDIATE 'DROP TABLE '||i.table_name||' CASCADE CONSTRAINTS'; END LOOP; FOR i IN (SELECT sequence_name FROM user_sequences WHERE sequence_name IN ('KINGDOM_S1','KNIGHT_S1')) LOOP EXECUTE IMMEDIATE 'DROP SEQUENCE '||i.sequence_name; END LOOP; END; / -- Create normalized kingdom table. CREATE TABLE kingdom ( kingdom_id NUMBER , kingdom_name VARCHAR2(20) , population NUMBER); -- Create a sequence for the kingdom table. CREATE SEQUENCE kingdom_s1; -- Create normalized knight table. CREATE TABLE knight ( knight_id NUMBER , knight_name VARCHAR2(24) , kingdom_allegiance_id NUMBER , allegiance_start_date DATE , allegiance_end_date DATE); -- Create a sequence for the knight table. CREATE SEQUENCE knight_s1; -- Create external import table. CREATE TABLE kingdom_knight_import ( kingdom_name VARCHAR2(20) , population NUMBER , knight_name VARCHAR2(24) , allegiance_start_date DATE , allegiance_end_date DATE) ORGANIZATION EXTERNAL ( TYPE oracle_loader DEFAULT DIRECTORY download ACCESS PARAMETERS ( RECORDS DELIMITED BY NEWLINE CHARACTERSET US7ASCII BADFILE 'DOWNLOAD':'kingdom_import.bad' DISCARDFILE 'DOWNLOAD':'kingdom_import.dis' LOGFILE 'DOWNLOAD':'kingdom_import.log' FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY "'" MISSING FIELD VALUES ARE NULL ) LOCATION ('kingdom_import.csv')) REJECT LIMIT UNLIMITED; |
Step #3 : Create a Procedure to ensure an all or nothing transaction
The procedure ensures that an all or nothing transaction occurs to both tables. Inside the procedure you have two MERGE
statements.
The first MERGE
statement uses a LEFT JOIN
to ensure that any new kingdom_name
will be added to the kingdom
table. The kingdom_name
and population
columns are the natural key in this model. The second MERGE
statement uses an INNER JOIN
to ensure that knight
rows are only inserted when they belong to an existing kingdom_name
. Naturally, the primary key capture occurs in this statement and it maps the primary key to the foreign key column in the knight
table.
The complete procedure code follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | -- Create a procedure to wrap the transaction. CREATE OR REPLACE PROCEDURE upload_kingdom IS BEGIN -- Set save point for an all or nothing transaction. SAVEPOINT starting_point; -- Insert or update the table, which makes this rerunnable when the file hasn't been updated. MERGE INTO kingdom target USING (SELECT DISTINCT k.kingdom_id , kki.kingdom_name , kki.population FROM kingdom_knight_import kki LEFT JOIN kingdom k ON kki.kingdom_name = k.kingdom_name AND kki.population = k.population) SOURCE ON (target.kingdom_id = SOURCE.kingdom_id) WHEN MATCHED THEN UPDATE SET kingdom_name = SOURCE.kingdom_name WHEN NOT MATCHED THEN INSERT VALUES ( kingdom_s1.nextval , SOURCE.kingdom_name , SOURCE.population); -- Insert or update the table, which makes this rerunnable when the file hasn't been updated. MERGE INTO knight target USING (SELECT k.kingdom_id , kki.knight_name , kki.allegiance_start_date AS start_date , kki.allegiance_end_date AS end_date FROM kingdom_knight_import kki INNER JOIN kingdom k ON kki.kingdom_name = k.kingdom_name AND kki.population = k.population) SOURCE ON (target.kingdom_allegiance_id = SOURCE.kingdom_id) WHEN MATCHED THEN UPDATE SET allegiance_start_date = SOURCE.start_date , allegiance_end_date = SOURCE.end_date WHEN NOT MATCHED THEN INSERT VALUES ( knight_s1.nextval , SOURCE.knight_name , SOURCE.kingdom_id , SOURCE.start_date , SOURCE.end_date); -- Save the changes. COMMIT; EXCEPTION WHEN OTHERS THEN ROLLBACK TO starting_point; RETURN; END; / |
Step #4 : Test the Process
You can test it by calling the procedure. Rerunning it will demonstrate that it doesn’t violate any of the rules.
EXECUTE upload_kingdom; |
As always, I hope this is useful to somebody besides me.
PL/SQL Workbook Code
I got a request Saturday for me to post code for the Oracle Database 11g PL/SQL Programming Workbook. You can download the book code here. It should also be on the McGraw-Hill web site tomorrow.
The irony for me is the timing of the request. I didn’t get it until late Saturday night when I had to make an early plane to Dallas, Texas on Sunday morning. It teaches me once again, that I should keep my book updates in one place and backup in a convenient carry-anywhere location.
I also found out that the Bulletin Board I’d set up wasn’t accessible. At least, accessible to anybody but bots. I uninstalled and re-installed it, and configured it. Now I’ll start maintaining it.
My author’s copies came
My co-author got his author copies about a week before me. The technical editor got his complementary book too the same day. I was wondering where mine got routed but now I’ve got mine, a few days after it was available on Amazon.
I think the new book looks good. John and I were probably a real pain during the editing process because we kept asking for more control during the proof copy to get things fixed. I think we messed up the release schedule by a couple weeks.
John and I think that the mastery questions in each chapter work well. My students who’ve seen it think it’s great because of the review sections and its smaller size. I’m re-verifying the code now, and it should be on McGraw-Hill’s website next week.
Sample PL/SQL Cursor Loops
A few of my students wanted me to post sample cursor loops, so here are examples of simple, FOR
, and WHILE
loops. There are a couple variations on approaches that demonstrate %TYPE
and %ROWTYPE
anchoring to data dictionary table definitions and local cursors.
Part of the idea behind these examples is to show the basic structure while mimicking the \G
option of MySQL. The \G
(Go) displays results as a list of column names and values by row. Ever since I discovered that in MySQL, I’ve hoped Oracle would incorporate something similar in their product. While discussing my wish list, I’d also like Oracle to make the FROM dual
optional (like MySQL does) when selecting a string or numeric literal. You can find an implementation here, that leverages an example from Tom Kyte.
You can click any of the titles to view the code, which isn’t needed when you don’t have JavaScript enabled or the RSS expands them for you.
Simple loop with local variables ↓
This simple loop example uses a static cursor and local variables that map to each column returned by the cursor. It uses the %TYPE
to anchor local variables to the data dictionary.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | SET SERVEROUTPUT ON SIZE 1000000 DECLARE -- Declare local variables that are anchored to column data types. lv_title item.item_title%TYPE; lv_subtitle item.item_subtitle%TYPE; lv_rating item.item_rating%TYPE; -- Declare a static cursor. CURSOR c IS SELECT i.item_title AS title , i.item_subtitle AS subtitle , i.item_rating AS rating FROM item i; BEGIN -- Open the cursor. OPEN c; -- Print a starting line. dbms_output.put_line('----------------------------------------------------------------------'); -- Start the simple loop block. LOOP -- Fetch a row of the cursor and assign it to the three local variables. FETCH c INTO lv_title , lv_subtitle , lv_rating; -- Exit when there aren't any more records in the cursor, without this you loop infinitely. EXIT WHEN c%NOTFOUND; -- Print the local variables on a single line each to mimic MySQL \G equivalent. dbms_output.put_line('ITEM.ITEM_TITLE ['||lv_title||']'); dbms_output.put_line('ITEM.ITEM_SUBTITLE ['||lv_subtitle||']'); dbms_output.put_line('ITEM.ITEM_RATING ['||lv_rating||']'); -- Print an ending line. dbms_output.put_line('----------------------------------------------------------------------'); END LOOP; -- Close the cursor and release the resources. CLOSE c; END; / |
Simple loop with a local record structure variable ↓
This simple loop example uses a static cursor, a local record structure data type, and a local variable of the local record structure data type. The local record structure maps to the columns returned by the cursor. It uses explicit data types that match those of the table. You could also use the %TYPE
to anchor the elements of the structure in the local data type, like the prior example. With explicit data types, you must modify the program when the definition of the table changes otherwise your program may fail at runtime.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | SET SERVEROUTPUT ON SIZE 1000000 DECLARE -- Declare a local record data type, with explicit data types (you could use %TYPE here too). TYPE title_type IS RECORD ( title VARCHAR2(60) , subtitle VARCHAR2(60) , rating VARCHAR2(8)); -- Declare a local variable of the local record structure data type. item_record TITLE_TYPE; -- Declare a static cursor. CURSOR c IS SELECT i.item_title AS title , i.item_subtitle AS subtitle , i.item_rating AS rating FROM item i; BEGIN -- Open the cursor. OPEN c; -- Print a starting line. dbms_output.put_line('----------------------------------------------------------------------'); -- Start the simple loop block. LOOP -- Fetch a row of the cursor and assign it to the local record structure variable. FETCH c INTO item_record; -- Exit when there aren't any more records in the cursor, without this you loop infinitely. EXIT WHEN c%NOTFOUND; -- Print the local variable elements on a single line each to mimic MySQL \G equivalent. dbms_output.put_line('ITEM.ITEM_TITLE ['||item_record.title||']'); dbms_output.put_line('ITEM.ITEM_SUBTITLE ['||item_record.subtitle||']'); dbms_output.put_line('ITEM.ITEM_RATING ['||item_record.rating||']'); -- Print an ending line. dbms_output.put_line('----------------------------------------------------------------------'); END LOOP; -- Close the cursor and release the resources. CLOSE c; END; / |
Simple loop with a local cursor structure variable ↓
This simple loop example uses a static cursor, a local variable that inherits its record structure from the local cursor. It does so by using the %ROWTYPE
against the cursor. Often the %ROWTYPE
is only applied when you return a structure that maps to the complete table definition. Sometimes I think using cursor_name%ROWTYPE is the only real purpose for shared cursors but I know that’s not really true.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | SET SERVEROUTPUT ON SIZE 1000000 DECLARE -- Declare a static cursor. CURSOR c IS SELECT i.item_title AS title , i.item_subtitle AS subtitle , i.item_rating AS rating FROM item i; -- Declare a local variable of that inherits its structure from a local cursor. item_record c%ROWTYPE; BEGIN -- Open the cursor. OPEN c; -- Print a starting line. dbms_output.put_line('----------------------------------------------------------------------'); -- Start the simple loop block. LOOP -- Fetch a row of the cursor and assign it to the local record structure variable. FETCH c INTO item_record; -- Exit when there aren't any more records in the cursor, without this you loop infinitely. EXIT WHEN c%NOTFOUND; -- Print the local variable elements on a single line each to mimic MySQL \G equivalent. dbms_output.put_line('ITEM.ITEM_TITLE ['||item_record.title||']'); dbms_output.put_line('ITEM.ITEM_SUBTITLE ['||item_record.subtitle||']'); dbms_output.put_line('ITEM.ITEM_RATING ['||item_record.rating||']'); -- Print an ending line. dbms_output.put_line('----------------------------------------------------------------------'); END LOOP; -- Close the cursor and release the resources. CLOSE c; END; / |
For loop with an implicit record structure variable ↓
This FOR
loop example uses a static cursor. When a FOR
loop uses a cursor it becomes a cursor FOR
loop, and the iterator i
becomes an implicit cursor record structure. You should note that this is a very compact program because a cursor FOR
loop manages opening and closing the cursor, and handling the loop exit implicitly. Many programmers default to this approach whenever it fits because it is simple and easy to implement.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | SET SERVEROUTPUT ON SIZE 1000000 DECLARE -- Declare a static cursor. CURSOR c IS SELECT i.item_title AS title , i.item_subtitle AS subtitle , i.item_rating AS rating FROM item i; BEGIN -- Print a starting line. dbms_output.put_line('----------------------------------------------------------------------'); -- Start a cursor FOR loop block. FOR i IN c LOOP -- Print the local variable elements on a single line each to mimic MySQL \G equivalent. dbms_output.put_line('ITEM.ITEM_TITLE ['||i.title||']'); dbms_output.put_line('ITEM.ITEM_SUBTITLE ['||i.subtitle||']'); dbms_output.put_line('ITEM.ITEM_RATING ['||i.rating||']'); -- Print an ending line. dbms_output.put_line('----------------------------------------------------------------------'); END LOOP; END; / |
For loop without a declaration block ↓
This FOR
loop example uses a static cursor. Like the prior example, this FOR
loop uses a cursor but it is defined inside the actual FOR
loop structure. That approach eliminates the need for the declaration block. It’s a nice feature that some may call a trick. As a rule, you should really avoid this style because formally defining your cursor is a good practice and improves code maintainability. Naturally, this is probably the most compact program because everything is managed implicitly including the cursor assignment to the loop structure.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | SET SERVEROUTPUT ON SIZE 1000000 BEGIN -- Print a starting line. dbms_output.put_line('----------------------------------------------------------------------'); -- Start a cursor FOR loop block with the static cursor in the definition. FOR i IN (SELECT i.item_title AS title , i.item_subtitle AS subtitle , i.item_rating AS rating FROM item i) LOOP -- Print the local variable elements on a single line each to mimic MySQL \G equivalent. dbms_output.put_line('ITEM.ITEM_TITLE ['||i.title||']'); dbms_output.put_line('ITEM.ITEM_SUBTITLE ['||i.subtitle||']'); dbms_output.put_line('ITEM.ITEM_RATING ['||i.rating||']'); -- Print an ending line. dbms_output.put_line('----------------------------------------------------------------------'); END LOOP; END; / |
WHILE
loop with a cursor guard on entry statement ↓
This WHILE
loop example uses a static cursor, like the prior examples. Unlike the prior example, the WHILE
loop is a guard on entry loop. The previous loops were guard on exit loops. This has much the same structure as the simple loop with a cursor record structure variable but differs on two key points.
Point one is that you must have two FETCH
statements because the guard on entry condition checks whether any records are found in the cursor. The first FETCH
statement checks whether at least a one row is returned. When true or false, it initializes the cursor attributes, like %FOUND
. The second FETCH
statement handles the second row returned to last row returned from the cursor.
Point two is that you don’t have an EXIT WHEN cursor_name%NOTFOUND
inside the loop because the guard condition stops the loop when it fails to find at least one record.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | DECLARE -- Declare a static cursor. CURSOR c IS SELECT i.item_title AS title , i.item_subtitle AS subtitle , i.item_rating AS rating FROM item i; -- Declare a local variable of that inherits its structure from a local cursor. item_record c%ROWTYPE; BEGIN -- Open the cursor. OPEN c; -- Print a starting line. dbms_output.put_line('----------------------------------------------------------------------'); -- Fetch the first record to put into context the cursor attributes, like %FOUND. FETCH c INTO item_record; -- Start the simple loop block with a guard on entry condition. WHILE (c%FOUND) LOOP -- Print the local variable elements on a single line each to mimic MySQL \G equivalent. dbms_output.put_line('ITEM.ITEM_TITLE ['||item_record.title||']'); dbms_output.put_line('ITEM.ITEM_SUBTITLE ['||item_record.subtitle||']'); dbms_output.put_line('ITEM.ITEM_RATING ['||item_record.rating||']'); -- Print an ending line. dbms_output.put_line('----------------------------------------------------------------------'); -- Fetch the second and subsequent rows of the cursor and assign it to a local variables. FETCH c INTO item_record; END LOOP; -- Close the cursor and release the resources. CLOSE c; END; / |
The WHILE
loop as presented is complex because of the pre-loop FETCH
statement, and the internal FETCH
statement. It’s may appear better to convert it to a pseudo-infinite loop. You do that by setting the condition in a WHILE
loop to a TRUE
constant, like this example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | DECLARE -- Declare a static cursor. CURSOR c IS SELECT i.item_title AS title , i.item_subtitle AS subtitle , i.item_rating AS rating FROM item i; -- Declare a local variable of that inherits its structure from a local cursor. item_record c%ROWTYPE; BEGIN -- Open the cursor. OPEN c; -- Print a starting line. dbms_output.put_line('----------------------------------------------------------------------'); -- Start the simple loop block with a guard on entry condition. WHILE (TRUE) LOOP -- Fetch the record set into a user-defined variable. FETCH c INTO item_record; -- Exit when there aren't any more records in the cursor, without this you loop infinitely. EXIT WHEN c%NOTFOUND; -- Print the local variable elements on a single line each to mimic MySQL \G equivalent. dbms_output.put_line('ITEM.ITEM_TITLE ['||item_record.title||']'); dbms_output.put_line('ITEM.ITEM_SUBTITLE ['||item_record.subtitle||']'); dbms_output.put_line('ITEM.ITEM_RATING ['||item_record.rating||']'); -- Print an ending line. dbms_output.put_line('----------------------------------------------------------------------'); END LOOP; -- Close the cursor and release the resources. CLOSE c; END; / |
The preceding example behaves much like a simple loop, and you have to ask what is the benefit of WHILE (TRUE) LOOP
over LOOP
. Generally, it appears that the WHILE
loop syntax is slighly longer to type.
I’m sure this will help my students and hope it helps somebody else.
Oracle 11g XDB Shake & Bake
It’s a bit awkward when a post generates a new question, but here’s a quick explanation and example of using XDB (XML Database Server) outside of the realm of APEX. More or less, XDB is an Apache Server equivalent configured inside the database. It’s really a protocol server tied into the Shared Server Oracle*Net Architecture (a correction provided by Marco Gralike). As a note, testing was done by using a NAT static IP addressing for the virtual Windows XP, Vista, and 7 environments.
This blog post will show you how to experiment with the PL/SQL Web Toolkit and build both password protected and unprotected database content. It assumes you have access to the SYS
privileged account.
Setting Up a Secure DAD
There’s secure and then there’s secure. This falls in the less than secure category but it does provide a password and uses basic HTTP authentication. The USER
is the schema name, and the PASSWORD
is the same as that for the SQL*Plus access to the schema.
- Connect as the privileged
SYS
user and run the following script. It creates a genericSTUDENT
user and grants minimalist privileges, then it creates a DAD (Data Access Descriptor), and authorizes the DAD. Don’t run the command if you’re actively using Oracle APEX on the default configuration of port 8080. It’s there for those folks you are running Tomcat on 8080.
-- This resets the default port so that it doesn't conflict with other environment. EXECUTE dbms_xdb.SETHTTPPORT(8181); -- This creates the STUDENT Data Access Descriptor. EXECUTE dbms_epg.create_dad('STUDENT_DAD','/sampledb/*'); -- This authorizes the STUDENT_DAD EXECUTE dbms_epg.authorize_dad('STUDENT_DAD','STUDENT'); |
- Connect as the
STUDENT
user and run the following script to create a PL/SQL Web Toolkit procedure.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | CREATE OR REPLACE PROCEDURE HelloWorld AS BEGIN -- Set an HTML meta tag and render page. owa_util.mime_header('text/html'); -- <META Content-type:text/html> htp.htmlopen; -- <HTML> htp.headopen; -- <HEAD> htp.htitle('Hello World!'); -- <TITLE>HelloWorld!</TITLE> htp.headclose; -- </HEAD> htp.bodyopen; -- <BODY> htp.line; -- <HR> htp.print('Hello ['||USER||']!'); -- Hello [dynamic user_name]! htp.line; -- <HR> htp.bodyclose; -- </BODY> htp.htmlclose; -- </HTML> END HelloWorld; / |
- Open a browser of your choice, and enter the following URL.
http://localhost:8181/sampledb/helloworld |
You then see (or should see) the following Basic HTTP Authentication dialog box. Enter the STUDENT
user as the User Name and the Password for the database account. Then, click the OK button.
Provided you enter the User Name and Password correctly, you should see the following inside the browser’s display panel. The USER
name is a system session scope variable, which will always return the owner of the package because its created as a Definers Rights procedure.
You have now successfully configured your Basic HTTP Authentication XDB, which may offer you some possibilities outside of using Oracle APEX.
Setting Up an Unsecured DAD
The trick here is building on what you did by eliminating the authentication. You do this by using the ANONYMOUS
account, like Oracle’s APEX does. Well, not quite like it does because APEX provides a very good user authentication model. It allows you to connect to the ANONYMOUS
user where you present and validate your credentials.
Since you have to do all the prior steps, these steps are numbered after those above. You start with step #4.
- Generally, the XML configuration is missing one key node that allows repository anonymous access. The missing node disallows anonymous login. You can run the code below as the
SYS
privileged user to open that up if its missing. You can check whether or not it’s missing by running this as theSYS
user:
SQL> @?/rdbms/admin/epgstat.sql |
If it returns the following as the last element of the output, you’ll need to run the PL/SQL block below.
+-------------------------------------------------------------------+ | ANONYMOUS access to XDB repository: | | To allow public access to XDB repository without authentication, | | ANONYMOUS access to the repository must be allowed. | +-------------------------------------------------------------------+ Allow repository anonymous access? ---------------------------------- false 1 row selected. |
When you run this script, make sure you’re the privileged SYS
user. Then, rerun the epgstat.sql
script to verify that you’ve enabled anonymous access to the repository. You may also need to refresh your browser cache before retesting it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | SET SERVEROUTPUT ON DECLARE lv_configxml XMLTYPE; lv_value VARCHAR2(5) := 'true'; -- (true/false) BEGIN lv_configxml := DBMS_XDB.cfg_get(); -- Check for the element. IF lv_configxml.existsNode('/xdbconfig/sysconfig/protocolconfig/httpconfig/allow-repository-anonymous-access') = 0 THEN -- Add missing element. SELECT insertChildXML ( lv_configxml , '/xdbconfig/sysconfig/protocolconfig/httpconfig' , 'allow-repository-anonymous-access' , XMLType('<allow-repository-anonymous-access xmlns="http://xmlns.oracle.com/xdb/xdbconfig.xsd">' || lv_value || '</allow-repository-anonymous-access>') , 'xmlns="http://xmlns.oracle.com/xdb/xdbconfig.xsd"') INTO lv_configxml FROM dual; DBMS_OUTPUT.put_line('Element inserted.'); ELSE -- Update existing element. SELECT updateXML ( DBMS_XDB.cfg_get() , '/xdbconfig/sysconfig/protocolconfig/httpconfig/allow-repository-anonymous-access/text()' , lv_value , 'xmlns="http://xmlns.oracle.com/xdb/xdbconfig.xsd"') INTO lv_configxml FROM dual; DBMS_OUTPUT.put_line('Element updated.'); END IF; -- Configure the element. DBMS_XDB.cfg_update(lv_configxml); DBMS_XDB.cfg_refresh; END; / |
- These tasks also require the privileged
SYSTEM
user account, and you should sign on to that account to run these commands. The first thing you may need to do is unlock theANONYMOUS
account. It is locked by default. After you unlock it, you’ll need to verify no default password was assigned by unassigning a password. The following two commands accomplish those tasks.
-- Unlock the user account. ALTER USER anonymous ACCOUNT UNLOCK; -- Ensure a password is assigned to the account so you can create a synonym later. ALTER USER anonymous IDENTIFIED BY ANONYMOUS; |
- These tasks require the privileged
SYS
user account because you’re going to create and authorize another DAD.
-- This creates the STUDENT_DB_DAD Data Access Descriptor. EXECUTE dbms_epg.create_dad('STUDENT_DB_DAD','/db/*'); -- This authorizes the STUDENT_DB_DAD EXECUTE dbms_epg.authorize_dad('STUDENT_DB_DAD','ANONYMOUS'); -- Open the anonymous account by setting the database-username parameter and value. EXECUTE dbms_epg.set_dad_attribute('STUDENT_DB_DAD','database-username','ANONYMOUS'); |
- Connect as the
STUDENT
user and grantEXECUTE
permissions on theHELLOWORLD
procedure to theANONYMOUS
user account. TheGRANT
allows you to give unrestricted access to theANONYMOUS
account, which in turn provides it to your web audience.
SQL> GRANT EXECUTE ON helloworld TO anonymous; |
- Connect as the
ANONYMOUS
user and create a local synonym that point to theSTUDENT.HELLOWORLD
procedure. TheSYNONYM
provides a program name for the URL statement. It’s hides the ownership of the actual procedure by supressing the schema name. (You may need to grantCREATE ANY SYNONYM
as theSYSTEM
user to theANONYMOUS
user.)
SQL> CREATE SYNONYM helloworld FOR student.helloworld; |
ANONYMOUS
account. The following syntax lets you do that as the privileged SYSTEM
user.
SQL> ALTER USER anonymous IDENTIFIED BY NULL; |
- Open a browser of your choice, and enter the following URL, which won’t require a User Name or Password.
http://localhost:8181/db/helloworld |
You should see the same browser panel information as that shown by step #3 above, except one thing. The difference is the user name, which should now be ANONYMOUS
. The execution occurs with the permissions of the invoker. This means you’ll see the data you’re allowed to see by the owning schema.
Oracle 11g XDB DADs
Somebody asked me why the DBMS_EPG.GET_DAD_LIST
is a procedure because you can’t just simply list the DAD
values. I answered that Oracle chose to implement it that way. Then, they asked how they could query it. I suggested they just run the epgstat.sql
diagnostic script provided in the $ORACLE_HOME/rdbms
directory, which provides those values and much more.
You can run the diagnostic script as the SYS
privileged user, or as any user that has been granted the XDBADMIN
role, like this:
SQL> @?/rdbms/admin/epgstat.sql |
Notwithstanding the diagnostic script, they asked how you could wrap the OUT
mode PL/SQL data type in the procedure call, and return the list of values in a SQL query. Because the formal parameter is a PL/SQL data type, this requires two key things. One is a local variable that maps to the DBMS_EPG
package collection data type, and a pipelined table function. Here’s one way to solve the problem:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | CREATE OR REPLACE TYPE dad_list AS TABLE OF VARCHAR2(4000); / CREATE OR REPLACE FUNCTION get_dbms_epg_dads RETURN dad_list PIPELINED IS -- Deine a local variable as the OUT mode target of GET_DAD_LIST procedure. SOURCE DBMS_EPG.VARCHAR2_TABLE; -- Declare a local variable of the SQL collection data type. list DAD_LIST := dad_list(); BEGIN -- Call the procedure to populate the source. dbms_epg.get_dad_list(SOURCE); -- Extend space for all defined DAD values. list.EXTEND(SOURCE.COUNT); -- Assign values from PL/SQL collection to SQL collection. FOR i IN 1..source.COUNT LOOP list(i) := SOURCE(i); PIPE ROW(list(i)); END LOOP; RETURN; END get_dbms_epg_dads; / -- Set SQL*Plus width. SET LINESIZE 79 -- Query collection. SELECT column_value AS "DAD LIST" FROM TABLE(get_dbms_epg_dads); |
Marco Gralike provided a simpler approach them the Pipelined Table Function here. I’ve copied the code example below:
1 2 3 4 5 6 | SELECT u.dad AS "PL/SQL DAD List" FROM XMLTable(XMLNAMESPACES ( DEFAULT 'http://xmlns.oracle.com/xdb/xdbconfig.xsd' ) , '/xdbconfig/sysconfig/protocolconfig/httpconfig/webappconfig/servletconfig/servlet-list/servlet[servlet-language="PL/SQL"]' PASSING DBMS_XDB.CFG_GET() COLUMNS DAD VARCHAR2(15) PATH '/servlet/servlet-name/text()') u; |
Hope this proves handy to somebody else, too.
Seeding a Calendar Table
While working on one of the labs, my students wanted a quick way to seed their CALENDAR
table. Here’s a small script to seed twenty years of a calendar.
-- Conditionally drop the table. BEGIN FOR i IN (SELECT TABLE_NAME FROM user_tables WHERE TABLE_NAME = 'MOCK_CALENDAR') LOOP EXECUTE IMMEDIATE 'DROP TABLE '||i.table_name||' CASCADE CONSTRAINTS'; END LOOP; END; / -- Create the table. CREATE TABLE mock_calendar ( short_month VARCHAR2(3) , long_month VARCHAR2(9) , start_date DATE , end_date DATE ); -- Seed the table with 10 years of data. DECLARE -- Create local collection data types. TYPE smonth IS TABLE OF VARCHAR2(3); TYPE lmonth IS TABLE OF VARCHAR2(9); -- Declare month arrays. short_month SMONTH := smonth('JAN','FEB','MAR','APR','MAY','JUN' ,'JUL','AUG','SEP','OCT','NOV','DEC'); long_month LMONTH := lmonth('January','February','March','April','May','June' ,'July','August','September','October','November','December'); -- Declare base dates. start_date DATE := '01-JAN-79'; end_date DATE := '31-JAN-79'; -- Declare years. years NUMBER := 20; BEGIN -- Loop through years and months. FOR i IN 1..years LOOP FOR j IN 1..short_month.COUNT LOOP INSERT INTO mock_calendar VALUES ( short_month(j) , long_month(j) , add_months(start_date,(j-1)+(12*(i-1))) , add_months(end_date,(j-1)+(12*(i-1)))); END LOOP; END LOOP; END; / -- Format set break for output. SET PAGESIZE 16 -- Format column output. COL short_month FORMAT A5 HEADING "Short|Month" COL long_month FORMAT A9 HEADING "Long|Month" COL start_date FORMAT A9 HEADING "Start|Date" COL end_date FORMAT A9 HEADING "End|Date" SELECT * FROM mock_calendar; |
The output will look like this with a page break by year:
Short Long Start End Month Month Date Date ----- --------- --------- --------- JAN January 01-JAN-79 31-JAN-79 FEB February 01-FEB-79 28-FEB-79 MAR March 01-MAR-79 31-MAR-79 APR April 01-APR-79 30-APR-79 MAY May 01-MAY-79 31-MAY-79 JUN June 01-JUN-79 30-JUN-79 JUL July 01-JUL-79 31-JUL-79 AUG August 01-AUG-79 31-AUG-79 SEP September 01-SEP-79 30-SEP-79 OCT October 01-OCT-79 31-OCT-79 NOV November 01-NOV-79 30-NOV-79 DEC December 01-DEC-79 31-DEC-79 |
Copyright or not?
I’m back from vacation and will start blogging again. I’ve got a couple items folks have asked me to cover and I’ll try to work those in during the next couple of weeks.
My son went over to Beijing to study Chinese this summer. I picked him up at the Airport last Friday. When he was there he found a copy of my Oracle Database 11g PL/SQL Programming in Chinese. He told me about it via Skype. I asked him to bring home a copy since my royalty statements don’t show the book was translated into Chinese. It cost about $12 US (click on the image to see a larger copy). The statements indicate it has only been translated into Russian, Portuguese, and Tata’s Indian Press at this point.
I thought it was an illegal copy, even though it was sold in a mainline store in Beijing as an original. Anyway I sent the scans of the book and ISBN to McGraw-Hill so they could sort it out. They told me it’s a legal copy, translation licensed, and that not all translations fees post to royalty statements before the translation. It’s only the fifth book, and I continue to learn about the process. Tsinghua University Press produces it. If you want a copy in Chinese, you’ll find it at their web site and my book is here. My son said they had an impressive collection of Oracle Press books on sale there too.