Beyond the functionality defined by the java.sql.Struct interface, the Struct class also has the following proprietary constructor and methods, all of which can throw a SQLException: STR
Trang 1ResultSetMetaData getMetaData( )
String getName( )
15.7.4 STRUCT Implements Struct
The oracle.sql.STRUCT class implements the java.sql.Struct interface A STRUCT not only implements a Struct, but is also used along with a StructDescriptor object to create a new Struct object in your Java program Beyond the functionality defined by the
java.sql.Struct interface, the Struct class also has the following proprietary constructor and methods, all of which can throw a SQLException:
STRUCT STRUCT(StructDescriptor type, Connection conn, Object
15.7.5 REF Implements Ref
The oracle.sql.REF class implements the java.sql.Ref interface A Ref is used as a pointer to an object row in the database Besides implementing the java.sql.Ref interface, REF also has the following proprietary methods, all of which can throw a SQLException:
OracleConnection getConnection( )
StructDescriptor getDescriptor( )
STRUCT getSTRUCT( )
Object getValue( )
Object getValue(Dictionary map)
boolean isConvertibleTo(Class jClass)
void setValue(Object value)
Object toJdbc( )
Now you know how to use a Struct, Array, and Ref to insert objects into a database, update objects, delete objects, and select objects from a database So let's move on to Chapter 16, where you'll learn to do the same with the strongly typed SQLData and CustomDatum interfaces
Chapter 16 Strongly Typed Object SQL
Strongly typed object SQL refers to the use of client-side custom Java classes to manipulate database-side SQL objects The classes themselves are referred to as custom because a Java
class is created to mirror its database counterpart To mirror database objects you can use one of two approaches: the JDBC API's standard SQLData interface or Oracle's CustomDatum
interface With the SQLData interface, a database object is represented as a custom Java class that implements the SQLData interface; however, a collection is still represented by an Array object, and a reference is still represented by a Ref object With the Oracle CustomDatum interface, a database object is represented as an Oracle custom class file that implements the CustomDatum and CustomDatumFactory interfaces Unlike the SQLData interface, The CustomDatum interface supports all database object types, including references and collections For example, in Chapter 15 we used a Struct object to manipulate a database object, an Array object for collections, and a Ref object to hold a database reference With strongly typed object SQL, you'll use a custom Java class to manipulate a database object, an Array object or another custom Java class for a collection, and a Ref object or yet another custom Java class to hold a database reference
Trang 2If you're concerned with portability, then you should use the SQLData interface Otherwise, since the SQLData interface currently doesn't provide support for collections and references, or if you're performing a data-processing task, I'd use Oracle's CustomDatum interface
In this chapter, we'll cover both the standard java.sql.SQLData and the Oracle
oracle.sql.CustomDatum interfaces Before we do, we'll spend some time in the next section covering how to use Oracle's JPublisher utility JPublisher can be used to automatically generate the custom Java classes for both the SQLData and CustomDatum interfaces It's important to take the time to read the next section because we use JPublisher throughout this chapter to generate the custom Java classes for the examples
16.1 JPublisher
Oracle's JPublisher utility queries the database for the database object types you specify, and using the mapping options you specify, creates either a SQLData or a CustomDatum
implementation of a Java class for each SQL object JPublisher itself is a Java program that has
a command-line interface You specify its runtime parameters on the command line when you execute the program, but all the command-line options must be listed on one line, and this is an invitation for errors Alternatively, instead of typing a long list of parameters on the command line, you can execute JPublisher using properties and input files We'll start by covering all the
command-line options, then discuss how most of them can be entered into a properties file, continue with input file syntax, and finish up with an outline of how to use JPublisher to generate
a custom Java class
16.1.1 Command-Line Options
Execute JPublisher by executing the jpub program at a host command prompt Specify any
command-line options by using the following syntax:
A valid value for the corresponding option_name
There should be no spaces following the switch character (-), nor around the equal sign (=)
Following are the options available for use with JPublisher along with descriptions of their
possible values Default values are underlined
-builtintypes={jdbc|oracle}
Controls type mappings, such as the choice between standard Java classes such as String and Oracle Java classes such as CHAR, for nonnumeric, non-LOB, nonuser-defined SQL or PL/SQL data types
-case={lower|mixed|same|upper}
Controls how JPublisher translates database type names to Java class and attribute names lower, same, and upper are self-explanatory For mixed, JPublisher uses the Java naming convention, removing any underscore ( _ ) or dollar sign ($) characters but
Trang 3using their placement in the database type name to denote the beginning of different words to support capitalization
Specifies the name of a mapping file A mapping file allows you to specify the data type
mapping between SQL and Java in a file rather than on the command line
.java files for a reference, varying array, or nested table type If the value is named, then
only those methods listed in the input file are wrapped
objectjdbc
Maps the numeric types to corresponding Java wrapper classes such as Short,
Integer, Long, Float, Double, etc This makes detecting database NULL values feasible
Trang 4Controls whether the schema name is used in the generated classes The default is to include the schema name
-package= java_package_name
Specifies a Java package name to be included in the generated classes
-props= properties_filename
Specifies the name of a properties file A properties file allows you to specify the
command-line options covered in this section in a file that in turn is read by JPublisher -sql= type_name: super_class_name: map_class_name
-sql= type_name: map_class_name
Specifies the name of a database object, an optional Java superclass name, and a Java class name for which to generate class files You can use this option multiple times to specify multiple object types for which to generate classes:
type_name
Identifies the name of the database type If you're going to extend a superclass, then use the first format and specify a super_class_name that you will extend with a subclass: the map_class_name
Specifies the database URL The default value is jdbc:oracle:oci8:@
-user= username/ password
A username and password that have access to the database types for which you want to generate classes This information must be specified in order to use JPublisher
-usertypes={jdbc|oracle}
Controls mappings for user-defined types and determines whether the SQLData or CustomDatum interface is implemented by the generated classes Selecting jdbc results in the use of the SQLData interface, while a value of oracle results in the use of the CustomDatum interface
All of the properties in the previous list, except for -props, can be specified in a properties file And for your sanity's sake, I hope you use one To show you why I feel the way I do, I'll provide
an example of a JPublisher command where I specify the properties on the command line If you wish to create classes for the five object types introduced in Chapter 14 and wish for those classes to use SQLData interface implementations, use the following command at the host's command prompt:
jpub.exe user=scott/tiger methods=false builtintypes=jdbc
Trang 5property, use an input file We'll examine both of these file types in the next two sections
16.1.2 Property File Syntax
Instead of listing all the desired command-line options on the command line when you run
JPublisher, you can put them in a properties file and specify the properties filename on the command line with the -props option To enter options in a properties file, prefix them with jpub For example, to specify the -user option, type the following into a text file:
Be warned that trailing spaces on your property values will make them invalid for example,
"jdbc " with a trailing space character, is not recognized, but jdbc is recognized If you make the mistake of leaving a trailing space character, you'll get an error message similar to this:
ERROR: Option -builtintypes=jdbc is invalid
This error will drive you crazy trying to figure out what's wrong when your option setting looks right
16.1.3 Input File Syntax
Instead of specifying the database types to generate classes on the command line, as we did in the earlier example, you can specify your class file generation options (those specified with the -sql option) in an input file that you in turn specify on the command line with the -input option Alternatively, you can specify the input file in the properties file with the jpub.input property
An input file is a text file with the following syntax (items in brackets are optional):
Trang 6The name of the class generated with the expectation that the class will be extended by
java_map_class_name, which in turn will be manually coded by a programmer to
extend java_super_class_name If the GENERATE clause is omitted, then the AS clause's java_map_class_name is generated
AS
A clause that determines the name of a subclass if the GENERATE clause is used or the name of the generated class if the GENERATE clause is omitted
java_map_class_name
The name of the class that will subclass the generated class file if the GENERATE clause
is used or the name of the class file that is generated if the GENERATE clause is omitted It's also the class name that is used when modifying the class map in your Java program (more on this later)
The name you wish to use for the method in the generated class
16.1.4 Writing a Class That Extends a Generated Class
If you use the GENERATE clause, you need to write the subclass that will extend the superclass When you do, your subclass must:
• Have a no argument constructor that calls the no argument constructor for the superclass For a CustomDatum class, you must also have a constructor that takes a Connection object and passes it to the superclass, and you must have another
constructor that takes a ConnectionContext object and passes it to the superclass
• Implement the CustomDatum or SQLData interface Your subclass activity does this automatically by inheriting from its parent class
• Implement CustomDatumFactory if it's implementing the CustomDatum interface Now that you have some background on how JPublisher works, let's actually use it to generate a SQLData class for the type person_typ
Trang 716.2 The SQLData Interface
The java.sql.SQLData interface allows you to create custom Java classes that mirror your user-defined database types But, as my mother-in-law would say, "What do you get for that?" If you haven't used an object database before, using a database to store objects, that is, both data and methods, requires a shift in your thinking Instead of just modeling the data around, and establishing relationships between, different things, you can complete the puzzle by including a thing's behavior When you create a user-defined data type in the database, you can also include methods for its behaviors You can continue to use relational SQL and retrieve the object data as though it were in tables, and execute object methods as though they were separate stored procedures, but with the SQLData interface, you don't have to Instead, you can create a Java object that will mimic your database object and retrieve an object directly from the database into your Java program as an object There is no longer any need to do any relational-to-object mapping in your Java program Now you can use objects
When you use SQLData, follow these steps:
1 Create custom Java classes to represent database user-defined data types
2 Add the custom Java classes to the Connection object's type map
3 For insert and update operations, use a PreparedStatement object with an
appropriately formulated SQL statement
4 Use the getObject( ) or setObject( ) accessor methods to get and set the object values as needed
Since we will use JPublisher to write our custom Java classes, I will not go into any great detail about hand-coding them However, I will briefly talk about the process for doing that in the next section
16.2.1 Hand-Coding a SQLData Implementation
Writing your own SQLData classes is really not that difficult The SQLData interface requires you
to implement three methods:
String getSQLTypeName( )
void readSQL(SQLInput stream, String typeName)
void writeSQL(SQLOutput stream)
The getSQLTypeName( ) method returns the database type name The readSQL( ) method uses the SQLInput stream that is passed to it from the JDBC driver to populate the attributes in the custom Java class For each attribute in the database type, the appropriate SQLInput object readXXX( ) method is called in the same order as the attributes in the database type For example, let's take location_typ It's defined as:
create type LOCATION_typ as object (
map member function get_map return varchar2,
static function get_id return number );
/
Trang 8Assuming that the class's variables are defined elsewhere in the class, a readSQL( ) method for this type would look something like this:
public void readSQL(SQLInput stream, String type)
import java.sql.*;
public class SQLDataLocation implements SQLData, Serializable {
private java.math.BigDecimal locationId;
private java.math.BigDecimal parentLocationId;
private String code;
private String name;
private java.sql.Timestamp startDate;
private java.sql.Timestamp endDate;
Trang 10}
}
But what if you have 100, or 500, or maybe even 1,000 types for which you need to create Java classes? Manually coding the classes can be an onerous and unproductive task, especially when you stop to consider that JPublisher can do the job for you
16.2.2 Using JPublisher to Generate SQLData Classes
The process of creating custom Java classes for your database types with JPublisher is:
1 Create database object types
2 Create a JPublisher mapping file, referred to as the input file
3 Create a JPublisher properties file that points to the mapping file
4 Execute JPublisher using the -props option
5 Compile any sqlj files created by JPublisher in order of dependence
6 Compile any java files created by JPublisher in order of dependence
16.2.2.1 Creating database objects
We covered step 1, creating database objects, in Chapter 14 In that chapter, we created several types, so we won't repeat that step here We ended up creating six types for our
Let's proceed to step 2 and create a mapping file
16.2.2.2 Creating a mapping file for SQLData
Of the six types mentioned previously, two, location_typ and person_typ, have methods Since the SQLData interface does not support database object methods, we need to create a superclass using JPublisher and then later hand-code a subclass that implements their methods
So for these two types, we use the GENERATE clause to create a superclass Then later, we create a subclass that implements JPublisher's generated class, which adds wrapper methods to call the database type's methods For the other four types, we simply use the AS clause Here's
our mapping file, sqldata.input:
SQL LOCATION_TYP GENERATE JLocation AS Location
SQL PERSON_IDENTIFIER_TYPE_TYP AS
PersonIdentifierType
SQL PERSON_IDENTIFIER_TYP AS PersonIdentifier SQL PERSON_TYP GENERATE JPerson AS Person
SQL PERSON_LOCATION_TYP AS Perso nLocation The first line instructs JPublisher to generate a superclass JLocation that will be extended by the subclass Location from the database type LOCATION_TYP Remember that although the case of the database data type is not important, the case of the GENERATE and AS clause's class
Trang 11names will be used in the classes themselves The second line instructs JPublisher to generate the PersonIdentifierType class from the database data type
PERSON_IDENTIFIER_TYPE_TYP After executing JPublisher, you end up with five classes: JLocation, PersonIdentifierType, PersonIdentifier, JPerson, and
PersonLocation But what about the sixth type, PERSON_IDENTIFIER_TAB? When using the SQLData interface, you'll use a java.sql.Array for Oracle collections, just as we did in Chapter 15
Now that we have the mapping, or input, file written, let's move on to the properties file
16.2.2.3 Creating a properties file for SQLData
The properties file will allow you to list the properties you can pass on the command line in a text file Using a properties file ensures that you use the same properties when generating all your
classes Here's the properties file sqldata.properties, which we'll use for generating the SQLData
Specifies the name of the input file
As stated earlier, you can specify all these values on the command line However, a properties file is a tidier approach
16.2.2.4 Executing JPublisher
Trang 12Now that you have an input and a properties file, you can generate the classes by executing JPublisher with the following command at the command prompt:
16.2.2.5 Examining JPublisher's output
Before we move on to using the classes generated by JPublisher, let's look at the source code that JPublisher created for the superclass JLocation.java:
public static final String _SQL_NAME = "SCOTT.LOCATION_TYP";
public static final int _SQL_TYPECODE = OracleTypes.STRUCT;
private java.math.BigDecimal m_locationId;
private java.math.BigDecimal m_parentLocationId;
private String m_code;
private String m_name;
private java.sql.Timestamp m_startDate;
private java.sql.Timestamp m_endDate;
Trang 14Overall, it's pretty similar to the SQLData interface code we hand-coded for type location_typ earlier Nothing earth-shattering And that's my point Why should you write this generic code when your computer can do it for you? However, since the SQLData interface does not define database object method support, and therefore, JPublisher does not support the creation of wrappers for database object methods, you'll have to write some code after all So let's take a look at extending a superclass
16.2.2.6 Extending a generated superclass
Now that we have the classes generated by JPublisher, we need to create the subclasses
Location and Person for JLocation and JPerson Since the process is similar for both, and person_typ has more methods, I'll cover the Person class here
Reviewing the criteria that we covered earlier for extending a JPublisher class, all we need to do
in this instance is create a class that extends JPerson and has a no argument constructor that calls its parent class's no argument constructor Accordingly, here's a minimal subclass: public class Person extends JPerson {
public class Person extends JPerson {
private Connection conn = null;
public Person( ) {
super( );
}
// We've added a constructor that takes a connection so we
// have one available to make store d-procedure calls
public Person(Connection conn) {
Trang 15public Integer getAge( ) throws SQLException {
Integer age = null;
public Integer getAgeOn(Timestamp date) throws SQLException {
Integer age = null;
// We've also added a setter method to set the connection
// so one is available for the stored -procedure calls
public void setConnection(Connect ion conn) {
Person person = new Person(conn);
Or, if we have retrieved a person from the database, we can call the setConnection( ) method to initialize the Connection in the Person instance
The first method in the Person class is getId( ) getId( ) is a wrapper method for the person_typ type's static method GET_ID( ) The static method GET_ID( ) returns the next sequence value for the personId attribute It's defined as static so that it is available when there is no instance of type person_typ Of course, it has to be this way, because the method is used only when creating a new instance Since GET_ID( ) is a static method, it's called using its type name, person_typ In our subclass, it has been implemented as an instance method, but it could have just as easily been a static method if we were to recode it to accept a Connection
Trang 16object However, when we consider how it will be used, that is, to get the next ID value for the personId attribute, there is no need to make it a static function in Java
The next method, getAge( ), is a wrapper class for the person_typ type's member method, GET_AGE( ) As defined in the database type, GET_AGE( ) has no arguments, yet we pass an argument So what's happening here? Since a member method requires an instance of its type in order to be executed, each member method has an implied first argument appropriately called SELF What we're doing in getAge( ) is passing the Java this reference to the member method as SELF
The third method, getAgeOn( ), is a wrapper class for the person_typ type's member method GET_AGE_ON( ) GET_AGE_ON( ) takes one argument, a DATE from which to calculate an age, but this time, we pass two arguments! Once again, that's because we pass the this reference
as the implied first argument, SELF
Notice that we've coded all three methods to fail silently if no Connection object is available by testing the existence of the Connection variable, conn, with an if statement
One question that begs to be asked as a result of all this discussion is why would anyone want to execute methods in the database when they could possibly do so more efficiently in the client? The next section attempts to answer this question
16.2.2.7 Database versus client method execution
Why would anyone execute a method in the database instead of writing code to execute the method on the client? Rather than take a one-sided stand, as the question implies, a better approach is to ask: "Where is the best place to execute a type's method?" With the first method, getId( ), there is no way to get a sequence's next value more efficiently in a client, and yet maintain control over how the sequence numbers are allocated, than in a database
Consequently, using a static type (user-defined database type) method is the best choice
However, using the member type methods getAge( ) and getAgeOn( ) instead of coding these in the custom Java class is questionable
If it is possible to implement a method in a client's invocation of an object with exactly the same results that the database would produce, then the method in question can be coded in the Java class However, keep in mind that we are now using the database as persistent storage for objects, not just for data Any application that accesses the database should be able to use an object's methods as well as its data This is a drastic departure from traditional relational
database thinking If a method is reproduced in another environment such as on the client, and later the functionality of said method is changed in the database, then the database and client implementations will be out-of-sync On the other hand, if the method is wrapped and called from the database, it can never be out-of-sync
There seems to be this pervasive impression that calling stored procedures, or making remote procedure calls, is inherently bad Yet CORBA and Java, two of the most popular and growing technologies, are built around the concept of remote object invocation Be very thoughtful when you decide how to implement database type methods in your Java classes
Of course, there are always the no-brainer member methods, which perform a significant amount
of database processing These are best done in the database because doing so eliminates the network overhead involved in passing data back and forth between client and database
Regardless, I recommend that you always create wrapper methods that call a database object's methods in the database rather than attempt to recode those methods on the client This allows you to move your object model into the database where it belongs
16.2.3 Adding Classes to a Type Map
Trang 17Now that we have a JPublisher SQLData class for each database type, it's time to put them to work Unlike the built-in SQL data types, custom Java classes have no default SQL-to-Java data type mapping supplied by the driver Instead, you, as the programmer, must provide the required mapping by adding your custom Java classes to a connection's type map The type map for a connection is usually a hash table that holds keyword value pairs, with the database object type
as the keyword and an empty instance of the custom Java class as the value
After you provide an updated type map to your connection, use the getObject( ) and
setObject( ) accessor methods as you would with any built-in SQL data type accessor to get and set values When you add your custom Java classes to a connection's type map, a call to the getObject( ) method returns an instance of an object of the Java type you specified, and a call to setObject( ) expects an instance of the Java type you specified If you don't update the type map, a call to the getObject( ) method gives you its default object, a Struct, while a call to a the setObject( ) methods expects a Struct To add entries to a type map, follow these steps:
1 Get the existing type map from a Connection object
2 Add your custom Java objects to the type map
3 Replace the Connection object's current type map with the one you updated
16.2.3.1 Getting an existing type map
When you first get a Connection object from DriverManager or from a DataSource object, the default type map is empty So, at the time you wish to update a connection's type map, if you know that this is the first time it's being updated, it's not necessary to retrieve the existing type map Instead, you can create a new Map object such as a HashTable, add your mapping entries
to it, and use it to update the connection However, it's easier and less problematic to just retrieve the existing type map, empty or not, from a connection To retrieve an existing type map from a Connection object, use the getTypeMap( ) method, which has the following signature: Map getTypeMap( )
For example, to get the type map for the current connection named conn, use code similar to the following:
java.util.Map map = conn.getTypeMap( );
Once you've retrieved the type map, you're ready to add mapping entries to it
16.2.3.2 Adding mapping entries
To add new entries to a type map, use the Map object's put( ) method, passing the database type name and a copy of the class that implements the database type To create a copy of a class, use the Class.forName( ) method The put( ) method has the following signature: Object put(Object key, Object value)
which breaks down as:
Trang 18A copy of an existing Object value for the specified key, or null
For example, to add the custom Java class Location, which mirrors the database type
LOCATION_TYP to the Map object retrieved earlier, your code will be similar to this:
map.put("SCOTT.LOCATION_TYP", Class.forNa me("Location"));
Here, the key, SCOTT.LOCATION_TYP, is the fully qualified name of the database object type upon which the LOCATION_OT object table was created For the key's value, the
Class.forName( ) method is called, passing the name of the Location class
Class.forName( ), in turn, instantiates a copy of the class When you're finished adding
mapping entries to the Map object, you're ready to update your connection with it
16.2.3.3 Setting the updated type map
The last step in adding your custom Java classes to a type map is to update your connection's type map by using the Connection object's setTypeMap( ) method The setTypeMap( ) method has the following signature:
setTypeMap(Map map)
in which map is the Map object to which you've added your desired entries For example, to
update the Connection object, conn, with the Map object, map, which we modified earlier, use the following code:
It's important to notice that we used the subclasses Location and Person, not the
superclasses JLocation and JPerson, when adding entries to the type map
16.2.4 Using getObject( ) with a Type Map
If all you do in your program is retrieve objects from a database, you have another option at your disposal Instead of updating your connection's type map, you can create a new type map and pass it to one of the overloaded forms of the getObject( ) method Here are the signatures for the two forms of the getObject( ) method that allow you to specify a type map:
Object getObject(int i, Map map)
Object getObject(String colName, Map map)
The first method takes the relative position of a column in the SELECT statement, starting with 1,
as the first parameter, and a type map as the second parameter The second method takes a column name (from the SELECT statement) as the first parameter and a type map for the
second These two methods allow you to use a type map to retrieve database objects without having to change your connection's type map
16.2.5 Inserting an Object
Trang 19Once you have your custom Java classes and an updated type map, you're ready to store an object in the database In this section, we'll concentrate on inserting a new object into the
database The process for inserting an object is basically the same as it was when using a Struct object, but this time, you'll be using a custom Java class instead of a Struct Because the process is basically the same, I won't get into as much detail here as I did in Chapter 15 Assuming you have updated a connection with an updated type map that includes your custom Java classes, the process for inserting an object is:
1 Create a new instance of your custom Java class, setting the values for the new object where appropriate
2 Formulate an INSERT statement for an object table where the VALUES clause has one placeholder for your new object
3 Create a PreparedStatement object using your INSERT statement
4 Use the setObject( ) method to set the value of the placeholder
5 Execute the prepared statement
16.2.5.1 Creating a new instance of a custom Java class
Creating a new instance of one of your custom Java classes is fairly straightforward If you've been using Java for any period of time, you've already done this many times To create a new instance, declare a variable of a custom Java class Then assign it an instance of its custom Java class by using the new operator:
Person person = new Person(conn);
Here, we've created a new instance of a Person object with the new operator and assigned it to the variable person In this case, we've used our alternative constructor that takes a connection
as an argument, so we can later call the person object's getId( ) method to allocate a new primary key sequence Now that we have a new person instance, we can use its accessor methods to set its attribute values For example:
// Call the Person object's getId( ) method to get the next
// sequence value from the database for its primary key
long personId = person.getId( );
person.setMothersMaidenName("Oh! I don't know!");
// The Oracle collection, PERSON_IDENTIFIER_TAB, must still be
// manipulated as a JDBC Array, but this time, we populate the
// Array object with our custom Java class personIdentifier
// instead of Struct
Object[] ids = new Object[2];
personIdentifier = new PersonIdentifier( );