by Krishna Sankar
In the last chapter, you saw how JDBC has ushered in an era of simple yet powerful SQL database access for Java programs. You saw how JDBC works, and a couple of JDBC examples were presented. In this chapter, you explore more of JDBC, especially how JDBC handles SQL statements and the variety of ways we can process the ResultSet it returns.
The Statement object does all of the work to interact with the Database Management System in terms of SQL statements. You can create many Statement objects from one Connection object. Internally, the Statement object would be storing the various data needed to interact with a database, including state information, buffer handles, and so on. But these are transparent to the JDBC application program.
NOTE: When a program attempts an operation that is not in sync with the internal state of the system (for example, a next() method to get a row when no SQL statements have been executed) this discrepancy is caught and an exception is raised. This exception, normally, is probed by the application program using the methods in the SQLException object.
JDBC supports three types of statements:
Before you explore these different statements, see the steps that a SQL statement goes through.
A Java application program first builds the SQL statement in a string buffer and passes this buffer to the underlying DBMS through some API calls. A SQL statement needs to be verified syntactically, optimized, and converted to an executable form before execution. In the Call Level Interface (CLI) Application Program Interface (API) model, the application program through the driver passes the SQL statement to the underlying DBMS, which prepares and executes the SQL statement.
After the DBMS receives the SQL string buffer, it parses the statement and does a syntax check run. If the statement is not syntactically correct, the system returns an error condition to the driver, which generates a SQLException. If the statement is syntactically correct, depending on the DBMS, many query plans are usually generated that are run through an optimizer (often a cost-based optimizer). Then the optimum plan is translated into a binary execution plan. After the execution plan is prepared, the DBMS usually returns a handle or identifier to this optimized binary version of the SQL statement to the application program.
The three JDBC statement types (Statement, PreparedStatement, and CallableStatement) differ in the timing of the SQL statement preparation and the statement execution. In the case of the simple Statement object, the SQL is prepared and executed in one step (at least from the application program point of view. Internally, the driver might get the identifier, command the DBMS to execute the query, and then discard the handle). In the case of a PreparedStatement object, the driver stores the execution plan handle for later use. In the case of the CallableStatement object, the SQL statement is actually making a call to a stored procedure that is usually already optimized.
NOTE: As you know, stored procedures are encapsulated business rules or procedures that reside in the database server. They also enforce uniformity across applications, as well as provide security to the database access. Stored procedures last beyond the execution of the program, so the application program does not spend any time waiting for the DBMS to create the execution plan.
Now look at each type of statement more closely and see what each has to offer a Java program.
A Statement object is created using the createStatement() method in the Connection object. Table 45.1 shows all methods available for the Statement object.
Return Type | Method Name | Parameter |
ResultSet | executeQuery | (String sql) |
int | executeUpdate | (String sql) |
boolean | execute | (String sql) |
boolean | getMoreResults | () |
void | close | () |
int | getMaxFieldSize | () |
void | setMaxFieldSize | (int max) |
int | getMaxRows | () |
void | setMaxRows | (int max) |
void | setEscapeProcessing | (boolean enable) |
int | getQueryTimeout | () |
void | setQueryTimeout | (int seconds) |
void | cancel | () |
java.sql.SQLWarning | getWarnings | () |
void | clearWarnings | () |
void | setCursorName | (String name) |
ResultSet | getResultSet | () |
int | getUpdateCount | () |
CAUTION:
Only one ResultSet can be opened per Statement object at one time.
NOTE: The JDBC processing is synchronous; that is, the application program must wait for the SQL statements to complete. But because Java is a multithreaded platform, the JDBC designers suggest using threads to simulate asynchronous processing.
The following example program shows how to use the Statement class to access a database.
See "Anatomy of a JDBC Application," Chapter 44
In this example, you will list all of the subjects (classes) available in the enrollment database and their location, Day, and times. The SQL statement for this is:
SELECT ClassName, Location, DaysAndTimes FROM Classes
You create a Statement object and pass the SQL string during the executeQuery() method call to get this data.
//Declare a method and some variables. public void ListClasses() throws SQLException { int i, NoOfColumns; String ClassName,ClassLocation, ClassSchedule; //Initialize and load the JDBC-ODBC driver. Class.forName ("jdbc.odbc.JdbcOdbcDriver"); //Make the connection object. Connection Ex1Con = DriverManager.getConnection( "jdbc:odbc:StudentDB;uid="admin";pw="sa"); //Create a simple Statement object. Statement Ex1Stmt = Ex1Con.createStatement(); //Make a SQL string, pass it to the DBMS, and execute the SQL statement. ResultSet Ex1rs = Ex1Stmt.executeQuery( "SELECT ClassName, Location, DaysAndTimes FROM Classes"); //Process each row until there are no more rows. // And display the results on the console. System.out.println("Class Location Schedule"); while (Ex1rs.next()) { // Get the column values into Java variables ClassName = Ex1rs.getString(1); ClassLocation = Ex1rs.getString(2); ClassSchedule = Ex1rs.getString(3); System.out.println(ClassName,ClassLocation,ClassSchedule); } }
As you can see, the program is very straightforward. You do the initial connection and create a Statement object. You pass the SQL along with the method executeQuery() call. The driver passes the SQL string to the DBMS which performs the query and returns the results. After the statement is finished, the optimized execution plan is lost.
In the case of a PreparedStatement object, as the name implies, the application program prepares a SQL statement using the java.sql.Connection.prepareStatement() method. The PreparedStatement() method takes a SQL string, which is passed to the underlying DBMS. The DBMS goes through the syntax run, query plan optimization, and the execution plan generation stages, but does not execute the SQL statement. Possibly, it returns a handle to the optimized execution plan that the JDBC driver stores internally in the PreparedStatement object.
The methods of the PreparedStatement object are shown in Table 45.2. Notice that the executeQuery(), executeUpdate(), and execute() methods do not take any parameters. They are just calls to the underlying DBMS to perform the already-optimized SQL statement.
Return Type | Method Name | Parameter |
ResultSet | executeQuery | () |
int | executeUpdate | () |
boolean | execute | () |
Return Type | Method Name | Parameter |
void | clearParameters | () |
void | setAsciiStream | (int parameterIndex, java.io.InputStream x, int length) |
void | setBinaryStream | (int parameterIndex, java.io.InputStream x, int length) |
void | setBoolean | (int parameterIndex, boolean x) |
void | setByte | (int parameterIndex, byte x) |
void | 1setBytes | (int parameterIndex, byte x[]) |
void | setDate | (int parameterIndex, java.sql.Date x) |
void | setDouble | (int parameterIndex, double x) |
void | setFloat | (int parameterIndex, float x) |
void | setInt | (int parameterIndex, int x) |
void | setLong | (int parameterIndex, long x) |
void | setNull | (int parameterIndex, int sqlType) |
void | setBignum | (int parameterIndex, Bignum x) |
void | setShort | (int parameterIndex, short x) |
void | setString | (int parameterIndex, String x) |
void | setTime | (int parameterIndex, java.sql.Time x) |
void | setTimestamp | (int parameterIndex, java.sql.Timestamp x) |
void | setUnicodeStream | (int parameterIndex, java.io.InputStream x, int length) |
Advanced Features--Object Manipulation | ||
void | setObject | (int parameterIndex, Object x, int targetSqlType, int scale) |
void | setObject | (int parameterIndex, Object x, int targetSqlType) |
void | setObject | (int parameterIndex, Object x) |
The following example program shows how to use the PreparedStatement class to access a database. The database schema is shown in Chapter 44. In this example, you optimize the example you developed in the Statement example.
See "Anatomy of a JDBC Application," Chapter 44
The simple Statement example can be improved in a few major ways. First, the DBMS goes through building the execution plan every time, so you make it a PreparedStatement. Secondly, the query lists all courses which could scroll away. You improve this situation by building a parameterized query as follows:
//Declare class variables Connection Con; PreparedStatement PrepStmt; boolean Initialized = false; private void InitConnection() throws SQLException { //Initialize and load the JDBC-ODBC driver. Class.forName ("jdbc.odbc.JdbcOdbcDriver"); //Make the connection object. Con = DriverManager.getConnection( "jdbc:odbc:StudentDB;uid="admin";pw="sa"); //Create a prepared Statement object. PrepStmt = Ex1Con.prepareStatement( "SELECT ClassName, Location, DaysAndTimes FROM Classes WHERE ClassName = ?"); Initialized = True; } public void ListOneClass(String ListClassName) throws SQLException { int i, NoOfColumns; String ClassName,ClassLocation, ClassSchedule; if (! Initialized) { InitConnection(); } // Set the SQL parameter to the one passed into this method PrepStmt.setString(1,ListClassName); ResultSet Ex1rs = PrepStmt.executeQuery() //Process each row until there are no more rows and // display the results on the console. System.out.println("Class Location Schedule"); while (Ex1rs.next()) { // Get the column values into Java variables ClassName = Ex1rs.getString(1); ClassLocation = Ex1rs.getString(2); ClassSchedule = Ex1rs.getString(3); System.out.println(ClassName,ClassLocation,ClassSchedule); } }
Now, if a student wants to check the details of one subject interactively, this example program can be used. You can save execution time and network traffic from the second invocation onwards because you are using the PreparedStatement object.
For a secure, consistent, and manageable multi-tier client/server system, the data access should allow the use of stored procedures. Stored procedures centralize the business logic in terms of manageability and also in terms of running the query. Java applets running on clients with limited resources cannot be expected to run huge queries. But the results are important to those clients. JDBC allows the use of stored procedures by the CallableStatement class and with the escape clause string.
A CallableStatement object is created by the prepareCall() method in the Connection object. The prepareCall() method takes a string as the parameter. This string, called an escape clause, is of the form
{[? =] call <stored procedure name> [<parameter>,<parameter> ...]}
The CallableStatement class supports parameters. These parameters are of the OUT kind from a stored procedure or the IN kind to pass values into a stored procedure. The parameter marker (question mark) must be used for the return value (if any) and any output arguments because the parameter marker is bound to a program variable in the stored procedure. Input arguments can be either literals or parameters. For a dynamic parameterized statement, the escape clause string takes the form
{[? =] call <stored procedure name> [<?>,<?> ...]}
The OUT parameters should be registered using the registerOutparameter() method--as shown in Table 45.4--before the call to the executeQuery(), executeUpdate(), or execute() methods.
Return Type | Method Name | Parameter |
void | registerOutParameter | (int parameterIndex, int sqlType) |
void | registerOutParameter | (int parameterIndex, int sqlType, int scale) |
Return Type | Method Name | Parameter |
boolean | getBoolean | (int parameterIndex) |
byte | getByte | (int parameterIndex) |
byte[] | getBytes | (int parameterIndex) |
java.sql.Date | getDate | (int parameterIndex) |
double | getDouble | (int parameterIndex) |
float | getFloat | (int parameterIndex) |
int | getInt | (int parameterIndex) |
long | getLong | (int parameterIndex) |
java.lang.Bignum | getBignum | (int parameterIndex, int scale) |
Object | getObject | (int parameterIndex) |
short | getShort | (int parameterIndex) |
String | getString | (int parameterIndex) |
java.sql.Time | getTime | (int parameterIndex) |
java.sql.Timestamp | getTimestamp | (int parameterIndex) |
Miscellaneous Functions | ||
boolean | wasNull | () |
The stored procedure call is of the form:
studentGrade = getStudentGrade(StudentID,ClassID)
In the JDBC call, you create a CallableStatement object with the "?" symbol as a placeholder for parameters, and then connect Java variables to the parameters as shown in the following example:
public void DisplayGrade(String StudentID, String ClassID) throws SQLException { int Grade; //Initialize and load the JDBC-ODBC driver. Class.forName ("jdbc.odbc.JdbcOdbcDriver"); //Make the connection object. Connection Con = DriverManager.getConnection( "jdbc:odbc:StudentDB;uid="admin";pw="sa"); //Create a Callable Statement object. CallableStatement CStmt = Con.prepareCall({?=call getStudentGrade[?,?]}); // Now tie the placeholders with actual parameters. // Register the return value from the stored procedure // as an integer type so that the driver knows how to handle it. // Note the type is defined in the java.sql.Types. CStmt.registerOutParameter(1,java.sql.Types.INTEGER); // Set the In parameters (which are inherited from the PreparedStatement class) CStmt.setString(1,StudentID); CStmt.setString(2,ClassID); // Now we are ready to call the stored procedure int RetVal = CStmt.executeUpdate(); // Get the OUT parameter from the registered parameter // Note that we get the result from the CallableStatement object Grade = CStmt.getInt(1); // And display the results on the console. System.out.println(" The Grade is : "); System.out.println(Grade); }
As you can see, JDBC has minimized the complexities of getting results from a stored procedure. It still is a little involved, but is simpler. Maybe in the future these steps will become even more simple.
Now that you have seen how to communicate with the underlying DBMS with SQL, see what you need to do to process the results sent back from the database as a result of the SQL statements.
The ResultSet object is actually a tubular data set; that is, it consists of rows of data organized in uniform columns. In JDBC, the Java program can see only one row of data at one time. The program uses the next() method to go to the next row. JDBC does not provide any methods to move backwards along the ResultSet or to remember the row positions (called bookmarks in ODBC). Once the program has a row, it can use the positional index (1 for the first column, 2 for the second column, and so on) or the column name to get the field value by using the getXXXX() methods. Table 45.6 shows the methods associated with the ResultSet object.
Return Type | Method Name | Parameter |
boolean | next | () |
void | close | () |
boolean | wasNull | () |
Get Data By Column Position | ||
java.io.InputStream | getAsciiStream | (int columnIndex) |
java.io.InputStream | getBinaryStream | (int columnIndex) |
boolean | getBoolean | (int columnIndex) |
byte | getByte | (int columnIndex) |
byte[] | getBytes | (int columnIndex) |
java.sql.Date | getDate | (int columnIndex) |
double | getDouble | (int columnIndex) |
float | getFloat | (int columnIndex) |
int | getInt | (int columnIndex) |
long | getLong | (int columnIndex) |
java.lang.Bignum | getBignum | (int columnIndex, int scale) |
Object | getObject | (int columnIndex) |
short | getShort | (int columnIndex) |
String | getString | (int columnIndex) |
java.sql.Time | getTime | (int columnIndex) |
java.sql.Timestamp | getTimestamp | (int columnIndex) |
java.io.InputStream | getUnicodeStream | (int columnIndex) |
Get Data By Column Name | ||
java.io.InputStream | getAsciiStream | (String columnName) |
java.io.InputStream | getBinaryStream | (String columnName) |
boolean | getBoolean | (String columnName) |
byte | getByte | (String columnName) |
byte[] | getBytes | (String columnName) |
java.sql.Date | getDate | (String columnName) |
double | getDouble | (String columnName) |
float | getFloat | (String columnName) |
int | getInt | (String columnName) |
long | getLong | (String columnName) |
java.lang.Bignum | getBignum | (String columnName, int scale) |
Object | getObject | (String columnName) |
short | getShort | (String columnName) |
String | getString | (String columnName) |
java.sql.Time | getTime | (String columnName) |
java.sql.Timestamp | getTimestamp | (String columnName) |
java.io.InputStream | getUnicodeStream | (String columnName) |
int | findColumn | (String columnName) |
SQLWarning | getWarnings | () |
void | clearWarnings | () |
String | getCursorName | () |
ResultSetMetaData | getMetaData | () |
Now that you have seen all of the main database-related classes, look at some of the supporting classes that are available in JDBC. These classes include the Date, Time, TimeStamp, and so on. Most of these classes extend the basic Java classes to add capability to handle and translate data types that are specific to SQL.
This package (see Table 45.7) gives a Java program the capability to handle SQL DATE information with only year, month, and day values.
Return Type | Method Name | Parameter |
Date | Date | (int year, int month, int day) |
Date | Date | (long date) |
Date | valueOf | (String s) |
String | toString | () |
int | getHours | () |
int | getMinutes | () |
int | getSeconds | () |
void | setHours | (int Hr) |
void | setMinutes | (int Min) |
void | setSeconds | (int Sec) |
void | setTime | (long date) |
As seen in Table 45.8, the java.sql.Time adds the Time object to the java.util.Date package to handle only hours, minutes, and seconds. java.sql.Time is also used to represent SQL TIME information.
Return Type | Method Name | Parameter |
Time | Time | (int hour, int minute, int second) |
Time | Time | (long time) |
Time | Time | valueOf(String s) |
String | toString | () |
int | getDate | () |
int | getDay | () |
int | getMonth | () |
int | getYear | () |
void | setDate | (int date) |
void | setMonth | (int month) |
void | setTime | (int time) |
void | setYear | (int year) |
The java.sql.Timestamp package adds the TimeStamp class to the java.util.Date package (see Table 45.9). It adds the capability of handling nanoseconds. But the granularity of the subsecond timestamp depends on the database field as well as the operating system.
Return Type | Method Name | Parameter |
TimeStamp | TimeStamp | (int year, int month, int date, int hour, int minute, int second, int nano) |
TimeStamp | TimeStamp | (long time) |
TimeStamp | valueOf | (String s) |
String | toString | () |
int | getNanos | () |
void | setNanos | (int n) |
boolean | after | (TimeStamp ts) |
boolean | before | (TimeStamp ts) |
boolean | equals | (TimeStamp ts) |
This class defines a set of XOPEN equivalent integer constants that identify SQL types. The constants are final types. Therefore, they cannot be redefined in applications or applets. Table 45.10 lists the constant names and their values.
Constant Name | Value |
BIGINT | -5 |
BINARY | -2 |
BIT | -7 |
CHAR | 1 |
DATE | 91 |
DECIMAL | 3 |
DOUBLE | 8 |
FLOAT | 6 |
INTEGER | 4 |
LONGVARBINARY | -4 |
LONGVARCHAR | -1 |
NULL | 0 |
NUMERIC | 2 |
OTHER | 1111 |
REAL | 7 |
SMALLINT | 5 |
TIME | 92 |
TIMESTAMP | 93 |
TINYINT | -6 |
VARBINARY | -3 |
VARCHAR | 12 |
This class provides methods for getting details when a DataTruncation warning or exception is thrown by a SQL statement. The data truncation could happen to a column value or parameter.
The main elements of a DataTruncation object are:
The DataTruncation object also consists of a datasize element that has the actual size (in bytes) of the truncated value and the transfer size, which is the number of bytes actually transferred.
The various methods, as listed in Table 45.11, let the Java program retrieve the values of these elements. For example, the getRead() method returns true if data truncation occurred during a read and a false if the truncation occurred during a write.
Return Type | Method Name | Parameter |
int | getDataSize | () |
int | getIndex | () |
boolean | getParameter | () |
boolean | getRead | () |
int | getTransferSize | () |
JDBC is an important step in the right direction to elevate the Java language to the Java platform. The Java APIs--including the Enterprise APIs (JDBC, RMI, Serialization, and IDL), Security APIs, and the Server APIs--are the essential ingredients for developing enterprise-level, distributed, multi-tier client/server applications.
The JDBC specification life cycle happened in the speed of the Net--one Net year is widely clocked as equaling seven normal years. The JDBC specification is fixed, so the developers and driver vendors are not chasing a moving target.
JDBC Compliant
Javasoft has instituted the JDBC Compliant certification for drivers. A particular driver will be called JDBC Compliant if it passes JDBC compliance tests developed by Javasoft and Intersolv. At present, a driver should support at least ANSI SQL92 Entry Level to pass the compliance tests.
NOTE: The JDBC released with JDK 1.1 (JDBC 1.2x) is not compatible with JDBC 1.10. Drivers written for JDBC 1.10 will not work with JDBC 1.2x The main difference is that in JDBC 1.10, the numeric SQL data type is mapped to java.lang.Bignum. In JDBC 1.2x, the java.lang.Bignum class is replaced by the java.math.BigDecimal and java.math.BigInteger classes. If you are using JDBC 1.2x, you should use the java.math.BigDecimal and java.math.BigInteger methods instead of the java.lang.Bignum methods.
By making JDBC a part of the Java language, you received all of the advantages of the Java language concepts for database access. Also, as all implementers have to support the Java APIs, JDBC has become a universal standard. This philosophy, stated in the JDBC specification as "provide a Java interface that is consistent with the rest of the Java system," makes JDBC an ideal candidate for use in Java-based database development.
Another good design philosophy is the driver independence of the JDBC. The underlying database drivers can either be native libraries--such as a DLL for the Windows system or Java routines connecting to listeners. The full Java implementation of JDBC is suitable for a variety of Network and other Java OS computers, thus making JDBC a versatile set of APIs.
NOTE: In my humble opinion, the most important advantage of JDBC is its simplicity and versatility. The goal of the designers was to keep the API and common cases simple and "support the weird stuff in separate interfaces." Also, they wanted to use multiple methods for multiple functionality. They have achieved their goals even in this first version. For example, the Statement object has the executeQuery() method for SQL statements returning rows of data, and the executeUpdate() method for statements without data to return. Also, uncommon cases, such as statements returning multiple ResultSets, have a separate method: execute().