|
Developer: Open Source
Add Some Spring to Your Oracle JDBC Access
by Dustin Marx
Learn how to use the Spring Framework to improve your Oracle JDBC access.
JDBC is a standard data-access protocol that is typically used to access relational databases. A significant advantage of JDBC is its standardized API, which allows for portability of Java-based data-accessing applications. JDBC is considered an integral part of both standard Java (J2SE) and enterprise Java (J2EE) and has been around since Java's early days.
The many positive aspects of JDBC have allowed it to play a significant role in many J2SE and J2EE applications. However, there are some characteristics that make it more difficult to use than we might desire. These tedious, and sometimes frustrating, characteristics of JDBC have led to the creation of publicly available JDBC abstraction frameworks (such as SQLExecutor and Apache Jakarta Commons DBUtils) as well as many more times that number of homegrown JDBC application frameworks. One publicly available JDBC abstraction framework is Spring Framework's JDBC abstraction.
The Spring Framework is a Java/J2EE application framework released under the Apache license that potentially supports multiple tiers in a J2EE application. A prominent feature of the Spring Framework is its support for more maintainable and robust JDBC data access. In this article, you will learn how the Spring Framework—which can be used in conjunction with the Oracle TopLink object/relational mapping tool—reduces much of the tedium and risk associated with writing JDBC code. Developers can use the Spring Framework to make their Oracle database-accessing JDBC code cleaner, less error-prone, and more flexible.
Proper Closure of Database Resources
A common mistake seen in JDBC code is failure to properly close connections. This results in misallocation of database resources. Likewise, closing the result set and the statement are also useful and often recommended practices. To ensure that these closures are performed appropriately even under abnormal execution conditions, code such as that shown in the finally block in Listing 1 is typically employed.
Listing 1
try
{
// JDBC Connection/Statement/Result Set
}
catch (SQLException sqlEx)
{
// Handle the exception
}
finally
{
try
{ // Closing connection *should* close statement and result set
if (stmt != null) stmt.close();
if (conn != null) conn.close();
}
catch (SQLException sqlEx)
{
System.err.println("SQLException NOT handled");
}
}
The finally clause is often employed to ensure that database connections and statements are closed. Even when developers do manage to ensure connections are closed using this mechanism, the code is verbose, bloated, and repetitive. The Spring Framework abstracts the connection handling and related resource management away from the developer, enabling more-consistent resource closures and more readable code.
First Spring Code Examples
The JDBC code in Listing 2 could be used to query commissions of employees in the well-known scott/tiger schema. As discussed earlier, a tremendous amount of "routine" code is necessary in this example in addition to the SQL code that actually queries the database.
Listing 2
List commissions = new ArrayList();
Statement stmt = null;
ResultSet rs = null;
try
{
stmt = this.myConnection.createStatement();
rs = stmt.executeQuery("SELECT comm FROM emp");
while ( rs.next() )
{
Integer commission = new Integer( rs.getInt("COMM") );
if ( rs.wasNull() )
{
// By assigning the commission to null, this effectively
// represents a null in the database as a Java null.
System.out.println( "\tCommission seen as " + commission +
" is really null");
commission = null;
}
commissions.add( commission );
}
}
catch (SQLException sqlEx) // checked
{
System.err.println( "Message: " + sqlEx.getMessage() );
System.err.println( "Error Code: " + sqlEx.getErrorCode() );
System.err.println( "SQL State: " + sqlEx.getSQLState() );
}
finally
{
try
{
if ( rs != null ) { rs.close(); }
if ( stmt != null ) { stmt.close(); }
}
catch (SQLException sqlEx) // checked
{
System.err.println( sqlEx.getMessage() );
}
}
Listing 3 shows code using the Spring Framework to provide functionality similar to that shown earlier in Listing 2.
Listing 3
List commissions = new ArrayList();
try
{
JdbcTemplate jt = new JdbcTemplate(this.myDataSource);
List commList = jt.queryForList( "SELECT comm FROM emp");
Iterator commIter = commList.iterator();
while ( commIter.hasNext() )
{
Number comm = (Number) ((Map) commIter.next()).get("COMM");
if (comm != null)
commissions.add( new Integer(comm.intValue()) );
else commissions.add( null ); }
}
catch ( DataAccessException ex ) // unchecked exception
{
System.err.println( ex.getMessage() );
}
It is noteworthy how much less code is required with the Spring Framework to implement essentially the same functionality as with straight JDBC. As Listing 3 shows, you do not need to remember to write and maintain code that manages resources (connections, statements, result sets). Even the small amount of exception handling shown in Listing 3 is not absolutely required because the DataAccessException is an unchecked exception. Because the Number type is used to return the commissions, there is no need to explicitly call the ResultSet's wasNull method. In fact, you won't even find the ResultSet syntax anywhere in Listing 3!
Listing 3 also demonstrates one of the fundamental classes provided by and used by the Spring Framework's JDBC support—JdbcTemplate. An instance of this Spring-provided class is instantiated with a data source and then one of its overridden queryForList methods is invoked on the template class with a provided SQL string. The queryForList method returns an ArrayList of HashMaps where each element in the ArrayList is a returned data row and each map entry within a particular array list element is a column value in that row.
JdbcTemplate provides many overridden versions of queryForList methods for querying for potentially multiple rows. This highly useful class also provides versions of methods such as queryForInt (return a single integer), queryForLong (return a single long), query, update, etc. The easiest way to distinguish among the different versions of these overridden methods is to read the "Method Detail" section of the Javadoc-based API documentation that is delivered with the Spring Framework. The methods differ in terms of type of statements used (such as Statement or PreparedStatement) and in features supported. JdbcTemplate also provides methods that require slightly more JDBC knowledge than the one used above, but which reward the extra effort with increased flexibility. These more flexible and slightly more JDBC knowledge-dependent methods are discussed later in this article.
JDBC Exception Handling
Referring back to Listing 1, note that java.sql.SQLException is the sole exception that is explicitly caught. SQLException is a single exception that captures a wide variety of database- and SQL-related exceptional conditions. The Javadoc comments describing the SQLException class explain the basic information that can be retrieved from an instance of SQLException. This information includes the error description string [getMessage()], the somewhat standardized SQLState exception String [getSQLState()], and a vendor-specific integer error code [getErrorCode()]. All three of these are employed in the trivial exception handling implemented in Listing 1.
SQLException is a checked exception (directly extends java.lang.Exception). Java's checked exceptions have become highly controversial and there seems to be building consensus in the Java community that checked exceptions should only be used when the exceptions can be handled in the application. Exceptions that cannot be handled in any meaningful way by application code should be allowed to go on unchecked. Because SQLException is a checked exception, application code must handle it, either catching it and doing something with it or explicitly throwing it to calling code.
A final nuance of SQLException is that it is specific to relational data sources that use the SQL. This makes it undesirable for inclusion in truly portable Data Access Objects (DAO) that should be independent of data repository type and its access language.
The Spring Framework's handling of SQLException is one of its most useful features in terms of enabling easier JDBC development and maintenance. The Spring Framework provides JDBC support that abstracts SQLException and provides a DAO-friendly, unchecked exception hierarchy.
Figure 1 (created with Oracle JDeveloper 10g's UML modeling tool) depicts some of the most interesting and significant Spring Framework exception classes for JDBC and DAO. All classes shown in Figure 1 belong in a subpackage under the org.springframework package. The JDBC-specific exception handling classes are in the jdbc subpackage and the more-general DAO-exception-handling classes are in the dao subpackage.
Handling Vendor-Specific Error Codes
As described above, the standard SQLException provides both a standardized piece of information (SQLState) and a vendor-specific piece of information (ErrorCode). As is the case for most databases and their JDBC driver implementations, the Oracle database and JDBC drivers provide significantly more detail about problems via the vendor-specific error code than via the vendor-neutral SQLState component of SQLException.
An obvious example of the significantly greater detail provided by the Oracle database and its JDBC driver via Error Code is the case of the SQLState code 42000 (generically this indicates a syntax error or access issue). This same SQLState value of 42000 is returned by SQLException with the Oracle JDBC driver for a wide range of different Oracle error codes. Some of the Oracle error codes that map to the single SQLState of 42000 include 900 ("invalid SQL statement"), 903 ("invalid table name"), 904 ("invalid identifier"), 911 ("invalid character"), and 936 ("missing expression"). It is also significant that any error originating in the Oracle JDBC driver (as opposed to originating in the Oracle Database) has no corresponding SQLState whatsoever. As these examples demonstrate, the Oracle-specific error codes provide significantly more detail about error conditions than do the vendor-neutral SQLStates.
It is sometimes important to distinguish between errors originating in the Oracle Database from those originating in the Oracle JDBC driver. For example, the 903 ("invalid table name") error code corresponds to Oracle database error code ORA-00903. On the other hand, the 17003 ("invalid column index") error code corresponds to the Oracle JDBC driver error code ORA-17003. Both these types of codes (database and JDBC driver) are specific to Oracle as indicated by the ORA- prefix. Because there are no SQLState codes for errors that originate in the Oracle JDBC driver, Oracle-specific error codes must be used for differentiating JDBC driver-caused errors.
Some of the most commonly seen errors in JDBC accessing an Oracle database are listed in Table 1 below. The examples in the "Example Code and/or Comment" column show types of SQL statements that lead to this error or provide additional comments on the specific error shown in that row of the table.
Table 1
| Error Label |
Oracle Error |
SQLState |
Example Code and/or Comment |
SQL-related errors based on variations of statement: SELECT ename FROM emp |
| "unique constraint" |
1 |
2300 |
Primary key violation is an example |
"resource busy and acquire with NOWAIT specified" |
54 |
61000 |
Only occurs when NOWAIT is specified |
"invalid SQL statement" |
900 |
42000 |
ename FROM emp |
"invalid table name" |
903 |
42000 |
SELECT ename FROM |
"invalid identifier" |
904 |
42000 |
SELECT empname FROM emp |
"invalid character" |
911 |
42000 |
SELECT ename FROM emp; |
"missing column" |
917 |
42000 |
Omission of comma when expected for column differentiation in an INSERTstatement is one cause. |
"FROM keyword not found where expected" |
923 |
42000 |
SELECT ename emp |
"missing expression" |
936 |
42000 |
SELECT FROM emp |
"table or view does not exist" |
942 |
42000 |
SELECT ename FROM empp |
"cannot insert null into" |
1400 |
23000 |
Trying to insert null into column with NOT NULL constraint |
"value larger than specified precision allows for this column" |
1438 |
22003 |
Trying to insert more numerical digits than precision on column allows |
"invalid number" |
1722 |
42000 |
Trying to perform numeric function on characters |
"integrity constraint failed" |
2291 |
23000 |
Trying to insert row with foreign key that has no match to existing primary key |
"value too large for column" |
12899 |
72000 |
Trying to insert larger value (such as too many characters) than the column allows |
"Io exception" |
17002 |
none |
Oracle JDBC driver-originated errors have no corresponding SQLState (null) |
"Invalid column index" |
17003 |
none |
|
"Invalid column name" |
17006 |
none |
|
"Numeric Overflow" |
17026 |
none |
|
The online resources section at the end of this article includes a link to a Web site that details the various Oracle database exceptions one might encounter. The Oracle JDBC driver error codes can be found in Appendix B of the Oracle JDBC Developer's Guide and Reference and several general Oracle database-originated error codes can be found in the Oracle Database Error Messages document (does not list product-specific ORA messages).
The Spring Framework supports both standard-based SQLState and vendor-specific error codes. The framework's support for vendor-specific error codes is implemented with looser coupling to the specific database than homegrown data-access software often is. The Spring Framework makes use of an XML configuration file to link certain vendor-specific errors commonly encountered in JDBC code to Spring-supported exception classes. The sql-error-codes.xml configuration file the Spring Framework provides for this purpose currently includes the configuration shown in Listing 4 for the Oracle Database. (Several other database vendors are also represented in this file but are not shown here.)
Listing 4
<bean id="Oracle”
class="org.springframework.jdbc.support.SQLErrorCodes">
<property name="badSqlGrammarCodes">
<value>900,903,904,917,936,942,17006</value>
</property>
<property name="invalidResultSetAccessCodes">
<value>17003</value>
</property>
<property name="dataAccessResourceFailureCodes">
<value>17002</value>
</property>
<property name="dataIntegrityViolationCodes">
<value>1,1400,1722,2291</value>
</property>
<property name="cannotAcquireLockCodes">
<value>54</value>
</property>
</bean>
The integer numbers separated by commas in the body of the value elements in sql-error-codes.xml correspond to the numeric portion of the vendor-specific error codes discussed earlier. Many of the numeric codes in the "badSqlGrammarCodes" category were listed in Table 1. The 17006 code is a JDBC driver error code and indicates "Invalid column name." The name attribute of the property element tags in Listing 4 instructs the Spring Framework on which type of exception to use to handle those specific error codes. For example, a 917 (ORA-00917) error would result in the Spring Framework throwing an unchecked BadSqlGrammarException. Because this configuration file is XML and is external to the code, it is easy to add other codes to this file to throw Spring-based JDBC exceptions most suitable for a particular vendor error code.
There are several reasons that you might wish to have specific exceptions thrown corresponding to their database's error code. For example, you may wish to handle select situations in which a SQLException normally gets thrown, but not handle all cases because, in many cases, there is little you could have the code do at runtime anyway. By making a finer-grain exception hierarchy available to the database developer and providing a loosely coupled connection between a specific database error and a specific exception, the Spring Framework enables you to more easily handle those exceptions that can be readily handled and choose to ignore the unchecked exceptions that cannot be reasonably handled.
One of the especially handy exception classes Spring provides for JDBC support is the BadSqlGrammarException. This exception class provides a method called getSql() that returns the SQL statement that was being invoked when the exception was thrown. Because this class is aware of SQL specifics (it is not a general DAO class), it also provides a handle to the standard SQLException via the getSQLException() method.
Besides adding other Oracle-specific error codes to the sql-error-codes.xml file to map them to existing Spring-provided exception classes, you could also create custom exception handling classes. Then, a custom SQLExceptionTranslator class can be written to tie Oracle error codes to these custom exception handling classes.
Using PreparedStatement with Spring
The Spring example in Listing 3 relies on Spring's wrapping of Statement for executing SQL. Often, however, PreparedStatement is preferred over Statement for executing SQL against the database. The Spring JdbcTemplate class provides many methods with the same functionality built on both Statement and PreparedStatement so that you can select which underlying type of JDBC statement you wish to use.
The Spring Javadoc-based API documentation specifies whether each method uses a Statement or PreparedStatement. You can also usually differentiate which type is used by a JdbcTemplate by whether SQL arguments are passed to the method along with the SQL string. If only a SQL string is passed in, the method typically uses Statement. If the method accepts arguments for the SQL string along with the SQL statement, the method typically uses PreparedStatement. The next two listings (Listings 5 and 6) show a standard JDBC access using PreparedStatement and a Spring-based access that wraps PreparedStatement.
Listing 5
String updateStr =
"UPDATE salgrade SET losal = ?, hisal = ? WHERE grade = ?";
PreparedStatement stmt = null;
try
{
stmt = this.myConnection.prepareStatement(updateStr);
stmt.setInt(1,aLowSal);
stmt.setInt(2,aHighSal);
stmt.setInt(3,aSalGrade);
updateStatus = stmt.execute();
stmt.close();
} // lots of catch and finally code typically follows here
Listing 6
String updateStr =
"UPDATE salgrade SET losal = ?, hisal = ? WHERE grade = ?";
JdbcTemplate jt = new JdbcTemplate(this.myDataSource);
jt.update( updateStr,
new Object[] { new Integer(aLowSal),
new Integer(aHighSal),
new Integer(aSalGrade) } );
// No handling/closing of PreparedStatement or catch/finally needed
Listing 6 shows an example using the Spring Framework to update the database. Although the syntax "PreparedStatement" does not appear in the listing, the use of JdbcTemplate's particular update method in this example does use the PreparedStatement. The standard use of JDBC requires SQLException to be caught and a finally block is necessary to ensure closure of the statement. None of this is required for the Spring-based code in Listing 6.
Note that there is no explicit mention of PreparedStatement in the executable code in Listing 6. The developer using this convenient JdbcTemplate update method need not be concerned about PreparedStatement specifics, its API, or about SQLException. Note that the developer uses an anonymous inner class to provide the values to be passed to the JdbcTemplate.update method along with the SQL string.
Using Oracle-Specific SQL
A useful characteristic of the Spring Framework is that it focuses "wrapping" only the most common and most frustrating aspects of JDBC development without unduly prohibiting use of proprietary SQL/JDBC when needed. While we would all like to keep our code completely standardized if such results came at little or no disadvantage, there are times when using a particular vendor's specific features will be prudent and possibly even necessary.
One example in the world of Oracle is the use of Oracle's ROWID for uniquely describing rows in Oracle tables. Listings 7 and 8 show traditional JDBC and Spring-powered JDBC code respectively for retrieving a ROWID from the scott/tiger EMP table based on a provided employee number. In both cases, a ROWID is available to be returned as a String.
Listing 7
String queryStr = "SELECT rowid FROM emp WHERE empno = "
+ aEmpNum; // aEmpNum set previously
String rowId = null;
try
{
stmt = this.myConnection.createStatement();
rs = stmt.executeQuery(queryStr);
while ( rs.next() )
{
rowId = rs.getString("ROWID");
}
} // lots of catch-finally code needed after this
Listing 8
String queryStr = "SELECT rowid FROM emp WHERE empno = "
+ aEmpNum;
String rowId = null;
try
{
JdbcTemplate jt = new JdbcTemplate(this.myDataSource);
oracle.sql.ROWID oraRowId =
(ROWID) jt.queryForObject(queryStr, ROWID.class);
rowId = oraRowId.stringValue();
}
catch ( IncorrectResultSizeDataAccessException wrongSizeEx )
{
// This unchecked exception is thrown in this case if more
// than one result is returned from the query.
// Explicitly printing out the results of this exception's
// methods getExpectedSize() and getActualSize() is really not
// necessary in this case because this exception's getMessage()
// returns this same information in sentence form.
System.err.println( wrongSizeEx.getMessage() );
System.err.print( "Expected " + wrongSizeEx.getExpectedSize()
+ " results, but actually got back "
+ wrongSizeEx.getActualSize() + " results.");
}
Besides showing the flexibility of the Spring Framework to support Oracle-specific keywords, Listing 8 also demonstrates the usefulness of one of Spring's DAO exceptions. The IncorrectResultSizeDataAccessException would be thrown in Listing 8 if queryStr was ever accidentally edited to return all ROWIDs.
Perhaps the best-known example of proprietary Oracle SQL is the ubiquitous query SELECT sysdate FROM dual. Listing 9 demonstrates how this Oracle-specific query (not part of the ANSI standard) can be used with the Spring Framework.
Listing 9
String queryStr = "SELECT sysdate FROM dual";
Date date = null;
try
{
JdbcTemplate jt = new JdbcTemplate(this.myDataSource);
date = (Date) jt.queryForObject(queryStr, Date.class);
}
catch ( BadSqlGrammarException badSqlEx ) // unchecked
{
System.err.println( badSqlEx.getMessage() );
System.err.println( "Bad SQL: " + badSqlEx.getSql() );
}
DDL Statements with Spring and JDBC
The previous code listings demonstrate using the Spring Framework to handle DML statements. The Spring Framework provides very simple syntax for supporting DDL statements. Listings 10 and 11 demonstrate standard JDBC code and Spring-wrapped JDBC code for running a statement that drops and purges a table from the Oracle database.
Listing 10
Statement stmt = null;
try
{
stmt = this.myConnection.createStatement();
stmt.executeUpdate("DROP TABLE salgrade PURGE");
}
catch ( SQLException sqlEx )
{
System.err.println("Message: " + sqlEx.getMessage());
System.err.println("Error Code: " + sqlEx.getErrorCode());
System.err.println("SQL State: " + sqlEx.getSQLState());
}
finally
{
try
{
if (stmt != null)
{
stmt.close();
}
}
catch (SQLException finallySqlEx) // checked exception
{
System.err.println(finallySqlEx.getMessage());
}
}
Listing 11
try
{
JdbcTemplate jt = new JdbcTemplate(this.myDataSource);
jt.execute("DROP TABLE salgrade PURGE");
}
catch ( BadSqlGrammarException badSqlEx ) // unchecked
{
System.err.println( badSqlEx.getMessage() );
System.err.println( "BadSQL: " + badSqlEx.getSql() );
}
At this point, it should be apparent that the Spring-based code is much simpler to read (and write and maintain) than the straight JDBC code. In fact, only two lines in that listing are absolutely needed since the captured exception is an unchecked exception.
Accessing Stored Procedures with the Spring Framework
Listings 13 and 14 demonstrate accessing a contrived stored procedure (shown in Listing 12) from straight JDBC and Spring-wrapped JDBC respectively.
Listing 12
CREATE OR REPLACE PROCEDURE salary_percentile (
salary IN emp.sal%TYPE,
low IN salgrade.losal%TYPE,
high IN salgrade.hisal%TYPE,
percentile OUT NUMBER )
AS
BEGIN
IF salary < 0 THEN
percentile := null;
ELSE
percentile := (salary - low) / (high - low);
END IF;
END;
Listing 13
String escapeString = "{call salary_percentile (?,?,?,?)}";
CallableStatement cStmt = null;
double percentile = -1.0;
final int PERCENTILE_INDEX = 4;
try
{
cStmt = this.myConnection.prepareCall(escapeString);
cStmt.setInt(1, aSalary); // aSalary passed into this method
cStmt.setInt(2, aLow); // aLow passed into this method
cStmt.setInt(3, aHigh); // aHigh passed into this method
cStmt.registerOutParameter(PERCENTILE_INDEX, Types.DOUBLE);
cStmt.execute();
percentile = cStmt.getDouble(PERCENTILE_INDEX);
}
catch ( SQLException sqlEx )
{
System.err.println("Message: " + sqlEx.getMessage());
System.err.println("Error Code: " + sqlEx.getErrorCode());
System.err.println("SQL State: " + sqlEx.getSQLState());
}
finally
{
if ( cStmt != null )
{
try
{
cStmt.close();
}
catch (SQLException finallySqlEx)
{
System.err.println(finallySqlEx.getMessage());
}
}
}
return percentile;
The example of using Spring-based code to access a stored procedure in Listing 14 demonstrates the use of the org.springframework.jdbc.object.StoredProcedure class. (The Spring package containing this StoredProcedure class also contains object representations of other types of SQL statements besides stored procedure calls. See Spring's documentation for more details on the object package and object representations of other SQL statement types.)
Listing 14
private class SalaryCalculator extends StoredProcedure
{
/** Name of procedure in database. */
public static final String PROC_NAME = "salary_percentile";
/**
* Constructor for this StoredProcedure class.
* @param ds Data Source.
*/
public SalaryCalculator(DataSource ds)
{
setDataSource(ds);
setSql(PROC_NAME);
// Parameters should be declared in same order here that
// they are declared in the stored procedure.
declareParameter(new SqlParameter("salary", Types.DOUBLE));
declareParameter(new SqlParameter("low", Types.INTEGER));
declareParameter(new SqlParameter("high", Types.INTEGER));
declareParameter(new SqlOutParameter( "percentile",
Types.DOUBLE ) );
compile();
}
/**
* Execute stored procedure.
* @return Results of running stored procedure.
*/
public Map executeCalculation( double aSalary,
int aLow,
int aHigh )
{
Map inParameters = new HashMap();
inParameters.put( "salary", new Double(aSalary) );
inParameters.put( "low", new Integer(aLow) );
inParameters.put( "high", new Integer(aHigh) );
Map out = execute( inParameters ); // Call on parent class
return out;
}
}
// . . .
// Code below is all that is needed to call your Stored Procedure
// created above.
// . . .
SalaryCalculator calcPercentile =
new SalaryCalculator(this.myDataSource);
Map calcResults =
calcPercentile.executeCalculation(aSalary, aLow, aHigh);
return ((Double)calcResults.get("percentile")).doubleValue();
// . . .
// remainder of class
// . . .
The code in Listing 14 represents code in a single class. The majority of the code shown in this listing is an inner class (SalaryCalculator) that extends Spring's StoredProcedure class. This developer-created class wraps the stored procedure shown in Listing 12. Only a few lines of code are necessary to invoke the SalaryCalculator class. Thus, the SalaryCalculator class abstracts away the majority of issues involved with invoking a stored procedure.
While the developer must write the class that extends StoredProcedure, there are worthwhile benefits from making the effort to do this rather than writing the stored procedure access directly. One benefit is the ability to use Spring's specific, unchecked DAO and JDBC exceptions rather than the generic, checked SQLException. Also, as shown in the code listing, the tedium of dealing with closing resources is abstracted away.
It is interesting to note the difference between the results returned from Listings 13 and 14 when a salary less than zero is passed into the stored procedure case. In the straight JDBC case (Listing 13), a value of 0.0 is returned and wasNull() is the only way to determine that the result was really null. In the Spring-based code (Listing 14), a Java null is returned in this case and no wasNull() call is necessary.
Accessing Oracle Objects with Spring
Spring Framework's JDBC abstraction can be used with Oracle objects such as the object created in Listing 15.
Listing 15
CREATE OR REPLACE TYPE address_type AS OBJECT
(
STREET VARCHAR2(20),
CITY VARCHAR2(15),
STATE CHAR(2),
ZIP CHAR(5)
);
/
CREATE TABLE emp_address_table
(
EMP_NUM NUMBER(4),
ADDRESS ADDRESS_TYPE
);
There are two commonly used approaches for accessing Oracle objects with JDBC. One approach is to use the standard JDBC interface java.sql.Struct with its Oracle driver-specific class implementation oracle.sql.STRUCT. The second approach is to generate Java classes that map to the Oracle object types. This is almost trivial to do for Oracle objects with Oracle JDeveloper 10g IDE and JPublisher.
An interesting side effect of using either the java.sql.Struct approach or the JPublisher one is that both lead to the necessity to handle SQLException if you want to access the data in these objects. For example, with the java.sql.Struct approach, the getAttributes() method throws SQLException. Likewise, the Java classes produced by JDeveloper/JPublisher also have methods that throw SQLException. The developer who accesses these Java objects must deal with these SQLExceptions or else can use Spring as shown in Listings 16 and 17.
Listing 16
String queryStr = "SELECT address FROM emp_address_table WHERE "
+ "emp_num = " + aEmpNum; // aEmpNum passed in
final List addresses = new ArrayList();
JdbcTemplate jt = new JdbcTemplate(this.myDataSource);
jt.query( queryStr,
new RowCallbackHandler()
{
public void processRow(ResultSet rs)
throws SQLException
{
// The Struct and ResultSet methods throw
// SQLException, so throws above is necessary
java.sql.Struct address =
(java.sql.Struct) rs.getObject(1);
String street =
address.getAttributes()[0].toString();
String city =
address.getAttributes()[1].toString();
String state =
address.getAttributes()[2].toString();
String zipCode =
address.getAttributes()[3].toString();
String addressStr = street + ", " + city + ", "
+ state + " " + zipCode;
addresses.add( addressStr );
}
}
);
Listing 17
String updateStr = "UPDATE emp_address_table SET address = ? "
+ "WHERE emp_num = ?";
JdbcTemplate jt = new JdbcTemplate( getDataSource() );
jt.update( updateStr,
new PreparedStatementSetter()
{
public void setValues(PreparedStatement ps)
throws SQLException
{
Address address = new Address();
address.setStreet(aStreet);
address.setCity(aCity);
address.setState(aState);
address.setZip(aZipCode);
ps.setObject(1, address);
ps.setInt(2, aEmpNum);
}
}
);
In both approaches to accessing Oracle objects with JDBC (Struct and SQLData), the returned classes are accessed with methods that throw SQLException. Listings 16 and 17 show how to use anonymous inner callback classes to "hide" the SQLExceptions behind the more specific and unchecked Spring Framework exception hierarchy. These rethrown exceptions make use of the same exception translation as the other examples in this article. These listings not only show specifically how to handle Spring-based access of Oracle objects, but also demonstrate how anonymous inner callback classes can be used when other JdbcTemplate convenience methods do not apply.
In Listing 16, ypu'll see ResultSet and SQLException in Spring-based code for the first time in this article. However, note that SQLException is not directly used even in this code. Instead, the Spring Framework handles any thrown SQLException via its exception handling mechanism and you only need worry about the Spring exceptions you care about catching and handling.
Listing 17 demonstrates the first use in this article of PreparedStatement in Spring-based code and shows another reference to SQLException. As with Listing 16, the SQLException is mainly for reference of the Spring Framework's JdbcTemplate class, which will handle it and present any exception as an unchecked Spring exception.
Listings 16 and 17 demonstrate the use of Spring's RowCallbackHandler and PreparedStatementSetter callback interfaces. These interfaces are implemented in these listings with anonymous inner classes. While the inner classes the developer writes must know more about ResultSet and PreparedStatement and their individual APIs than the simpler uses of JdbcTemplate shown in previous listings, you are still free from having to worry about handling SQLException; JdbcTemplate handles the exception handling.
Previous Spring-based code listings (such as JdbcTemplate used in Listings 3 and 6) in this article did not even mention ResultSet, Statement, PreparedStatement, or SQLException. These highly abstracted methods are particularly useful for developers who do not want to deal with specifics of JDBC. However, those extremely convenient methods are not as flexible as the inner-class approaches demonstrated in Listings 16 and 17. The more flexible methods shown in Listings 16 and 17 can be used when necessary with only a slight knowledge of basic JDBC APIs. In all cases, exception handling is handled consistently with the Spring exception hierarchy and you needn't be concerned about SQLException.
Other Advantages
Besides the advantages the Spring Framework brings to JDBC as described in some detail above, there are several other advantages of using the Spring Framework with your JDBC that are not discussed any further here. These include the following advantages:
- Spring's JdbcTemplate class supports several other Spring-provided interfaces that are similar to the ones discussed in this article. These include ResultSetExtractor and PreparedStatementCreator, which are similar to RowCallbackHandler and PreparedStatementSetter respectively. Another of the many interfaces that JdbcTemplate supports is the highly useful BatchPreparedStatementSetter.
- While the Spring JDBC abstraction does not need to be used in a Spring application context, the optional use of the JDBC abstraction in this context provides benefit. The Spring Framework can be used to wire data access objects to business objects via configuration files rather than with direct coupling in the code.
- The org.springframework.jdbc.object package supports relational database operations (including DML statements and stored procedures) as reusable objects. These threadsafe objects give the developer an even higher level of abstraction from the relational database than provided by Spring's JdbcTemplate described earlier in this article and used in all the code examples. I personally like the level of abstraction that JdbcTemplate provides and feel that its handling of routine issues like exceptions, result sets, and resource management provide exactly the level of support I desire. However, if an organization or its developers would like to enjoy an even higher level of abstraction away from relational database concepts without embracing a complete object-relational (O/R) mapping technology (such as TopLink or Hibernate), the org.springframework.jdbc.object package may be worth considering. The stored procedure example in Listing 12 provides a brief foray into this package.
- The Spring Framework provides a simple DataSource implementation that can be used outside of a container and provides an abstract datasource class you can override as needed (see Chapter 10 of Spring's reference documentation).
- Besides JDBC, the Spring Framework supports multiple O/R mapping technologies such as Hibernate, iBatis, and Java Data Objects (JDO). (See Chapter 11 of Spring's reference documentation.) The online references section at the bottom of this article includes a link to another article on using Spring with Oracle's TopLink object-relational mapping product.
- The Spring Framework facilitates development of technology-agnostic DAO interfaces and implementations that share same exception hierarchy. (See Chapter 9 of Spring Reference Documentation.) This topic was briefly alluded to in several places in the article.
- The Spring Framework provides support for catching SQLWarnings as an exception (SQLWarningException) when desired. This is not particularly useful with the Oracle database and JDBC drivers because they tend to throw SQLException for nearly every condition. It is a more useful feature for developers who need to know about warnings and are using a database that uses SQLWarning more extensively. In those cases, the developer who uses JDBC directly must specifically ask if a SQLWarning occurred. The Spring Framework can be used to treat these warnings like any other Spring exception for easier and more consistent handling.
- The Spring Framework provides the org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor interface and some implementations (such as SimpleNativeJdbcExtractor) of this interface. These are useful for accessing Oracle features via an Oracle connection or ResultSet when the connection is "wrapped" by another DataSource (such as that used with some application servers) or obtained through certain connection pools.
- For creating instances of oracle.sql.BLOB (binary large object) and oracle.sql.CLOB (character large object), Spring provides the class org.springframework.jdbc.support.lob.OracleLobHandler.
- The Spring-provided OracleSequenceMaxValueIncrementer class provides the next value of an Oracle sequence. It effectively provides the same information that would be provided if you used the following command directly: select someSequence.nextval from dual (where someSequence is the name of your sequence in the Oracle database). An advantage of this approach is that the DataFieldMaxValueIncrementer interface can be used in a DAO hierarchy without tight coupling of the Oracle-specific implementation.
Other Uses of the Spring Framework
This article has focused on the use of Spring to write JDBC code that is more maintainable and less error-prone. The Spring Framework can be used for much more than this. Besides doing the same thing for many object-relational mapping technologies that it does for JDBC, Spring also has features supporting all other tiers found in enterprise applications. These include its lightweight IoC (Inversion of Control) container support, a Web framework, and other J2EE application support.
The Spring Framework is designed to be nonintrusive or minimally intrusive. There are many things you can use the Spring Framework for without forcing your application to depend upon all of the Spring Framework. Because Spring is open source, it is useful as a tool for providing concrete examples of good application design principles. Even if you are unable to use the Spring Framework directly in your applications at this time, it will likely be worth your time to use the Spring Framework for examples of good design and code principles. The JDBC support Spring provides does not require use of any of Spring's other features and is an excellent path for beginning use of the Spring Framework without tight coupling with other parts of Spring.
Dustin Marx is a senior software engineer and architect at Raytheon Co.
Send us your comments
|