MacLochlainns Weblog

Michael McLaughlin's Technical Blog

Site Admin

Archive for the ‘Oracle Developer’ tag

Session Variables

without comments

In MySQL and Oracle, you set a session variable quite differently. That means you should expect there differences between setting a session variable in Postgres. This blog post lets you see how to set them in all three databases. I’m always curious what people think but I’m willing to bet that MySQL is the simplest approach. Postgres is a bit more complex because you must use a function call, but Oracle is the most complex.

The difference between MySQL and Postgres is an “@” symbol versus a current_setting() function call. Oracle is more complex because it involves the mechanics in Oracle’s sqlplus shell, SQL dialect, and PL/SQL language (required to assign a value to a variable).

MySQL

MySQL lets you declare a session variable in one step and use it one way in a SQL statement or stored procedure.

  1. You set a session variable on a single line with the following command:

    SET @my_variable_name := 'My Value';

  2. You can query a variable from the pseudo table dual or as a comparison value in the SELECT-list

    SELECT @my_variable_name AS "The Value" FROM dual;

    or WHERE clause

    SELECT column_name
    FROM   table_name
    WHERE  column_name = @my_variable_name;

Postgres

Postgres lets you declare a session variable in one step and use it one way in a SQL statement or stored procedure.

  1. You set a session variable in a single line. It iss critical to note that you must use double quotes around the session variable name and single quotes for the value. You raise an error when you use a single quote instead a double quote around the session variable name. The syntax is:

    SET SESSION "videodb.table_name" = 'new_hire';

  2. You can query a variable from the pseudo table dual or as a comparison value in the SELECT-list with the current_setting() function call.

    SELECT current_setting('videodb.table_name') AS "The Value";

    or WHERE clause

    SELECT column_name
    FROM   table_name
    WHERE  column_name = current_setting('videodb.table_name');

Oracle

There are two steps required to declare a session variable in Oracle. First, you need to define the variable in the SQL*Plus session. Oracle lets you define a variable like you would define a variable in the C language, using extern before the variable’s type. Second, you assign a value to the session variable in an anonymous PL/SQL block. There is no single line statement to declare a variable with an initial value.

  1. You set a session variable by using the VARIABLE keyword, a variable name, and data type. The supported data types are: BLOB, BFILE, BINARY_DOUBLE, BINARY_FLOAT, CHAR, CLOB, NCHAR, NCLOB, NVARCHAR2, REFCURSOR, and VARCHAAR2. You define a variable with the following syntax:

    VARIABLE bv_variable_name VARCHAR2(30)

  2. You assign a value to the bind variable inside an anonymous block by prefacing the variable name with a colon. You assign values inside PL/SQL with the walrus operator (:=) and a string enclosed by single quotes. Anonymous blocks start with a BEGIN and end with an END followed by a semicolon (;) and a forward slash (/) to dispatch the block for execution. The following example shows a full block:

    BEGIN
      :bv_variable_name := 'Some Value';
    END;
    /

  3. You can query any declared variable from the pseudo table dual or as a comparison value in the SELECT-list

    SELECT :bv_variable_name FROM dual;

    or WHERE clause

    SELECT column_name
    FROM   table_name
    WHERE  column_name = :bv_variable_name;

Written by maclochlainn

September 28th, 2019 at 9:01 pm

Misleading ORA- Message

without comments

Oracle error messages are more or less the best in the industry but time-to-time they saddle you with a bad or misleading message. For example, I was running one of the code modules from my Oracle Database 12c PL/SQL Programming book for a class exercise and got this error message:

BEGIN
*
ERROR AT line 1:
ORA-22288: FILE OR LOB operation  failed
ORA-06512: AT "STUDENT.LOAD_CLOB_FROM_FILE", line 71
ORA-06512: AT line 11

Oddly enough, it was simple to identify generally. It failed on a call to the DBMS_LOB.LOADCLOBFROMFILE procedure. However, the better question is why did it fail because the virtual directory resolved and the permissions worked.

The first test was to try another file, which worked perfectly with the same code. That meant it had to be something with the physical file. I took a look and sure enough I found a character set problem, like the following:

… he reveals that the Nazgûl, or Ringwraiths, have left Mordor to capture the Ring and kill whoever carries it.

and,

The group flees to the elvish realm of Lothlórien …

The “û” and “ó” characters were incompatible with the default NLS_LANG setting of the database and a CLOB limits the use of non-standard character sets. It’s ashamed that Oracle didn’t through a character set error, which would have expedited resolution of the problem.

As always, I hope this helps those looking for solutions.

Written by maclochlainn

August 17th, 2019 at 4:52 pm

Oracle Error Bash f(x)

without comments

My students always struggle initially with basic Linux skills. I wrote little function for their .bashrc file to help them avoid the frustration. It finds and displays all errors by file name, line number and error message for a collection of log files in a single directory (or folder).

errors()
{
  # Determine if any log files exist and check for errors.
  label="File Name:Line Number:Error Code"
  list=`ls ./*.$1 | wc -l`
  if [[ $list} -eq 1 ]]; then
    echo ${label}
    echo "--------------------------------------------------"
    filename=`ls *.txt`
    echo ${filename}:`find . -type f | grep -in *.txt -e ora\- -e pls\- -e sp2\-`
  elif [[ ${list} -gt 1 ]]; then
    echo ${label}
    echo "--------------------------------------------------"
    find . -type f | grep -in *.txt -e ora\- -e pls\- -e sp2\-
  fi
}

Let’s say you name your log files with a file extension of .txt, then you would call the function like this:

errors txt

It would return output like the following:

common_lookup_lab.txt:229:ORA-02275: such a referential constraint already exists in the table
common_lookup_lab.txt:239:ORA-02275: such a referential constraint already exists in the table

As always, I hope this helps those looking for a solution.

Written by maclochlainn

August 13th, 2019 at 8:17 pm

Create Oracle User

without comments

After you create and provision the Oracle Database 11g XE, you create an instance with the following two step process.

  1. Create a student Oracle user account with the following command:

    CREATE USER student IDENTIFIED BY student
    DEFAULT TABLESPACE users QUOTA 200M ON users
    TEMPORARY TABLESPACE temp;

  2. Grant necessary privileges to the newly created student user:

    GRANT CREATE CLUSTER, CREATE INDEXTYPE, CREATE OPERATOR
    ,     CREATE PROCEDURE, CREATE SEQUENCE, CREATE SESSION
    ,     CREATE TABLE, CREATE TRIGGER, CREATE TYPE
    ,     CREATE VIEW TO student;

As always, I hope this helps those looking for how to do something that’s less than clear because everybody uses tools.

Written by maclochlainn

