With this column cursor, I extract the name, datatype, and length information for each column in the table.. • To display the output of a table in a readable fashion, I need to provide a
Trang 1AND table_name = table_in;
Using Dot Notation
Notice that my cursor takes two parameters, owner_in and table_in, but that my program itself only accepts a single table name parameter Rather than have the user pass this information in as two separate parameters, she can use standard dot notation, as in SCOTT.EMP, and the intab procedure will parse them as follows:
dot_loc := INSTR (table_nm, '.');
IF dot_loc > 0
THEN
owner_nm := SUBSTR (table_nm, 1, dot_loc−1);
table_nm := SUBSTR (table_nm, dot_loc+1);
END IF;
You should always try to make the interface as seamless and intelligent as possible for your users You should also always try to make use of existing programs to implement your own In this case (as pointed out to me by Dan Clamage, a technical reviewer), you can also use DBMS_UTILITY.NAME_TOKENIZE to parse this object name
With this column cursor, I extract the name, datatype, and length information for each column in the table How should I store all of this information in my PL/SQL program? To answer this question, I need to think about how that data will be used It turns out that I will use it in many ways, for example:
•
I will use the column names to build the select list for the query
•
To display the output of a table in a readable fashion, I need to provide a column header that shows the names of the columns over their data These column names must be spaced out across the line of data in, well, columnar format So I need the column name and the length of the data for that column
•
To fetch data into a dynamic cursor, I need to establish the columns of the cursor with calls to
DEFINE_COLUMN For this, I need the column datatype and length
•
To extract the data from the fetched row with COLUMN_VALUE, I need to know the datatypes of each column, as well as the number of columns
•
To display the data, I must construct a string containing all the data (using TO_CHAR to convert numbers and dates) Again, I must pad out the data to fit under the column names, just as I did with the header line
Therefore, I need to work with the column information several times throughout my program, yet I do not want to read repeatedly from the data dictionary As a result, when I query them out of the all_tab_columns view, I will store the column data in three PL/SQL tables
Table Description
colname The name of each column
coltype The datatype of each column, a string describing the datatype
collen The number of characters required to display the column data
So if the third column of the emp table is SAL, then colname(3) = `SAL', coltype(3) = `NUMBER', and
Trang 2collen(3) = 7, and so forth.
The name and datatype information is stored directly from the data dictionary When I work with the
DBMS_SQL built−ins, however, they do not use the strings describing the datatypes (such as "CHAR" and
"DATE") Instead, DEFINE_COLUMN and COLUMN_VALUE rely on PL/SQL variables to infer the
correct datatypes So I use three local functions, is_string, is_date, and is_number, to help me translate a datatype into the correct variable usage The is_string function, for example, validates that both CHAR and VARCHAR2 are string datatypes:
FUNCTION is_string (row_in IN INTEGER) RETURN BOOLEAN
IS
BEGIN
RETURN (coltype(row_in) IN ('CHAR', 'VARCHAR2'));
END;
Figuring out the appropriate number of characters required to fit the column's data (the contents of the collen PL/SQL table) is a bit more complicated
2.5.4.4 Computing column length
I need to take several different aspects of the column into account:
1
The length of the column name (you don't want the column length to be smaller than the header)
2
The maximum length of the data If it's a string column, that information is contained in the
data_length column of all_tab_columns
3
If it's a number column, that information is contained in the data_precision column of the view (unless the datatype is unconstrained, in which case that information is found in the data_length column)
4
If it's a date column, the number of characters will be determined by the length of the date format mask
As you can see, the type of data partially determines the type of calculation I perform for the length Here's the formula for computing a string column's length:
GREATEST
(LEAST (col_rec.data_length, string_length_in),
LENGTH (col_rec.column_name))
The formula for a numeric column length is as follows:
GREATEST
(NVL (col_rec.data_precision, col_rec.data_length),
LENGTH (col_rec.column_name))
Finally, here's the formula for a date column length:
GREATEST (LENGTH (date_format_in), LENGTH (col_rec.column_name))
I use these formulas inside a cursor FOR loop that sweeps through all the columns for a table (as defined in all_tab_columns) This loop (shown following) fills my PL/SQL tables:
Trang 3FOR col_rec IN
col_cur (owner_nm, table_nm)
LOOP
/* Construct select list for query */
col_list := col_list || ', ' || col_rec.column_name;
/* Save datatype and length for calls to DEFINE_COLUMN */
col_count := col_count + 1;
colname (col_count) := col_rec.column_name;
coltype (col_count) := col_rec.data_type;
/* Compute the column length with the
above formulas in a local module */
collen (col_count) := column_lengths;
/* Store length and keep running total */
line_length := line_length + v_length + 1;
/* Construct column header line */
col_header := col_header || ' ' || RPAD (col_rec.column_name, v_length);
END LOOP;
When this loop completes, I have constructed the select list, populated my PL/SQL tables with the column information I need for calls to DEFINE_COLUMN and COLUMN_VALUE, and also created the column header line Now that was a busy loop!
Next step? Construct the WHERE clause In the following code, I check to see if the "WHERE clause" might actually just be a GROUP BY or ORDER BY clause In those cases, I'll skip the WHERE part and attach this other information
IF where_clause IS NOT NULL
THEN
IF (where_clause NOT LIKE 'GROUP BY%' AND
where_clause NOT LIKE 'ORDER BY%')
THEN
where_clause := 'WHERE ' || LTRIM (where_clause, 'WHERE');
END IF;
END IF;
I have now finished construction of the SELECT statement Time to parse it, and then construct the various columns in the dynamic cursor object
2.5.4.5 Defining the cursor structure
The parse phase is straightforward enough I simply cobble together the SQL statement from its processed and refined components
DBMS_SQL.PARSE
(cur,
'SELECT ' || col_list ||
' FROM ' || table_in || ' ' || where_clause,
DBMS_SQL.NATIVE);
Of course, I want to go far beyond parsing I want to execute this cursor Before I do that, however, I must give some structure to the cursor Remember: when you open a cursor, you have merely retrieved a handle to
a chunk of memory When you parse the SQL statement, you have associated a SQL statement with that memory But as a next step, you must define the columns in the cursor so that it can actually store fetched data
With Method 4 dynamic SQL, this association process is complicated I cannot "hard−code" the number or type of calls to DEFINE_COLUMN in my program; I do not have all the information until runtime
Trang 4Fortunately, in the case of intab, I have kept track of each column to be retrieved Now all I need to do is issue
a call to DEFINE_COLUMN for each row defined in my PL/SQL table colname Before we go through the actual code, here are some reminders about DEFINE_COLUMN
The header for this built−in procedure is as follows:
PROCEDURE DBMS_SQL.DEFINE_COLUMN
(cursor_handle IN INTEGER,
position IN INTEGER,
datatype_in IN DATE|NUMBER|VARCHAR2)
There are three things to keep in mind with this built−in:
1
The second argument is a number; DEFINE_COLUMN does not work with column names −− only
with the sequential position of the column in the list
2
The third argument establishes the datatype of the cursor's column It does this by accepting an
expression of the appropriate type You do not, in other words, pass a string like "VARCHAR2" to DEFINE_COLUMN Instead, you would pass a variable defined as VARCHAR2
3
When you are defining a character−type column, you must also specify the maximum length of values retrieved into the cursor
In the context of intab, the row in the PL/SQL table is the Nth position in the column list The datatype is stored in the coltype PL/SQL table, but must be converted into a call to DEFINE_COLUMN using the
appropriate local variable These complexities are handled in the following FOR loop:
FOR col_ind IN 1 col_count
LOOP
IF is_string (col_ind)
THEN
DBMS_SQL.DEFINE_COLUMN
(cur, col_ind, string_value, collen (col_ind));
ELSIF is_number (col_ind)
THEN
DBMS_SQL.DEFINE_COLUMN (cur, col_ind, number_value);
ELSIF is_date (col_ind)
THEN
DBMS_SQL.DEFINE_COLUMN (cur, col_ind, date_value);
END IF;
END LOOP;
When this loop is completed, I will have called DEFINE_COLUMN for each column defined in the PL/SQL tables (In my version this is all columns for a table In your enhanced version, it might be just a subset of all these columns.) I can then execute the cursor and start fetching rows The execution phase is no different for Method 4 than it is for any of the other simpler methods,
fdbk := DBMS_SQL.EXECUTE (cur);
where fdbk is the feedback returned by the call to EXECUTE Now for the finale: retrieval of data and
formatting for display
Trang 52.5.4.6 Retrieving and displaying data
I use a cursor FOR loop to retrieve each row of data identified by my dynamic cursor If I am on the first row,
I will display a header (this way, I avoid displaying the header for a query which retrieves no data) For each row retrieved, I build the line and then display it:
LOOP
fdbk := DBMS_SQL.FETCH_ROWS (cur);
EXIT WHEN fdbk = 0;
IF DBMS_SQL.LAST_ROW_COUNT = 1
THEN
/* We will display the header information here */
.
END IF;
/* Construct the line of text from column information here */
.
DBMS_OUTPUT.PUT_LINE (col_line);
END LOOP;
The line−building program is actually a numeric FOR loop in which I issue my calls to COLUMN_VALUE I call this built−in for each column in the table (information that is stored in −− you guessed it −− my PL/SQL tables) As you can see below, I use my is_* functions to determine the datatype of the column and therefore the appropriate variable to receive the value
Once I have converted my value to a string (necessary for dates and numbers), I pad it on the right with the appropriate number of blanks (stored in the collen PL/SQL table) so that it lines up with the column headers
col_line := NULL;
FOR col_ind IN 1 col_count
LOOP
IF is_string (col_ind)
THEN
DBMS_SQL.COLUMN_VALUE (cur, col_ind, string_value);
ELSIF is_number (col_ind)
THEN
DBMS_SQL.COLUMN_VALUE (cur, col_ind, number_value);
string_value := TO_CHAR (number_value);
ELSIF is_date (col_ind)
THEN
DBMS_SQL.COLUMN_VALUE (cur, col_ind, date_value);
string_value := TO_CHAR (date_value, date_format_in);
END IF;
/* Space out the value on the line
under the column headers */
col_line := col_line || ' ' || RPAD (NVL (string_value, ' '),
collen (col_ind));
END LOOP;
There you have it A very generic procedure for displaying the contents of a database table from within a PL/SQL program It all fell pretty smoothly into place once I got the idea of storing my column structures in a set of PL/SQL tables Drawn on repeatedly, those in−memory tables made it easy to implement Method 4 dynamic SQL −− another example of how taking full advantage of everything PL/SQL has to offer
strengthens your ability to implement quickly and cleanly