Ideally, I would like to allow a developer to call fk_name with no more information than in the following examples: :call.name := fk_name :call.caller_id, 'caller'; :call.call_type_ds :=
Trang 1The function assumes that the datatype of the foreign key is INTEGER; I have encountered many tables with VARCHAR2 foreign keys True, the reasons are usually suspect, but it happens just the same
Can I change fk_name to handle some of these concerns? I can certainly add a parameter with a maximum name length to use in the call to COLUMN_VALUE I can support VARCHAR2 datatypes for foreign keys
by placing the function inside a package and overloading the definition of the function What about all those parameters and the naming conventions? Ideally, I would like to allow a developer to call fk_name with no more information than in the following examples:
:call.name := fk_name (:call.caller_id, 'caller');
:call.call_type_ds := fk_name (:call.call_type_id, 'call_type');
In this scenario, the function would use the name of the table to generate the names of the key and name columns and stuff them into the SQL statement Sounds reasonable to me The second version of fk_name supports default names for these two columns
In this version, the last three parameters have default values If the user does not specify an ID column name, the default is the table name with the default suffix The same goes for the fk_name column If the user
includes a value for either of these arguments, then if the value starts with an underscore ( _ ), it will be used
as a suffix to the table name Otherwise, the value will be used as a complete column name The following table shows how parameter values will be converted inside the program
Column to fk_name Converted Value
ID column caller_number caller_number
ID column caller_name caller_name
Name column _fullname caller_fullname
Here, then, in Version 2, we have an even more generic function to return foreign key names
/* Filename on companion disk: fkname2.sf */*
CREATE OR REPLACE FUNCTION fk_name
(fk_id_in IN INTEGER,
fk_table_in IN VARCHAR2,
fk_id_col_in IN VARCHAR2 := '_ID',
fk_nm_col_in IN VARCHAR2 := '_NM',
max_length_in IN INTEGER := 100)
RETURN VARCHAR2
/* I will not repeat any comments from first version of fk_name */
IS
/*
|| Local variables to hold column names, since I must construct
|| those names based on the values provided If the column names
|| are NULL, then fall back on the defaults.
*/
fk_id_column VARCHAR2(60) := NVL (fk_id_col_in, '_ID');
fk_nm_column VARCHAR2(60) := NVL (fk_nm_col_in, '_NM');
cur INTEGER := DBMS_SQL.OPEN_CURSOR;
fdbk INTEGER;
/*
|| The return value of the function Notice that even though one
|| of the parameters now specifies a maximum size for the return
|| value, I still do have to hardcode a size in my declaration.
Trang 2*/
return_value VARCHAR2(100) := NULL;
/*−−−−−−−−−−−−−−−−−−−−−− Local Module −−−−−−−−−−−−−−−−−−−−−−−−−−−*/
PROCEDURE convert_column (col_name_inout IN OUT VARCHAR2)
/*
|| Construct the column name If the argument begins with a "_",
|| use as suffix to table name Otherwise, substitute completely.
*/
IS
BEGIN
IF SUBSTR (col_name_inout, 1, 1) = '_'
THEN
col_name_inout := fk_table_in || col_name_inout;
ELSE
/* Default value on variable declaration already handles it */
NULL;
END IF;
END;
BEGIN
/* Convert the column names as necessary based on arguments */
convert_column (fk_id_column);
convert_column (fk_nm_column);
/* Parse statement using converted column names */
DBMS_SQL.PARSE
(cur,
'SELECT ' || fk_nm_column ||
' FROM ' || fk_table_in ||
' WHERE ' || fk_id_column || ' = :fk_value',
DBMS_SQL.NATIVE);
DBMS_SQL.BIND_VARIABLE (cur, 'fk_value', fk_id_in);
DBMS_SQL.DEFINE_COLUMN (cur, 1, fk_nm_column, max_length_in);
fdbk := DBMS_SQL.EXECUTE (cur);
fdbk := DBMS_SQL.FETCH_ROWS (cur);
IF fdbk > 0
THEN
DBMS_SQL.COLUMN_VALUE (cur, 1, return_value);
END IF;
DBMS_SQL.CLOSE_CURSOR (cur);
RETURN return_value;
END;
With this new version of fk_name, I can certainly retrieve the caller's name and the call type description without specifying all the columns, assuming that their columns match my conventions
•
Assume that table caller has caller_id and caller_nm columns:
:call.name := fk_name (:call.caller_id, 'caller');
•
Assume that call_type table has call_type_id and call_type_nm columns:
:call.call_type_ds := fk_name (:call.call_type_id, 'call_type');
Of course, conventions do not hold so consistently in the real world In fact, I have found that database
administrators and data analysts will often treat an entity like caller, with its caller ID number and caller name, differently from the way they would treat a caller type, with its type code and description The columns for the caller type table are more likely to be caller_typ_cd and caller_typ_ds Fortunately, fk_name will still handle
Trang 3this situation as follows:
•
A full set of defaults works just fine:
:call.name := fk_name (:call.caller_id, 'caller');
•
Use alternative suffixes for a code table:
:call.call_type_ds :=
fk_name (:call.call_type_id, 'call_type', '_cd', '_ds');
You might scoff and say, "Why bother providing just the suffixes? Might as well go ahead and provide the full column names." But there is a value to this approach: if the data analysts have adopted standards for their naming conventions of tables and key columns, the fk_name interface supports and reinforces these standards, and avoids supplying redundant information
Is that it, then? Have we gone as far as we can go with fk_name? Surely some of you have looked at those rather simple SELECT statements and thought, "Gee, very few of my lookups actually resemble such
queries." I agree Sometimes you will need to check an additional column on the table, such as a "row active?" flag You might even have several records, all for the same primary key, but active for different periods So you should also pass a date against which to check
How can you handle these application−specific situations? When in doubt, just add another parameter!
Sure Why not add a parameter containing either a substitute WHERE clause for the SQL statement, or a clause to be appended to the rest of the default WHERE clause? The specification for fk_name would then change to the following:
/* Filename on companion disk: fkname3.sf */*
FUNCTION fk_name
(fk_id_in IN INTEGER,
fk_table_in IN VARCHAR2,
fk_id_col_in IN VARCHAR2 := '_ID',
fk_nm_col_in IN VARCHAR2 := '_NM',
max_length_in IN INTEGER := 100,
where_clause_in IN VARCHAR2 := NULL)
RETURN VARCHAR2;
The rule for this WHERE clause would be as follows: if the string starts with the keywords AND or OR, then the text is appended to the default WHERE clause Otherwise, the argument substitutes completely for the default WHERE clause
Rather than repeat the entire body of fk_name, I offer only the modifications necessary to PARSE, and thus effect this change in the following code:
IF UPPER (where_clause_in) LIKE 'AND%' OR
UPPER (where_clause_in) LIKE 'OR%'
THEN
/* Append the additional Boolean expressions to default */
where_clause :=
' WHERE ' || fk_id_column || ' = :fk_value ' || where_clause_in;
ELSIF where_clause_in IS NOT NULL
THEN
/* Substitute completely the WHERE clause */
where_clause := ' WHERE ' || where_clause_in;
Trang 4ELSE
/* Just stick with default */
where_clause := ' WHERE ' || fk_id_column || ' = :fk_value';
END IF;
/* Now the call to PARSE uses the pre−processed WHERE clause */
DBMS_SQL.PARSE
(cur,
'SELECT ' || fk_nm_column || ' FROM ' ||
fk_table_in || where_clause, DBMS_SQL.NATIVE);
Using this final version of fk_name, I can perform lookups as follows:
•
Retrieve only the description of the call type if that record is still flagged as active Notice that I must stick several single quotes together to get the right number of quotes in the evaluated argument passed
to fk_name
:call.call_type_ds :=
fk_name (:call.call_type_id, 'call_type', '_cd', '_ds', 25, 'AND row_active_flag = ''Y''');
•
Retrieve the name of the store kept in the record for the current year Notice that the ID and name arguments in the call to fk_name are NULL I have to include values here since I want to provide the WHERE clause, but I will pass NULL and thereby use the default values (without having to know what those defaults are!)
/* Only the record for this year should be used */
year_number := TO_CHAR (SYSDATE, 'YYYY');
/*
|| Pass check for year to WHERE clause */
:store.description :=
fk_name (:store.store_id, 'store_history', NULL, NULL, 60, 'AND TO_CHAR (eff_date, ''YYYY'') = ''' ||
year_number || '''');
The fragment of the WHERE clause passed to fk_name can be arbitrarily complex, including subselects, correlated subqueries, and a whole chain of conditions joined by ANDs and ORs
2.5.3 A Wrapper for DBMS_SQL DESCRIBE_COLUMNS
The DESCRIBE_COLUMNS procedure provides a critical feature for those of us writing generic, flexible code based on dynamic SQL With earlier versions of DBMS_SQL, there was no way to query runtime memory to find out the internal structure of a cursor Now you can do this with DESCRIBE_COLUMNS, but
it is very cumbersome As shown in the section Section 2.3.11, "Describing Cursor Columns "," you must declare a PL/SQL table, read the cursor structure into that table, and then traverse the table to get the
information you need
A much better approach is to write the code to perform these steps once and then encapsulate all that
knowledge into a package Then you can simply call the programs in the package and not have to worry about all the internal data structures and operations that have to be performed
You will find an example of this "wrapper" around DESCRIBE_COLUMNS on your companion disk Here is the specification of that package:
/* Filename on companion disk: desccols.spp */*
CREATE OR REPLACE PACKAGE desccols
IS
Trang 5varchar2_type CONSTANT PLS_INTEGER := 1;
number_type CONSTANT PLS_INTEGER := 2;
date_type CONSTANT PLS_INTEGER := 12;
char_type CONSTANT PLS_INTEGER := 96;
long_type CONSTANT PLS_INTEGER := 8;
rowid_type CONSTANT PLS_INTEGER := 11;
raw_type CONSTANT PLS_INTEGER := 23;
mlslabel_type CONSTANT PLS_INTEGER := 106;
clob_type CONSTANT PLS_INTEGER := 112;
blob_type CONSTANT PLS_INTEGER := 113;
bfile_type CONSTANT PLS_INTEGER := 114;
PROCEDURE forcur (cur IN INTEGER);
PROCEDURE show (fst IN INTEGER := 1, lst IN INTEGER := NULL);
FUNCTION numcols RETURN INTEGER;
FUNCTION nthcol (num IN INTEGER) RETURN DBMS_SQL.DESC_REC;
END desccols;
/
Before we look at the implementation of this package, let's explore how you might use it I declare a set of constants that give names to the various column types This way, you don't have to remember or place in your code the literal values Now notice that there are no other data structures defined in the specification Most importantly, there is no declaration of a PL/SQL table based on DBMS_SQL.DESC_T to hold the description information That table is instead hidden away inside the package body You call the desccols.forcur
procedure to "describe the columns for a cursor," passing it your cursor ID or handle, to load up that table by calling DESCRIBE_COLUMNS You then can take any of the following actions against that PL/SQL table of column data:
•
Show the column information by calling desccols.show (the prototype on disk shows only the column name and column type)
•
Retrieve the total number of columns in the table with a call to desccols.numcols
•
Retrieve all the information for a specific column by calling the desccols.nthcol function
The following script defines a cursor, extracts the cursor information with a call to desccols.forcur, and then shows the cursor information:
/* Filename on companion disk: desccols.tst */*
DECLARE
cur INTEGER := DBMS_SQL.OPEN_CURSOR;
BEGIN
DBMS_SQL.PARSE
(cur, 'SELECT ename, sal, hiredate FROM emp', DBMS_SQL.NATIVE);
DBMS_SQL.DEFINE_COLUMN (cur, 1, 'a', 60);
DBMS_SQL.DEFINE_COLUMN (cur, 1, 1);
DBMS_SQL.DEFINE_COLUMN (cur, 1, SYSDATE);
desccols.forcur (cur);
desccols.show;
DBMS_SQL.CLOSE_CURSOR (cur);
END;
/
Here is the output in SQL*Plus:
Column 1
ENAME