August 13th, 2019 at 1:39 pm

Fedora 30 Missing Library

without comments

Having run into an obsolete library issue installing Oracle Database 18c XE on Fedora, Version 30, I opted to revert my student image to Oracle Database 11g XE. The installation went without issue but when I tried to log into SQL*Plus as the oracle user, I got the following error message:

sqlplus: error while loading shared libraries: libnsl.so.1: cannot open shared object file: No such file or directory

The libnsl.so.1 library is no longer installed as part of the distribution for Fedora 28 forward but you can install it with the yum tool, like:

yum install -y libnsl

If you attempted to run the oracle-xe utility to configure the database prior to adding this library, it fails to provision the instance without a message. You won’t get the message until you manually try to connect as the sysdba privileged user. At that point, you’ll determine the instance wasn’t provisioned.

You can see that the installation failed when the oracle-xe utility fails to print the following lines to the console after the options are entered:

Starting Oracle Net Listener...Done
Configuring database...Done
Starting Oracle Database 11g Express Edition instance...Done
Installation completed successfully.

After installing the missing library, the oracle-xe utility works correctly. Alas, it looks like I’ll never bother to sort the Oracle Database 18c XE issues because after this version of the image we are moving the courses to a PostgreSQL database. PostgreSQL offers the smaller footprint that supports the core learning objectives of the courses.

As always, I hope this helps those looking for a solution.

Written by maclochlainn

August 11th, 2019 at 9:29 pm

Add user as sudoer

without comments

Somebody asked why adding a user to the wheel group in didn’t enable them as a sudoer, as qualified in my earlier Fedora post. The reason is that you also need to modify the primary group in the /etc/passwd file to specify the Group ID value for the wheel group as the primary group of the designated student user.

You can identify the Group ID with the following command:

cat /etc/group | grep wheel

It should return the following for the wheel group:

wheel:x:10:student

You need to check the target student user in the /etc/passwd file, which you can do with the following command:

cat /etc/passwd | grep student

It should return the following for the student user, which has a default group value equal to the user of the same name:

student:x:1000:1000:Student:/home/student:/bin/bash

As the root user, edit the /etc/passwd file to correct the student user’s primary group ID, as follows:

student:x:1000:10:Student:/home/student:/bin/bash

You should see the following two lines. If you want authorized sudoers to provide a password (recommended), then modify the first line by removing the # comment. If you don’t want authorized sudoers to provide a password, modify the second line by removing the # comment. Open the /etc/sudoers file with vi or gedit if you’d like a GUI editor.

# %wheel     ALL=(ALL)      ALL
# %wheel     ALL=(ALL)      NOPASSWD: ALL

Hope this helps. It’s a quick update for Fedora 30, you su to root and add your user to the sudoers list with the following syntax:

usermod someusername -a -G wheel

By the way, don’t forget to log off and then back on to the account.

Written by maclochlainn

June 12th, 2019 at 1:03 am

Posted in Fedora,Linux,Unix

Tagged with ,

Find files with errors

without comments

My students wanted a quick solution on how to find the log files that contain errors. That’s a simple line of code in Linux if you want any Oracle errors that start with ORA-:

find $HOME/lab2 -type f | xargs grep -i ora\-

It takes only a moment more to look for errors starting with ORA- or PLS-, like:

find $HOME/lab2 -type f | xargs grep -i -e ora\- -e pls\-

The latter might return something like this:

contact_lab.txt:ORA-00904: "MEMBER_LAB_ID": invalid identifier 
contact_lab.txt:ORA-00942: table or view does not exist 
contact_lab.txt:ORA-00942: table or view does not exist 
member_lab.txt:ORA-02264: name already used by an existing constraint 
member_lab.txt:ORA-00955: name is already used by an existing object

You can improve the error identification by identifying line numbers by adding -n option, like:

find $HOME/lab2 -type f | xargs grep -in -e ora\- -e pls\-

The latter might return something like this when there are two or more files:

contact_lab.txt:76:ORA-00904: "MEMBER_LAB_ID": invalid identifier 
contact_lab.txt:150:ORA-00942: table or view does not exist 
contact_lab.txt:157:ORA-00942: table or view does not exist 
member_lab.txt:75:ORA-02264: name already used by an existing constraint 
member_lab.txt:149:ORA-00955: name is already used by an existing object

Unfortunately, the command raises an error when there aren’t any files found of with a qualified extension. It also fails to prepend the file name when there’s only one qualified file name. As a result of these deficiencies, I’ve written the following Bash shell script. I’ve opted to call it the .findErrors.bashrc file name and deploy it in the user’s $HOME directory.

#!/bin/bash
 
  # Assign any file filter to the ext variable.
  ext=${1}
 
  # Assign the extension or simply use a wildcard for all files.
  if [ ! -z ${ext} ]; then
    ext="*.${ext}"
  else
    ext="*"
  fi
 
  # Assign the number of qualifying files to a variable.
  fileNum=$(ls -l ${ext} 2>/dev/null | grep -v ^l | wc -l)
 
  # Evaluate the number of qualifying files and process.
  if [ ${fileNum} -eq "0" ]; then
    echo "[0] files exist."
  elif [ ${fileNum} -eq "1" ]; then
    fileName=$(ls ${ext})
    find `pwd` -type f | grep -in ${ext} -e ora\- -e pls\- |
    while IFS='\n' read list; do
      echo "${fileName}:${list}"
    done
  else
    find `pwd` -type f | grep -in ${ext} -e ora\- -e pls\- |
    while IFS='\n' read list; do
      echo "${list}"
    done                                                                                                                                                   
  fi

You can modify the errors() function with or without a file extension to identify errors beginning with ORA- or PLS- in their log files. As always, I hope this helps those looking for a solution.

Written by maclochlainn

May 21st, 2019 at 8:04 pm

Python & Oracle 1

without comments

While Python is an interpreted language, Python is a very popular programming language. You may ask yourself why it is so popular? The consensus answers to why it’s so popular points to several factors. For example, Python is a robust high-level programming language that lets you:

  • Get complex things done quickly
  • Automate system and data integration tasks
  • Solve complex analytical problems

You find Python developers throughout the enterprise. Development, release engineering, IT operations, and support teams all use Python to solve problems. Business intelligence and data scientists use Python because it’s easy to use.

Developers don’t generally write end-user applications in Python. They mostly use Python as scripting language. Python provides a simple syntax that lets developers get complex things done quickly. Python also provides you with a vast set of libraries that let you can leverage to solve problems. Those libraries often simplify how you analyze complex data or automate repetitive tasks.

