CHAPTER 11 Records
Suppose that I have defined a table to keep track of my
favorite books:
CREATE TABLE books (
book_id INTEGER,
isbn VARCHAR2(13),
title VARCHAR2(200),
summary VARCHAR2(2000),
author VARCHAR2(200),
date_published DATE,
page_count NUMBER
);
I can then easily create a record based on this table, populate it with a query from the
database, and then access the individual columns through the record’s fields:
DECLARE
my_book books%ROWTYPE;
BEGIN
SELECT *
INTO my_book
FROM books
WHERE title = 'Oracle PL/SQL Programming, 6th Edition';
IF my_book.author LIKE '%Feuerstein%'
THEN
DBMS_OUTPUT.put_line ('Our newest ISBN is ' || my_book.isbn);
END IF;
END;
I can also define my own record type and use that as the basis for declaring records.
Suppose, for example, that I want to work only with the author and title of a book. Rather than use %ROWTYPE to declare my record, I will instead create a record type:
DECLARE
TYPE author_title_rt IS RECORD (
author books.author%TYPE
,title books.title%TYPE
);
l_book_info author_title_rt;
BEGIN
SELECT author, title INTO l_book_info
FROM books WHERE isbn = '978-1-449-32445-2';
Declaring Records
- Table-based record
DECLARE
one_book books%ROWTYPE;
- Cursor-based record
DECLARE
CURSOR my_books_cur IS
SELECT * FROM books
WHERE author LIKE '%FEUERSTEIN%';
one_SF_book my_books_cur%ROWTYPE;
DECLARE
TYPE book_info_rt IS RECORD (
author books.author%TYPE,
category VARCHAR2(100),
total_page_count POSITIVE);
steven_as_author book_info_rt;
Here is an example of the creation of a record based on a cursor variable:
DECLARE
TYPE book_rc IS REF CURSOR RETURN books%ROWTYPE;
book_cv book_rc;
one_book book_cv%ROWTYPE;
BEGIN
...
The other way to declare and use a record is to do so implicitly, with a cursor FOR loop.
In the following block, the book_rec record is not defined in the declaration section;
PL/SQL automatically declares it for me with the %ROWTYPE attribute against the
loop’s query:
BEGIN
FOR book_rec IN (SELECT * FROM books)
LOOP
calculate_total_sales (book_rec);
END LOOP;
END;
- Programmer-Defined Records
- Declaring programmer-defined record TYPEs
The general syntax of the record TYPE definition is:
TYPE type_name IS RECORD
(field_name1 datatype1 [[NOT NULL]:=|DEFAULT default_value],
field_name2 datatype2 [[NOT NULL]:=|DEFAULT default_value],
...
field_nameN datatypeN [[NOT NULL]:=|DEFAULT default_value]
);
Here is an example of a record TYPE statement:
TYPE company_rectype IS RECORD (
comp# company.company_id%TYPE
, list_of_names DBMS_SQL.VARCHAR2S
, dataset SYS_REFCURSOR
);
To build a customer sales record, for example, I first define a record type called customer_sales_rectype, as follows:
PACKAGE customer_sales_pkg
IS
TYPE customer_sales_rectype IS RECORD
(customer_id customer.customer_id%TYPE,
customer_name customer.name%TYPE,
total_sales NUMBER (15,2)
);
I can then use this new record type to declare records with the same structure as this type:
DECLARE
prev_customer_sales_rec customer_sales_pkg.customer_sales_rectype;
top_customer_rec customer_sales_pkg.customer_sales_rectype;
Notice that I do not need the %ROWTYPE attribute, or any other kind of keyword, to
denote this as a record declaration. The %ROWTYPE attribute is needed only for table
and cursor records.
Working with Records
- Record-level operations
Suppose that I have created the following table:
CREATE TABLE cust_sales_roundup (
customer_id NUMBER (5),
customer_name VARCHAR2 (100),
total_sales NUMBER (15,2)
)
Then the three records defined as follows all have compatible structures, and I can “mix
and match” the data in these records as shown:
DECLARE
cust_sales_roundup_rec cust_sales_roundup%ROWTYPE;
CURSOR cust_sales_cur IS SELECT * FROM cust_sales_roundup;
cust_sales_rec cust_sales_cur%ROWTYPE;
TYPE customer_sales_rectype IS RECORD
(customer_id NUMBER(5),
customer_name customer.name%TYPE,
total_sales NUMBER(15,2)
);
preferred_cust_rec customer_sales_rectype;
BEGIN
-- Assign one record to another.
cust_sales_roundup_rec := cust_sales_rec;
preferred_cust_rec := cust_sales_rec;
END;
In this example, I’ll assign a default value to a record. You can initialize a record at
the time of declaration by assigning it another compatible record. In the following
program, I assign an IN argument record to a local variable. I might do this so that
I can modify the values of fields in the record:
PROCEDURE compare_companies
(prev_company_rec IN company%ROWTYPE)
IS
curr_company_rec company%ROWTYPE := prev_company_rec;
BEGIN
...
END;
In this next initialization example, I create a new record type and record. I then
create a second record type using the first record type as its single column. Finally,
I initialize this new record with the previously defined record:
DECLARE
TYPE first_rectype IS RECORD (var1 VARCHAR2(100) := 'WHY NOT');
first_rec first_rectype;
TYPE second_rectype IS RECORD (nested_rec first_rectype := first_rec);
BEGIN
...
END;
I can also perform assignments within the execution section, as you might expect.
In the following example I declare two different rain_forest_history records and
then set the current history information to the previous history record:
DECLARE
prev_rain_forest_rec rain_forest_history%ROWTYPE;
curr_rain_forest_rec rain_forest_history%ROWTYPE;
BEGIN
... initialize previous year rain forest data ...
-- Transfer data from previous to current records.
curr_rain_forest_rec := prev_rain_forest_rec;
I can move data directly from a row in a table to a record in a program by fetching
directly into a record. Here are two examples:
DECLARE
/*
|| Declare a cursor and then define a record based on that cursor
|| with the %ROWTYPE attribute.
*/
CURSOR cust_sales_cur IS
SELECT customer_id, customer_name, SUM (total_sales) tot_sales
FROM cust_sales_roundup
WHERE sold_on < ADD_MONTHS (SYSDATE, −3)
GROUP BY customer_id, customer_name;
cust_sales_rec cust_sales_cur%ROWTYPE;
BEGIN
/* Move values directly into record by fetching from cursor */
OPEN cust_sales_cur;
FETCH cust_sales_cur INTO cust_sales_rec;
CLOSE cust_sales_cur;
In this next block, I declare a programmer-defined TYPE that matches the data
retrieved by the implicit cursor. Then I SELECT directly into a record based on that
type:
DECLARE
TYPE customer_sales_rectype IS RECORD
(customer_id customer.customer_id%TYPE,
customer_name customer.name%TYPE,
total_sales NUMBER (15,2)
);
top_customer_rec customer_sales_rectype;
BEGIN
/* Move values directly into the record: */
SELECT customer_id, customer_name, SUM (total_sales)
INTO top_customer_rec
FROM cust_sales_roundup
WHERE sold_on < ADD_MONTHS (SYSDATE, −3)
GROUP BY customer_id, customer_name;
I can set all fields of a record to NULL with a direct assignment:
/* File on web: record_assign_null.sql */
FUNCTION dept_for_name (
department_name_in IN departments.department_name%TYPE
)
RETURN departments%ROWTYPE
IS
l_return departments%ROWTYPE;
FUNCTION is_secret_department (
department_name_in IN departments.department_name%TYPE
)
RETURN BOOLEAN
IS
BEGIN
RETURN CASE department_name_in
WHEN 'VICE PRESIDENT' THEN TRUE
ELSE FALSE
END;
END is_secret_department;
BEGIN
SELECT *
INTO l_return
FROM departments
WHERE department_name = department_name_in;
IF is_secret_department (department_name_in)
THEN
l_return := NULL;
END IF;
RETURN l_return;
END dept_for_name;
- Field-level operations
When you need to access a field within a record (to either read or change its value), you
must use dot notation, just as you would when identifying a column from a specific
database table. The syntax for such a reference is:
[[schema_name.]package_name.]record_name.field_name
I create a record based on the rain_forest_history table, populate it with values, and then insert a record into that same table:
DECLARE
rain_forest_rec rain_forest_history%ROWTYPE;
BEGIN
/* Set values for the record */
rain_forest_rec.country_code := 1005;
rain_forest_rec.analysis_date := ADD_MONTHS (TRUNC (SYSDATE), −3);
rain_forest_rec.size_in_acres := 32;
rain_forest_rec.species_lost := 425;
/* Insert a row in the table using the record values */
INSERT INTO rain_forest_history
(country_code, analysis_date, size_in_acres, species_lost)
VALUES
(rain_forest_rec.country_code,
rain_forest_rec.analysis_date,
rain_forest_rec.size_in_acres,
rain_forest_rec.species_lost);
...
END;
Starting with Oracle9i Database Release 2, you can also perform a record-level insert,
simplifying the preceding INSERT statement into nothing more than this:
INSERT INTO rain_forest_history VALUES rain_forest_rec;
- Field-level operations with nested records
In the following example I declare a record TYPE for all the elements of a telephone number (phone_rectype), and then declare a record TYPE that collects all the phone numbers for a person together in a single structure (contact_set_rectype):
DECLARE
TYPE phone_rectype IS RECORD
(intl_prefix VARCHAR2(2),
area_code VARCHAR2(3),
exchange VARCHAR2(3),
phn_number VARCHAR2(4),
extension VARCHAR2(4)
);
-- Each field is a nested record...
TYPE contact_set_rectype IS RECORD
(day_phone# phone_rectype,
eve_phone# phone_rectype,
fax_phone# phone_rectype,
home_phone# phone_rectype,
cell_phone# phone_rectype
);
auth_rep_info_rec contact_set_rectype;
BEGIN
in the following assignment, sets the fax phone number’s area code to the home phone number’s area code:
auth_rep_info_rec.fax_phone#.area_code := auth_rep_info_rec.home_phone#.area_code;
- Field-level operations with package-based records
Finally, here is an example demonstrating references to packaged records (and packagebased record TYPEs). Suppose that I want to plan out my summer reading (for all those days I will be lounging about in the sand outside my Caribbean hideaway). I create a package specification as follows:
CREATE OR REPLACE PACKAGE summer
IS
TYPE reading_list_rt IS RECORD (
favorite_author VARCHAR2 (100),
title VARCHAR2 (100),
finish_by DATE);
must_read reading_list_rt;
wifes_favorite reading_list_rt;
END summer;
CREATE OR REPLACE PACKAGE BODY summer
IS
BEGIN -- Initialization section of package
must_read.favorite_author := 'Tepper, Sheri S.';
must_read.title := 'Gate to Women''s Country';
END summer;
With this package compiled in the database, I can then construct my reading list as
follows:
DECLARE
first_book summer.reading_list_rt;
second_book summer.reading_list_rt;
BEGIN
summer.must_read.finish_by := TO_DATE ('01-AUG-2009', 'DD-MON-YYYY');
first_book := summer.must_read;
second_book.favorite_author := 'Hobb, Robin';
second_book.title := 'Assassin''s Apprentice';
second_book.finish_by := TO_DATE ('01-SEP-2009', 'DD-MON-YYYY');
END;
I declare two local book records. I then assign a “finish by” date to the packaged mustread book (notice the package.record.field syntax) and assign that packaged record to my first book of the summer record. I then assign values to individual fields for the second book of the summer.
Comparing Records
To test for record equality, you must write code that compares each field individually. If a record doesn’t have many fields, this isn’t too cumbersome. For the reading list record, you would write something like this:
DECLARE
first_book summer.reading_list_rt := summer.must_read;
second_book summer.reading_list_rt := summer.wifes_favorite;
BEGIN
IF first_book.favorite_author = second_book.favorite_author
AND first_book.title = second_book.title
AND first_book.finish_by = second_book.finish_by
THEN
lots_to_talk_about;
END IF;
END;
There is one complication to keep in mind. If your requirements indicate that two NULL
records are equal (equally NULL), you will have to modify each comparison to something like this:
(first_book.favorite_author = second_book.favorite_author
OR( first_book.favorite_author IS NULL AND
second_book.favorite_author IS NULL))
Trigger Pseudorecords
When you are writing code inside database triggers for a particular table, the database
makes available to you two structures, OLD and NEW, which are pseudorecords. These
structures have the same format as table-based records declared with %ROWTYPE—
a field for every column in the table:
OLD
This pseudorecord shows the values of each column in the table before the current
transaction started.
NEW
This pseudorecord reveals the new values of each column about to be placed in the
table when the current transaction completes.
When you reference OLD and NEW within the body of the trigger, you must preface
those identifiers with a colon; within the WHEN clause, however, do not use the colon.
Here is an example:
TRIGGER check_raise AFTER UPDATE OF salary ON employee
FOR EACH ROW
WHEN (OLD.salary != NEW.salary) OR
(OLD.salary IS NULL AND NEW.salary IS NOT NULL) OR
(OLD.salary IS NOT NULL AND NEW.salary IS NULL)
BEGIN
IF :NEW.salary > 100000 THEN ...
%ROWTYPE and invisible columns (Oracle Database 12c)
As of 12.1, you can now define invisible columns in relational tables. An invisible column
is a user-defined hidden column, which means that if you want to display or assign a
value to an invisible column, you must specify its name explicitly. Here is an example
of defining an invisible column in a table:
CREATE TABLE my_table (i INTEGER, d DATE, t TIMESTAMP INVISIBLE)
You can make an invisible column visible with an ALTER TABLE statement, as in:
ALTER TABLE my_table MODIFY t VISIBLE
The SELECT * syntax will not display an INVISIBLE column. However, if you include
an INVISIBLE column in the select list of a SELECT statement, then the column will
be displayed. You cannot implicitly specify a value for an INVISIBLE column in the
VALUES clause of an INSERT statement. You must specify the INVISIBLE column in
the column list. You must explicitly specify an INVISIBLE column in %ROWTYPE
attributes.
If, however, I make that column visible, Oracle will then create a field for it in a
%ROWTYPE-declared record. This also means that after you make an invisible column visible, Oracle will change the status of all program units that declare records using
%ROWTYPE against that column’s table to INVALID.