This article explains how to use the Python programming language with the Oracle database. It shows you how to install and use the cx_Oracle library to query data. Part 2 will cover how you insert, update, and delete data in the Oracle database, and how you call and use PL/SQL stored functions and procedures.

The article has two parts:

  • How you install and use cx_Oracle with the Oracle database
  • How you query data statically or dynamically

This audience for this article should know the basics of writing a Python program. If you’re completely new to Python, you may want to get a copy of Eric Matthes’ Python Crash Course: A Hands-On, Project-Based Introduction to Programming. More experienced developers with shell scripting backgrounds may prefer Al Sweigart’s Automate the Boring Stuff with Python.

This article uses Python 2.7, which appears to be the primary commercial version of Python in most organizations. At least, it’s what most vendors ship with Linux distros. It also happens to be the Python distro on Fedora Linux.

How you install and use cx_Oracle with the Oracle database

The first step requires that you test the current version of Python on your Operating System (OS). For the purpose of this paper, you use the student user account. The student user is in the sudoer list, which gives the account super user privileges.

You can find the Python version by opening a Terminal session and running the following command:

[student@localhost ~]$ python -V

It displays:

Python 2.7.5

You can download the current version of the cx_Oracle library at the Python Software Foundation’s web site. At the time of writing, the current version of the cx_Oracle is the cx_Oracle 5.2.1 version. The cx_Oracle library is available for download as a Red Hat Package Manager (RPM) module.

You download the cx_Oracle-5.2.1-11g-py26-1.x86_64.rpm to the /tmp directory or to a sudoer-enabled user’s downloads directory. Let’s assume you download the RPM into the /tmp directory. After you download the RPM, you can install it with the yum utility with this syntax:

yum install -y /tmp/cx_Oracle-5.2.1-11g-py27-1.x86_64.rpm

However, the most current version is now 7.0. You want the following file on Fedora 64-bit Linux, which can be found at the Python Package Index web site:

cx_Oracle-7.0.0-cp27-cp27mu-manylinux1_x86_64.whl

A wheel file requires that you use the pip utility (make sure to upgrade to the current version), like:

sudo pip install cx_Oracle-7.0.0-cp27-cp27mu*.whl

It should print the following to the console:

Processing ./cx_Oracle-7.0.0-cp27-cp27mu-manylinux1_x86_64.whl                                            
Installing collected packages: cx-Oracle                                                                  
Successfully installed cx-Oracle-7.0.0

The cx_Oracle library depends on the Oracle Client software, which may or may not be installed. It installs without a problem but would raise a runtime error when using the Python software. You can check whether cx_Oracle is installed with the following syntax:

rpm –qa oracle-instantclient11.2-basic

If the oracle-instantclient11.2-basic library isn’t installed, the command returns nothing. If the oracle-instantclient11.2-basic library is installed it returns the following:

oracle-instantclient11.2-basic-11.2.0.4.0-1.x86_64

Assuming you don’t have the Oracle Client software installed, you should download it from Oracle’s Instant Client Downloads web page. After you download the RPM, you install the Oracle 11g Release 2 Client software with the following syntax:

yum install -y /tmp/oracle-instantclient11.2-basic-11.2.0.4.0-1.x86_64.rpm

You now have the necessary software installed and configured to run and test Python programs that work with the Oracle database. Python uses a standard path configuration to look for Python modules or libraries. You can see that set of path values by connecting to the Python IDLE environment, which is the runtime environment. The IDLE environment is very much like the SQL*Plus environment.

You connect to the Python IDLE environment by typing the following:

python

It opens the Python IDLE environment. It should display the following:

Python 2.7.5 (default, Apr 10 2015, 08:09:05) 
[GCC 4.8.3 20140911 (Red Hat 4.8.3-7)] on linux2
Type "help", "copyright", "credits" or "license" for more information.

You import the sys library and then you can print the path elements with the following command:

>>> import sys
print sys.path

It should print the following for Python 2.7 in Fedora Linux:

['', '/usr/lib64/python27.zip', '/usr/lib64/python2.7', '/usr/lib64/python2.7/plat-linux2', '/usr/lib64/python2.7/lib-tk', '/usr/lib64/python2.7/lib-old', '/usr/lib64/python2.7/lib-dynload', '/usr/lib64/python2.7/site-packages', '/usr/lib64/python2.7/site-packages/gtk-2.0', '/usr/lib/python2.7/site-packages']

You can now test whether the environment works by typing the following commands in the IDLE environment:

>>> import cx_Oracle
db = cx_Oracle.connect("student/student@xe")
print db.version

It prints:

11.2.0.2.0

The other two sections require you to test components inside Python files. That means you need to supplement the default Python path variable. You do that by adding values to the Python environment variable, which is $PYTHONPATH.

The following adds the /home/student/Code/python directory to the Python path variable:

export set PYTHONPATH=/home/student/Code/python

Next, we create an connection.py file, which holds the following:

# Import the Oracle library.
import cx_Oracle
 
try:
  # Create a connection.
  db = cx_Oracle.connect("student/student@xe")
 
  # Print a message.
  print "Connected to the Oracle " + db.version + " database."
 
except cx_Oracle.DatabaseError, e:
  error, = e.args
  print >> sys.stderr, "Oracle-Error-Code:", error.code
  print >> sys.stderr, "Oracle-Error-Message:", error.message
 
finally
  # Close connection. 
  db.close()

The import statement adds the cx_Oracle library to the program scope. The cx_Oracle library’s connect function takes either the user name and password, or the user name, password, and TNS alias.

The except block differs from what you typically see. The code value maps to the SQLCODE value and the message value maps to the SQLERRM value.

You can test the connection.py file as follows in the /home/student/Code/python directory:

python connection.py

It prints the following:

Connected to the Oracle 11.2.0.2.0 database.

This section has shown you how to setup the cx_Oracle library, and how you can test the cx_Oracle library with Python programs.

How you query data statically or dynamically

The prior section shows you how to connect to an Oracle instance and how to verify the driver version of the cx_Oracle library. Like most ODBC and JDBC software, Python first creates a connection. Then, you need to create a cursor inside a connection.

The basicCursor.py program creates a connection and a cursor. The cursor holds a static SQL SELECT statement. The SELECT statement queries a string literal from the pseudo dual table.

# Import the Oracle library.
import sys
import cx_Oracle
 
try:
  # Create a connection.
  db = cx_Oracle.connect("student/student@xe")
 
  # Create a cursor.
  cursor = db.cursor()
 
  # Execute a query.
  cursor.execute("SELECT 'Hello world!' FROM dual")
 
  # Read the contents of the cursor.
  for row in cursor:
    print (row[0]) 
 
except cx_Oracle.DatabaseError, e:
  error, = e.args
  print >> sys.stderr, "Oracle-Error-Code:", error.code
  print >> sys.stderr, "Oracle-Error-Message:", error.message
 
finally:
  # Close cursor and connection. 
  cursor.close()
}  db.close()

The connect function assigns a database connection to the local db variable. The cursor function returns a cursor and assigns it to the local cursor variable. The execute function dispatches the query to Oracle’s SQL*Plus and returns the result set into a row element of the local cursor variable. The for-each loop reads the row element from the cursor variable and prints one row at a time. Since the cursor only returns a string literal, there’s only one row to return.

You test the program with this syntax:

python basicConnection.py

It prints:

Hello world!

The next basicTable.py program queries the item table. The item table holds a number of rows of data. The code returns each row inside a set of parentheses.

# Import the Oracle library.
import cx_Oracle
 
try:
  # Create a connection.
  db = cx_Oracle.connect("student/student@xe")
 
  # Create a cursor.
  cursor = db.cursor()
 
  # Execute a query.
  cursor.execute("SELECT item_title " +
                 ",      item_rating " +
                 "FROM   item " +
                 "WHERE  item_type = "
                 "        (SELECT common_lookup_id " +
                 "         FROM   common_lookup " +
                 "         WHERE  common_lookup_type = 'DVD_WIDE_SCREEN')")
 
  # Read the contents of the cursor.
  for row in cursor:
    print (row[0], row[1]) 
 
except cx_Oracle.DatabaseError, e:
  error, = e.args
  print >> sys.stderr, "Oracle-Error-Code:", error.code
  print >> sys.stderr, "Oracle-Error-Message:", error.message
 
finally:
  # Close cursor and connection. 
  cursor.close()
  db.close()

The SQL query is split across several lines by using the + operator. The + operator concatenates strings, and it lets you format a long query statement. The range for loop returns tuples from the cursor. The tuples are determined by the SELECT-list of the query.

The query returns the following type of results:

('Casino Royale', 'PG-13')
...
('Star Wars - Episode I', 'PG')
('Star Wars - Episode II', 'PG')
('Star Wars - Episode III', 'PG-13')
('Star Wars - Episode IV', 'PG')
('Star Wars - Episode V', 'PG')
('Star Wars - Episode VI', 'PG')

At this point, you know how to work with static queries. The next example shows you how to work with dynamic queries. The difference between a static and dynamic query is that an element of the string changes.

You have two options for creating dynamic strings. The first lets you glue a string inside a query. The second lets you embed one or more bind variables in a string. As a rule, you should use bind variables because they avoid SQL injection risks.

The following is the basicDynamicTable.py script

# Import the Oracle library.
import cx_Oracle
 
sRate = 'PG-13'
 
try:
  # Create a connection.
  db = cx_Oracle.connect("student/student@xe")
 
  # Define a dynamic statment.
  stmt = "SELECT item_title, item_rating FROM item WHERE item_rating = :rating"
 
  # Create a cursor.
  cursor = db.cursor()
 
  # Execute a statement with a bind variable.
  cursor.execute(stmt, rating = sRate)
 
  # Read the contents of the cursor.
  for row in cursor:
    print (row[0], row[1]) 
 
except cx_Oracle.DatabaseError, e:
  error, = e.args
  print >> sys.stderr, "Oracle-Error-Code:", error.code
  print >> sys.stderr, "Oracle-Error-Message:", error.message
 
finally:
  # Close cursor and connection. 
  cursor.close()
  db.close()

You need to assign a dynamic SQL statement to a local string variable. The bind variable is preceded with a colon (:). The execute function takes a string variable with the dynamic SQL statement. Then, you provide a name and value pair. The name needs to match the bind variable in the dynamic SQL statement. The value needs to map to a local Python variable.

The query should return a full list from the item table for the two item_title and item_rating columns:

('Casino Royale', 'PG-13')
...
('Harry Potter and the Goblet of Fire', 'PG-13')
('Harry Potter and the Order of the Phoenix', 'PG-13')
('The Lord of the Rings - Fellowship of the Ring', 'PG-13')
('The Lord of the Rings - Two Towers', 'PG-13')
('The Lord of the Rings - The Return of the King', 'PG-13')
('The Lord of the Rings - The Return of the King', 'PG-13')

This article should have shown you how to effectively work static and dynamic queries. You can find the scripts on the github.com server.

Written by maclochlainn

December 6th, 2018 at 11:40 pm

Lab Correction

without comments

Anyone using the August 2018 Fedora image should note that I neglected to put the right transaction_upload2.csv file in the /u01/app/oracle/upload directory. You can fix that by navigating to the Lab 12 Instructions web page and click on the zip file link to download the correct file. You will see the following dialog asking whether you want to open the file with the Ark utility, click OK to continue:

After clicking OK to open in Ark, you will see the following Ark dialog:

Click on the Home option in the Places dialog to the left, then click the Downloads option. You should see the following dialog before you click the Extract button.

Open a Konsole session and become the root superuser with the following command:

su - root

Change directory to Lab8_Final_CSV_Files directory where you extracted the transaction_upload2.csv file, like this:

cd /home/student/Downloads/Lab8_Final_CSV_Files

Copy the transaction_upload2.csv file to the /u01/app/oracle/upload directory with the following command:

cp /home/student/Downloads/Lab8_Final_CSV_Files/transaction_upload2.csv /u01/app/oracle/upload/.

Change directory to the /u01/app/oracle/upload directory and run the following long list (ll) command:

ll

You should see the following:

-rw-r--r--. 1 oracle dba      80 Aug 23 22:13 character.csv
drwxr-xr-x. 2 oracle dba    4096 Aug 23 20:44 preproc
drwxr-xr-x. 2 oracle dba    4096 Aug 23 23:35 textfile
-rw-r--r--. 1 oracle dba  128700 Dec  4 15:46 transaction_upload2.csv
-rw-r--r--. 1 oracle dba 1739520 Aug 23 22:04 transaction_upload.csv

The transaction_upload2.csv file contains a value of 3 for the created_by and last_updated_by user values. There shouldn’t be a value of 3 in the system_user_id column of the system_user table. The transaction_upload2.csv file should contain a value of 1002 for the created_by and last_updated_by user values.

You can modify the transaction_upload2.csv file once you’ve put it in the correct directory as the root user with the following command:

cat transaction_upload2.csv | sed -e 's/\,3\,/\,1002\,/g' > x; cp x transaction_upload2.csv; rm x

The new image will correct this problem.

Written by maclochlainn

December 4th, 2018 at 4:34 pm

Logging Triggers

without comments

Oracle Logging Trigger Results

This article demonstrates how you can write log files from triggers on different tables to the same logging table. This approach leverages Oracle’s object types and column substitutability features. It also eliminates the requirement to create a unique logging table for each logging trigger. The trick to accomplishing this requires mastering two skills.

The first skill requires you to learn how to create user-defined types (UDTs) and subtypes. The UDT stores the elements common to all logging data, and the UDT subtype stores the unique column values of individual tables. The second skill requires you to learn how to create a logging table that uses a base UDT as a column type, and to learn how to insert new data into and query subtype data from a UDT subtype.

You will learn both skills in this article. If you’re new to database triggers and Oracle’s object types, I’d recommend you check out my earlier “Critical and Non-critical Triggers” and “Object Types and Column Substitutability” articles.

The article works through the steps in four parts. You create:

  1. Five tables and sequences, and one UDT base type and two subtypes that map to the specific tables
  2. A reusable autonomous stored procedure
  3. Two data manipulation language (DML) triggers
  4. A test case with standalone PL/SQL blocks that query the data

This article uses small headers to organize the parts. After creating and testing the parts, there are some observations and suggestions at the end of the article.

Creating Tables, Sequences, and Types

You create five tables because of foreign key dependencies. The application_user table supports the use of who-audit columns. Who-audit columns document the user who creates and last updates every row of data. Who-audit columns must link to an access control list (ACL), which are typically a list of user names and their encrypted password keys.

The following creates the ACL table and sequence:

SQL> CREATE TABLE application_user
  2  ( application_user_id    NUMBER       CONSTRAINT app_user_pk PRIMARY KEY
  3  , application_user_name  VARCHAR2(30) CONSTRAINT app_user_nn1 NOT NULL
  4  , created_by             NUMBER       CONSTRAINT app_user_nn2 NOT NULL
  5  , creation_date          DATE         CONSTRAINT app_user_nn3 NOT NULL
  6  , last_updated_by        NUMBER       CONSTRAINT app_user_nn4 NOT NULL
  7  , last_update_date       DATE         CONSTRAINT app_user_nn5 NOT NULL
  8  , CONSTRAINT app_user_fk1 FOREIGN KEY(created_by)
  9    REFERENCES application_user(app_user_id)
 10  , CONSTRAINT app_user_fk2 FOREIGN KEY(last_updated_by)
 11    REFERENCES application_user(app_user_id));
SQL> CREATE SEQUENCE application_user_seq;

After you create the application_user table and application_user_seq sequence, you need to insert one row. The row let’s you validate the created_by and last_updated_by who-audit columns.

The following creates the mpaa table and mpaa_seq sequence:

SQL> CREATE TABLE mpaa
  2  ( mpaa_id           NUMBER         CONSTRAINT mpaa_pk  PRIMARY KEY
  3  , rating_code       VARCHAR2(5)    CONSTRAINT mpaa_nn1 NOT NULL
  4  , rating_name       VARCHAR2(30)   CONSTRAINT mpaa_nn2 NOT NULL
  5  , rating_desc       VARCHAR2(180)  CONSTRAINT mpaa_nn3 NOT NULL
  6  , created_by        NUMBER         CONSTRAINT mpaa_nn4 NOT NULL
  7  , creation_date     DATE           CONSTRAINT mpaa_nn5 NOT NULL
  8  , last_updated_by   NUMBER         CONSTRAINT mpaa_nn6 NOT NULL
  9  , last_update_date  DATE           CONSTRAINT mpaa_nn7 NOT NULL
 10  , CONSTRAINT mpaa_fk1 FOREIGN KEY(created_by)
 11    REFERENCES application_user(application_user_id)
 12  , CONSTRAINT mpaa_fk2 FOREIGN KEY(last_updated_by)
 13    REFERENCES application_user(application_user_id));
SQL> CREATE SEQUENCE mpaa_seq;

The mpaa table supports film ratings for the film table. The film table’s mpaa_id column holds foreign key values that reference the mpaa table. The film and employee tables are the principle testing tables for the stored procedure, triggers, and trigger event logging.

The following creates the film table and film_seq sequence:

SQL> CREATE TABLE film
  2  ( film_id           NUMBER        CONSTRAINT film_pk PRIMARY KEY
  3  , film_name         VARCHAR2(40)  CONSTRAINT film_nn1 NOT NULL
  4  , release_date      DATE          CONSTRAINT film_nn2 NOT NULL
  5  , mpaa_id           NUMBER        CONSTRAINT film_nn3 NOT NULL
  6  , created_by        NUMBER        CONSTRAINT film_nn4 NOT NULL
  7  , creation_date     DATE          CONSTRAINT film_nn5 NOT NULL
  8  , last_updated_by   NUMBER        CONSTRAINT film_nn6 NOT NULL
  9  , last_update_date  DATE          CONSTRAINT film_nn7 NOT NULL
 10  , CONSTRAINT film_fk1 FOREIGN KEY(created_by)
 11    REFERENCES application_user(application_user_id)
 12  , CONSTRAINT film_fk2 FOREIGN KEY(last_updated_by)
 13    REFERENCES application_user(application_user_id)
 14  , CONSTRAINT film_fk3 FOREIGN KEY (mpaa_id)
 15    REFERENCES mpaa (mpaa_id));
SQL> CREATE SEQUENCE film_seq;

The following creates the employee table and employee_seq sequence:

SQL> CREATE TABLE employee
  2  ( employee_id      NUMBER
  3  , employee_number  VARCHAR2(10)
  4  , first_name       VARCHAR2(20) CONSTRAINT employee_nn1 NOT NULL
  5  , middle_name      VARCHAR2(20)
  6  , last_name        VARCHAR2(20) CONSTRAINT employee_nn2 NOT NULL
  7  , created_by       NUMBER       CONSTRAINT employee_nn3 NOT NULL
  8  , creation_date    DATE         CONSTRAINT employee_nn5 NOT NULL
  9  , last_updated_by  NUMBER       CONSTRAINT employee_nn6 NOT NULL
 10  , last_update_date DATE         CONSTRAINT employee_nn7 NOT NULL
 11  , CONSTRAINT employee_pk  PRIMARY KEY (employee_id)
 12  , CONSTRAINT employee_fk1 FOREIGN KEY (created_by)
 13    REFERENCES application_user (application_user_id)
 14  , CONSTRAINT employee_fk2 FOREIGN KEY (last_updated_by)
 15    REFERENCES application_user (application_user_id));
SQL> DROP SEQUENCE employee_seq;

You should populate some data in the application_user, mpaa, film, and employee tables. This testing ensures the interdependencies work.

Before you create the trigger_log table, you need to create three UDTs. The base_t object type requires you create a base_t object type and implement a base_t object body.

The following creates the base_t object type:

SQL> CREATE OR REPLACE
  2    TYPE base_t IS OBJECT
  3    ( oname  VARCHAR2(30)
  4    , CONSTRUCTOR FUNCTION base_t
  5      RETURN SELF AS RESULT
  6    , MEMBER FUNCTION get_oname RETURN VARCHAR2
  7    , MEMBER PROCEDURE set_oname (oname VARCHAR2)
  8    , MEMBER FUNCTION to_string RETURN VARCHAR2)
  9    INSTANTIABLE NOT FINAL;
 10  /

The following creates the base_t object body:

SQL> CREATE OR REPLACE
  2    TYPE BODY base_t IS
  3    /* A default constructor w/o formal parameters. */
  4    CONSTRUCTOR FUNCTION base_t
  5    RETURN SELF AS RESULT IS
  6      BEGIN
  7        self.oname := 'BASE_T';
  8        RETURN;
  9      END;
 10    /* An accessor, or getter, method. */
 11    MEMBER FUNCTION get_oname RETURN VARCHAR2 IS
 12      BEGIN
 13        RETURN self.oname;
 14      END get_oname;
 15    /* A mutator, or setter, method. */
 16    MEMBER PROCEDURE set_oname
 17    ( oname  VARCHAR2 ) IS
 18      BEGIN
 19        self.oname := oname;
 20      END set_oname;
 21    /* A to_string conversion method. */
 22    MEMBER FUNCTION to_string RETURN VARCHAR2 IS
 23      BEGIN
 24        RETURN self.oname;
 25      END to_string;
 26  END;
 27  /

Lines 4 through 9 implements a no-argument constructor that automatically assigns a literal value to the oname field on line 7. This type of constructor lets you create an instance of the base_t object type without providing an oname. Lines 11 through 14 implements a getter for the oname field, and lines 16 through 20 implements a setter for the oname field.

Lines 22 through 26 implements a to_string function that prints the oname field value. The to_string function also provides a convenient way to test the object type of object instance stored in tables, as you will see later in this article.

You can now create the UDT subtypes for the employee and film tables. The base_t name represents the base type or a super type. The subtypes for the employee and film tables use the more conventional _obj suffix.

The following creates the employee_obj UDT subtype:

SQL> CREATE OR REPLACE
  2    TYPE employee_obj UNDER base_t
  3    ( employee_id      NUMBER
  4    , employee_number  VARCHAR2(10)
  5    , first_name       VARCHAR2(20)
  6    , middle_name      VARCHAR2(20)
  7    , last_name        VARCHAR2(20)
  8    , created_by        NUMBER
  9    , creation_date     DATE
 10    , last_updated_by   NUMBER
 11    , last_update_date  DATE);
 12  /

The following creates the film_obj UDT subtype:

SQL> CREATE OR REPLACE
  2    TYPE film_obj UNDER base_t
  3    ( film_id           NUMBER
  4    , film_name         VARCHAR2(40)
  5    , release_date      DATE
  6    , mpaa_id           NUMBER
  7    , created_by        NUMBER
  8    , creation_date     DATE
  9    , last_updated_by   NUMBER
 10    , last_update_date  DATE);
 11    /

After creating the base_t UDT and the employee_obj and film_obj subtypes, you can create the trigger_log table. The following creates the trigger_log table and trigger_log_s sequence:

SQL> CREATE TABLE trigger_log
  2  ( trigger_log_id      NUMBER
  3  , table_name          VARCHAR2(30)
  4  , trigger_event       VARCHAR2(6)
  5  , transaction_status  VARCHAR2(9)
  6  , old_instance        BASE_T
  7  , new_instance        BASE_T );

The surrogate key for the table is the trigger_log_id column. The composite key of the table_name, trigger_event, and transaction_status columns define the natural key for table. The old_instance and new_instance columns hold respectively the values for any table before and after the DML event.

Autonomous Procedure

You have a 32,000-byte limit on the size of database triggers. Also, you have a limit on the scope of database triggers. A database trigger must run in the same context as the DML event, which means a trigger can’t write a log file when it raises an exception. You can write a log file when the trigger raises an exception by calling a procedure that runs as an anonymous transaction.

The following implements anonymous-transaction procedure:

SQL> CREATE OR REPLACE
  2    PROCEDURE log_trigger_result
  3    ( pv_table_name          VARCHAR2
  4    , pv_trigger_event       VARCHAR2
  5    , pv_transaction_status  VARCHAR2
  6    , pv_old_instance        BASE_T
  7    , pv_new_instance        BASE_T ) IS
  8
  9    /* Set precompiler directive to run in a separate context. */
 10    PRAGMA AUTONOMOUS_TRANSACTION;
 11  BEGIN
 12    /* Write to the log table. */
 13    INSERT INTO trigger_log
 14    ( trigger_log_id
 15    , table_name
 16    , trigger_event
 17    , transaction_status
 18    , old_instance
 19    , new_instance )
 20    VALUES
 21    ( trigger_log_s.NEXTVAL
 22    , pv_table_name
 23    , pv_trigger_event
 24    , pv_transaction_status
 25    , pv_old_instance
 26    , pv_new_instance );
 27
 28    /* Commit the autonmous transaction. */
 29    COMMIT;
 30  END log_trigger_result;
 31  /

Lines 6 and 7 uses a base_t UDT as a parameter type, which means it accepts a base_t type or any subtype. Line 10 set a pre-compiler directive that enables the log_trigger_result procedure to run in an independent thread of execution.

Autonomous Procedure

The INSERT statement designates two base_t columns on lines 18 and 19, and then it passes the two base_t parameters in the VALUES clause. Line 29 commits the record into the trigger_log table.

The following implements an INSERT or UPDATE event trigger on the employee table:

SQL> CREATE OR REPLACE TRIGGER employee_t1
  2    BEFORE INSERT OR UPDATE OF last_name ON employee
  3    FOR EACH ROW
  4    WHEN (REGEXP_LIKE(NEW.last_name,' '))
  5  DECLARE
  6    /* DML event label. */
  7    lv_employee_event      VARCHAR2(6);
  8    lv_transaction_status  VARCHAR2(9) := 'REJECTED';
  9
 10    /* Declare exception. */
 11    e EXCEPTION;
 12    PRAGMA EXCEPTION_INIT(e,-20001);
 13  BEGIN
 14    /* Check for an event and assign event value. */
 15    IF INSERTING THEN
 16      /* Check for a empty image_id primary key column value,
 17         and assign the next sequence value when it is missing. */
 18      IF :NEW.employee_id IS NULL THEN
 19        SELECT employee_seq.NEXTVAL
 20        INTO   :NEW.employee_id
 21        FROM   dual;
 22      END IF;
 23      :NEW.last_name := REGEXP_REPLACE(:NEW.last_name,' ','-',1,1);
 24      lv_transaction_status := 'PROCESSED';
 25      lv_employee_event := 'INSERT';
 26    ELSE
 27      lv_employee_event := 'UPDATE';
 28    END IF;
 29
 30    /* Log the details captured by an insert or update. */
 31    log_trigger_result
 32    ( pv_table_name => 'EMPLOYEE'
 33    , pv_trigger_event => lv_employee_event
 34    , pv_transaction_status => lv_transaction_status
 35    , pv_new_instance =>
 36        employee_obj(
 37          oname => 'EMPLOYEE_OBJ'
 38        , employee_id => :old.employee_id
 39        , employee_number => :old.employee_number
 40        , first_name => :old.first_name
 41        , middle_name => :old.middle_name
 42        , last_name => :old.last_name
 43        , created_by => :old.created_by
 44        , creation_date => :old.creation_date
 45        , last_updated_by => :old.last_updated_by
 46        , last_update_date => :old.last_update_date )
 47    , pv_old_instance =>
 48        employee_obj(
 49          oname => 'EMPLOYEE_OBJ'
 50        , employee_id => :NEW.employee_id
 51        , employee_number => :NEW.employee_number
 52        , first_name => :NEW.first_name
 53        , middle_name => :NEW.middle_name
 54        , last_name => :NEW.last_name
 55        , created_by => :NEW.created_by
 56        , creation_date => :NEW.creation_date
 57        , last_updated_by => :NEW.last_updated_by
 58        , last_update_date => :NEW.last_update_date ));
 59
 60    /* Throw exception. */
 61    IF UPDATING THEN
 62      RAISE_APPLICATION_ERROR(-20001
 63        ,'No two-part last names without a hyphen.');
 64    END IF;
 65
 66  EXCEPTION
 67    /* Capture an exception. */
 68    WHEN e THEN
 69      ROLLBACK;
 70      dbms_output.put_line('[Trigger Event: '||lv_employee_event||']');
 71      dbms_output.put_line(SQLERRM);
 72    WHEN OTHERS THEN
 73      dbms_output.put_line(SQLERRM);
 74  END;
 75  /

Line 8 sets the lv_transaction_status to REJECTED by default. Line 15 checks for an INSERT statement as the triggering event. It sets the lv_transaction_status to PROCESSED and sets the lv_employee_event to INSERT on lines 24 and 25. An UPDATE statement sets the lv_employee_event variable to UPDATE on line 27.

Lines 35 through 46 create an instance of the employee_obj as the old part of the INSERT statement. It should always be a null value for an INSERT statement. Lines 47 through 58 create an instance of the employee_obj as the new part of the INSERT statement.

The following implements an INSERT or UPDATE event trigger on the film table:

SQL> CREATE OR REPLACE TRIGGER film_t1
  2    BEFORE INSERT OR UPDATE OF film_name ON film
  3    FOR EACH ROW
  4  DECLARE
  5    /* DML event label. */
  6    lv_trigger_event  VARCHAR2(6);
  7    lv_transaction_status  VARCHAR2(9) := 'REJECTED';
  8
  9    /* Declare exception. */
 10    e EXCEPTION;
 11    PRAGMA EXCEPTION_INIT(e,-20001);
 12  BEGIN
 13    /* Check for an event and assign event value. */
 14    IF INSERTING THEN
 15      /* Check for a empty image_id primary key column value,
 16         and assign the next sequence value when it is missing. */
 17      IF :NEW.film_id IS NULL THEN
 18        SELECT film_seq.NEXTVAL
 19        INTO   :NEW.film_id
 20        FROM   dual;
 21      END IF;
 22      lv_trigger_event := 'INSERT';
 23      lv_transaction_status := 'PROCESSED';
 24    ELSIF UPDATING THEN
 25      lv_trigger_event := 'UPDATE';
 26    END IF;
 27
 28    /* Log the details captured by an insert or update. */
 29    log_trigger_result
 30    ( pv_table_name => 'FILM'
 31    , pv_trigger_event => lv_trigger_event
 32    , pv_transaction_status => lv_transaction_status
 33    , pv_new_instance =>
 34        film_obj(
 35          oname => 'FILM_OBJ'
 36        , film_id => :old.film_id
 37        , film_name => :old.film_name
 38        , release_date => :old.release_date
 39        , mpaa_id => :old.mpaa_id
 40        , created_by => :old.created_by
 41        , creation_date => :old.creation_date
 42        , last_updated_by => :old.last_updated_by
 43        , last_update_date => :old.last_update_date )
 44    , pv_old_instance =>
 45        film_obj(
 46          oname => 'FILM_OBJ'
 47        , film_id => :NEW.film_id
 48        , film_name => :NEW.film_name
 49        , release_date => :NEW.release_date
 50        , mpaa_id => :NEW.mpaa_id
 51        , created_by => :NEW.created_by
 52        , creation_date => :NEW.creation_date
 53        , last_updated_by => :NEW.last_updated_by
 54        , last_update_date => :NEW.last_update_date ));
 55
 56    /* Throw exception. */
 57    IF UPDATING THEN
 58      RAISE_APPLICATION_ERROR(-20001,'Film names not updateable.');
 59    END IF;
 60
 61  EXCEPTION
 62    /* Capture an exception. */
 63    WHEN e THEN
 64      ROLLBACK;
 65      dbms_output.put_line('[Trigger Event: '||lv_trigger_event||']');
 66      dbms_output.put_line(SQLERRM);
 67    WHEN OTHERS THEN
 68      dbms_output.put_line(SQLERRM);
 69  END;
 70  /

The film_t1 trigger does much the same thing as the employee_t1 trigger. The difference occurs in the INSERT statement. The film_t1 trigger constructs an old and new film_obj instances to the autonomous procedure.

You use INSERT and UPDATE statements as test cases for the complete model. The INSERT statement would look like the following:

SQL> INSERT INTO employee
  2  ( employee_id
  3  , employee_number
  4  , first_name
  5  , last_name
  6  , created_by
  7  , creation_date
  8  , last_updated_by
  9  , last_update_date )
 10  VALUES
 11  ( employee_seq.NEXTVAL
 12  ,'B98765-678'
 13  ,'Catherine'
 14  ,'Zeta Jones'
 15  , 1
 16  , TRUNC(SYSDATE)
 17  , 1
 18  , TRUNC(SYSDATE));

The INSERT statements should complete without error, but the UPDATE statement should raise an error. You can use the following UPDATE statement:

SQL> UPDATE employee
  2  SET employee_number = 'B98765-678'
  3  ,   first_name = 'Catherine'
  4  ,   last_name = 'Zeta Jones'
  5  ,   created_by = 1
  6  ,   creation_date = TRUNC(SYSDATE)
  7  ,   last_updated_by = 1
  8  ,   last_update_date = TRUNC(SYSDATE)
  9  WHERE first_name = 'Catherine'
 10  AND   middle_name IS NULL
 11  AND   last_name = 'Zeta-Jones';

It throws the following exception:

UPDATE employee
       *
ERROR at line 1:
ORA-04092: cannot ROLLBACK in a trigger
ORA-06512: at "STUDENT.EMPLOYEE_T1", line 64
ORA-20001: No two-part last names without a hyphen.
ORA-04088: error during execution of trigger 'STUDENT.EMPLOYEE_T1'

An INSERT statement lets you test the film table, and UPDATE statement lets you test the film table. The film_t allows the INSERT statement but raises an exception with an UPDATE statement.

Query Data with Standalone PL/SQL

You have the ability to query the results with PL/SQL. The following anonymous block lets you print the results of the trigger architecture for employee_obj records.

SQL> DECLARE
  2    /* Declare a cursor with subcursors. */
  3    CURSOR c IS
  4      SELECT   trigger_log_id
  5      ,        table_name
  6      ,        trigger_event
  7      ,        transaction_status
  8      ,        TREAT(new_instance AS employee_obj)
  9      ,        TREAT(old_instance AS employee_obj)
 10      FROM     trigger_log
 11      WHERE    table_name = 'EMPLOYEE';
 12
 13    /* Declare scalar variables. */
 14    lv_trigger_log_id      NUMBER;
 15    lv_table_name          VARCHAR2(30);
 16    lv_trigger_event       VARCHAR2(6);
 17    lv_transaction_status  VARCHAR2(9);
 18
 19    /* Declare UDT variables. */
 20    lv_old_record  EMPLOYEE_OBJ;
 21    lv_new_record  EMPLOYEE_OBJ;
 22
 23  BEGIN
 24    /* Open base cursor and fetch records until none are found. */
 25    OPEN c;
 26    LOOP
 27      FETCH c
 28      INTO lv_trigger_log_id
 29      ,    lv_table_name
 30      ,    lv_trigger_event
 31      ,    lv_transaction_status
 32      ,    lv_old_record
 33      ,    lv_new_record;
 34      EXIT WHEN c%NOTFOUND;
 35
 36      dbms_output.put_line('========================================');
 37      dbms_output.put_line('Trigger_Log_ID  [Row] : '
 38      || lv_trigger_log_id);
 39      dbms_output.put_line('Table_Name      [Row] : '
 40      || lv_table_name);
 41      dbms_output.put_line('Table_Name      [Row] : '
 42      || lv_trigger_event);
 43      dbms_output.put_line('Transaction     [Row] : '
 44      || lv_transaction_status);
 45      dbms_output.put_line(
 46        '----------------------------------------');
 47      dbms_output.put_line('OName           [Old] : '
 48      || lv_old_record.oname);
 49      dbms_output.put_line('Employee_ID     [Old] : '
 50      || lv_old_record.employee_id);
 51      dbms_output.put_line('Employee_Number [Old] : '
 52      || lv_old_record.employee_number);
 53      dbms_output.put_line('First_Name      [Old] : '
 54      || lv_old_record.first_name);
 55      dbms_output.put_line('Middle_Name     [Old] : '
 56      || lv_old_record.middle_name);
 57      dbms_output.put_line('Last_Name       [Old] : '
 58      || lv_old_record.last_name);
 59      dbms_output.put_line(
 60        '----------------------------------------');
 61      dbms_output.put_line('OName           [New] : '
 62      || lv_new_record.oname);
 63      dbms_output.put_line('Employee_ID     [New] : '
 64      || lv_new_record.employee_id);
 65      dbms_output.put_line('Employee_Number [New] : '
 66      || lv_new_record.employee_number);
 67      dbms_output.put_line('First_Name      [New] : '
 68      || lv_new_record.first_name);
 69      dbms_output.put_line('Middle_Name     [New] : '
 70      || lv_new_record.middle_name);
 71      dbms_output.put_line('Last_Name       [New] : '
 72      || lv_new_record.last_name);
 73    END LOOP;
 74    CLOSE c;
 75
 76    /* Print the close the set. */
 77    dbms_output.put_line(
 78      '========================================');
 79  END;
 80  /

The cursor on lines 4 through 11 includes a key trick for reading the object types on lines 8 and 9. The TREAT function instructs the query to instantiate the base_t column as an employee_obj subtype.

You access the object instance on lines 47 through 58 by referring to the lv_new_record variable. You access the individual field element with a dot notation. The same approach lets you access the lv_old_record variable’s contents.

It generates the following output from the employee table:

========================================
Trigger_Log_ID  [ROW] : 1
Table_Name      [ROW] : EMPLOYEE
Table_Name      [ROW] : INSERT
TRANSACTION     [ROW] : PROCESSED
----------------------------------------
OName           [Old] : EMPLOYEE_OBJ
Employee_ID     [Old] :
Employee_Number [Old] :
First_Name      [Old] :
Middle_Name     [Old] :
Last_Name       [Old] :
----------------------------------------
OName           [NEW] : EMPLOYEE_OBJ
Employee_ID     [NEW] : 1
Employee_Number [NEW] : B98765-678
First_Name      [NEW] : Catherine
Middle_Name     [NEW] :
Last_Name       [NEW] : Zeta-Jones
========================================
Trigger_Log_ID  [ROW] : 2
Table_Name      [ROW] : EMPLOYEE
Table_Name      [ROW] : UPDATE
TRANSACTION     [ROW] : REJECTED
----------------------------------------
OName           [Old] : EMPLOYEE_OBJ
Employee_ID     [Old] : 1
Employee_Number [Old] : B98765-678
First_Name      [Old] : Catherine
Middle_Name     [Old] :
Last_Name       [Old] : Zeta-Jones
----------------------------------------
OName           [NEW] : EMPLOYEE_OBJ
Employee_ID     [NEW] : 1
Employee_Number [NEW] : B98765-678
First_Name      [NEW] : Catherine
Middle_Name     [NEW] :
Last_Name       [NEW] : Zeta Jones
========================================

This article has shown you how to create a framework for the writing trigger results from multiple tables into a single logging table. It’s also shown you how to leverage column substitutability with the base_t type column.

While this example has shown you to query with an anonymous block, you should really use an object table function. You would develop one object table function for each different type of output.

Written by maclochlainn

November 25th, 2018 at 6:26 pm