Java Enterprise in a Nutshell (In a Nutshell (OReilly))

8.4. Results

When a SQL query executes, the results form a pseudotable that contains all rows that fit the query criteria. For instance, here's a textual representation of the results of the query string "SELECT NAME, CUSTOMER_ID, PHONE FROM CUSTOMERS":

NAME CUSTOMER_ID PHONE -------------------------------- ----------- ------------------- Jane Markham 1 781 555-1212 Louis Smith 2 617 555-1213 Woodrow Lang 3 508 555-7171 Dr. John Smith 4 (011) 42 323-1239

This kind of textual representation is not very useful for Java programs. Instead, JDBC uses the java.sql.ResultSet interface to encapsulate the query results as Java primitive types and objects. You can think of a ResultSet as an object that represents an underlying table of query results, where you use method calls to navigate between rows and retrieve particular column values.

A Java program might handle the previous query as follows:

Statement stmt = con.createStatement( ); ResultSet rs = stmt.executeQuery( "SELECT NAME, CUSTOMER_ID, PHONE FROM CUSTOMERS"); while(rs.next( )) { System.out.print("Customer #" + rs.getString("CUSTOMER_ID")); System.out.print(", " + rs.getString("NAME")); System.out.println(", is at " + rs.getString("PHONE"); } rs.close( ); stmt.close( );

Here's the resulting output:

Customer #1, Jane Markham, is at 781 555-1212 Customer #2, Louis Smith, is at 617 555-1213 Customer #3, Woodrow Lang, is at 508 555-7171 Customer #4, Dr. John Smith, is at (011) 42 323-1239

The code loops through each row of the ResultSet using the next( ) method. When you start working with a ResultSet, you are positioned before the first row of results. That means you have to call next( ) once just to access the first row. Each time you call next( ), you move to the next row. If there are no more rows to read, next( ) returns false. Note that with the JDBC 1.0 ResultSet, you can only move forward through the results, and since there is no way to go back to the beginning, you can read them only once. Updateable ResultSets were introduced in JDBC 2.0, and we'll discuss them later.

Individual column values are read in our example using the getString( ) method. getString( ) is one of a family of getXXX( ) methods, each of which returns data of a particular type. Each getXXX( ) method has two versions: one that takes the case-insensitive String name of the column to be read (e.g., "PHONE", "CUSTOMER_ID") and one that takes a SQL-style column index. Note that column indexes run from 1 to n, unlike Java array indexes, which run from 0 to n - 1, where n is the number of columns.

The most important getXXX( ) method is getObject( ), which can return any kind of data packaged in an object wrapper. For example, calling getObject( ) on an integer field returns an Integer object, while calling it on a date field yields a java.sql.Date object. Table 8-1 lists the different getXXX( ) methods, along with the corresponding SQL data type and Java data type. Where the return type for a getXXX( ) method is different from the Java type, the return type is shown in parentheses. Note that the java.sql.Types class defines integer constants that represent the standard SQL data types .

Table 8-1. SQL data types, Java types, and default getXXX( ) methods

SQL data type

Java type

getXXX( ) method

CHAR

String

getString( )

VARCHAR

String

getString( )

LONGVARCHAR

String

getString( )

NUMERIC

java.math.BigDecimal

getBigDecimal( )

DECIMAL

java.math.BigDecimal

getBigDecimal( )

BIT

Boolean (boolean)

getBoolean( )

TINYINT

Integer (byte)

getByte( )

SMALLINT

Integer (short)

getShort( )

INTEGER

Integer (int)

getInt( )

BIGINT

Long (long)

getLong( )

REAL

Float (float)

getFloat( )

FLOAT

Double (double)

getDouble( )

DOUBLE

Double (double)

getDouble( )

BINARY

byte[]

getBytes( )

VARBINARY

byte[]

getBytes( )

LONGVARBINARY

byte[]

getBytes( )

DATE

java.sql.Date

getDate( )

TIME

java.sql.Time

getTime( )

TIMESTAMP

java.sql.Timestamp

getTimestamp( )

BLOB

java.sql.Blob

getBlob( )

CLOB

java.sql.Clob

getClob( )

Note that this table merely lists the default mappings according to the JDBC specification, and some drivers don't follow these mappings exactly. Also, a certain amount of casting is permitted. For instance, the getString( ) method returns a String representation of just about any data type.

8.4.1. Handling Nulls

Sometimes database columns contain null, or empty, values. However, because of the way certain database APIs are written, it is impossible for JDBC to provide a method to determine before the fact whether or not a column is null.[*] Methods that don't return an object of some sort are especially vulnerable. getInt( ), for instance, resorts to returning a value of -1. JDBC deals with this problem via the wasNull( ) method, which indicates whether or not the last column read was null:

[*] The driver can figure this out after reading the object, but since some driver implementations and database connection protocols allow you to reliably read a value from a column only once, implementing an isNull( ) method requires the ResultSet to cache the entire row in memory. While many programs do exactly this, it is not appropriate behavior for the lowest-level result handler.

int numberInStock = rs.getInt("STOCK"); if(rs.wasNull( )) System.out.println("Result was null"); else System.out.println("In Stock: " + numberInStock);

Alternately, you can call getObject( ) and test to see if the result is null:

Object numberInStock = rs.getObject("STOCK"); if(numberInStock == null) System.out.println("Result was null");

8.4.2. Large Data Types

You can retrieve large chunks of data from a ResultSet as a stream. This can be useful when reading images from a database or loading large documents from a data store, for example. The relevant ResultSet methods are getAsciiStream( ), getBinaryStream( ), and getCharacterStream( ); each method has column name and column index variants, just like the other getXXX( ) methods. Each of these methods returns an InputStream. Here's a code sample that retrieves an image from a PICTURES table and writes the image to an OutputStream of some kind (this might be a ServletOutputStream for a Java servlet that produces a GIF from a database):

ResultSet rs = stmt.executeQuery("SELECT IMAGE FROM PICTURES WHERE PID = " + req.getParameter("PID")); if (rs.next( )) { BufferedInputStream gifData = new BufferedInputStream(rs.getBinaryStream("IMAGE")); byte[] buf = new byte[4 * 1024]; // 4K buffer int len; while ((len = gifData.read(buf, 0, buf.length)) != -1) { out.write(buf, 0, len); } }

The JDBC 2.0 (and later) APIs include Blob and Clob objects to handle large data types ; we discuss these objects later in this chapter.

8.4.3. Dates and Times

JDBC defines three classes devoted to storing date and time information: java.sql.Date, java.sql.Time, and java.sql.Timestamp. These correspond to the SQL DATE, TIME, and TIMESTAMP types. The java.util.Date class is not suitable for any of them, so JDBC defines a new set of wrapper classes that extends (or limits) the standard Date class to fit the JDBC mold.

The SQL DATE type contains only a date, so the java.sql.Date class contains only a day, month, and year. SQL TIME (java.sql.Time) includes only a time of day, without date information. SQL TIMESTAMP (java.sql.Timestamp) includes both, but at nanosecond precision (the standard Date class is incapable of handling more than milliseconds).

Since different DBMS packages have different methods of encoding date and time information, JDBC supports the ISO date escape sequences, and individual drivers must translate these sequences into whatever form the underlying DBMS requires. The syntax for dates, times, and timestamps is:

{d 'yyyy-mm-dd'} {t 'hh:mm:ss'} {ts 'yyyy-mm-dd hh:mm:ss.ms.microseconds.ns'}

A TIMESTAMP needs to be specified only up to seconds; the remaining values are optional. Here is an example that uses a date escape sequence (where dateSQL is a Statement of some sort):

dateSQL.execute("INSERT INTO FRIENDS(BIRTHDAY) VALUES ({d '1978-12-14'})");

8.4.4. Advanced Results Handling

With JDBC 1.0, the functionality provided by the ResultSet interface is rather limited. There is no support for updates of any kind, and access to rows is limited to a single, sequential read (i.e., first row, second row, third row, and so on with no going back). As of 2.0, JDBC supports scrollable and updateable result sets , which allows for advanced record navigation and in-place data manipulation.

With scrolling, you can move forward and backward through the results of a query rather than just using the next( ) method to move to the next row. In terms of scrolling, there are now three distinct types of ResultSet objects: forward-only (as in JDBC 1.0), scroll-insensitive, and scroll-sensitive. A scroll-insensitive result set generally doesn't reflect changes to the underlying data while scroll-sensitive ones do. In fact, the number of rows in a sensitive result set doesn't even need to be fixed.

As of JDBC 2.0, result sets are also updateable. From this perspective, there are two different kinds of result sets: read-only result sets that don't allow changes to the underlying data and updateable result sets that allow such changes, subject to transaction limitations and so on.

To create an updateable, scroll-sensitive result set, we pass two extra arguments to the createStatement( ) method;

Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATEABLE);

If you don't pass any arguments to createStatement( ), you get a forward-only, read-only result set, just as you would using JDBC 1.0. Note that if you specify a scrollable result set (either sensitive or insensitive), you must also specify whether or not the result set is updateable. After you have created a scrollable ResultSet, use the methods listed in Table 8-2 to navigate through it. As with JDBC 1.0, when you start working with a ResultSet, you are positioned before the first row of results.

Table 8-2. JDBC 2.0 (and later) record-scrolling functions

Method

Function

first( )

Move to the first record.

last( )

Move to the last record.

next( )

Move to the next record.

previous( )

Move to the previous record.

beforeFirst( )

Move to immediately before the first record.

afterLast( )

Move to immediately after the last record.

absolute(int)

Move to an absolute row number. Takes a positive or negative argument.

relative(int)

Move backward or forward a specified number of rows. Takes a positive or negative argument.

The JDBC 2.0 API also includes a number of methods that tell you where you are in a ResultSet. You can think of your position in a ResultSet as the location of a cursor in the results. The isFirst( ) and isLast( ) methods return TRue if the cursor is located on the first or last record, respectively. isAfterLast( ) returns true if the cursor is after the last row in the result set while isBeforeFirst( ) returns true if the cursor is before the first row.

With an updateable ResultSet, you can change data in an existing row, insert an entirely new row, or delete an existing row. To change data in an existing row, use the new updateXXX( ) methods of ResultSet. Let's assume we want to update the CUSTOMER_ID field of the first row we retrieve (OK, it's a contrived example, but bear with us):

Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATEABLE); ResultSet rs = stmt.executeQuery( "SELECT NAME, CUSTOMER_ID FROM CUSTOMERS"); rs.first( ); rs.updateInt(2, 35243); rs.updateRow( );

Here we use first( ) to navigate to the first row of the result set and then call updateInt( ) to change the value of the customer ID column in the result set. After making the change, call updateRow( ) to actually make the change in the database. If you forget to call updateRow( ) before moving to another row in the result set, any changes you made are lost. If you need to make a number of changes in a single row, do so with multiple calls to updateXXX( ) methods and then a single call to updateRow( ). Just be sure you call updateRow( ) before moving on to another row.

The technique for inserting a row is similar to updating data in an existing row, with a few important differences. The first step is to move to what is called the insert row, using the moveToInsertRow( ) method. The insert row is a blank row associated with the ResultSet that contains all the fields, but no data; you can think of it as a pseudorow in which you can compose a new row. After you have moved to the insert row, use updateXXX( ) methods to load new data into the insert row and then call insertRow( ) to append the new row to the ResultSet and the underlying database. Here's an example that adds a new customer to the database:

ResultSet rs = stmt.executeQuery( "SELECT NAME, CUSTOMER_ID FROM CUSTOMERS"); rs.moveToInsertRow( ); rs.updateString(1, "Tom Flynn"); rs.updateInt(2, 35244); rs.insertRow( );

Note that you don't have to supply a value for every column, as long as the columns you omit can accept null values. If you don't specify a value for a column that can't be null, you'll get a SQLException. After you call insertRow( ), you can create another new row or you can move back to the ResultSet using the various navigation methods shown in Table 8-2. One final navigation method that isn't listed in the table is moveToCurrentRow( ). This method takes you back to where you were before you called moveToInsertRow( ); it can be called only while you are in the insert row.

Deleting a row from an updateable result set is easy. Simply move to the row you want to delete and call the deleteRow( ) method. Here's how to delete the last record in a ResultSet:

rs.last( ); rs.deleteRow( );

Calling deleteRow( ) also deletes the row from the underlying database.

Note that not all ResultSet objects are updateable. In general, the query must reference only a single table without any joins. Due to differences in database implementations, there is no single set of requirements for what makes an updateable ResultSet.

As useful as scrollable and updateable result sets are, the JDBC specification doesn't require driver vendors to support them. If you are building middleware or some other kind of system that requires interaction with a wide range of database drivers, you should avoid this functionality for the time being. The extended DatabaseMetaData object, introduced in JDBC 2.0, can provide information about scrolling and concurrency support .

8.4.5. Java-Aware Databases

Java is object-oriented; relational databases aren't. As a result, it's decidedly difficult to shoehorn a Java object into a stubbornly primitive-oriented database table. Luckily, the wind is changing, and newer database systems, including object-oriented database management systems (OODBMSs) provide direct support for storing and manipulating objects, and in some cases Java code as well. While a regular relational database can store only a limited number of primitive types, a JDBMS[*] system can store entire, arbitrary Java objects .

[*] A number of these products also use Java as a trigger language, generally in a JDBC structure.

Say we want to store a customized Java Account object in the ACCOUNTS table in a database. With a standard DBMS and JDBC 1.0, we have to pull each piece of data (such as account number, account holder, and balance) out of the Account object and write it to a complicated database table. To get data out, we reverse the process. Short of serializing the Account object and writing it to a binary field (a rather complex operation), we're stuck with this clumsy approach.

JDBC 2.0 extended the getObject( ) method to support these new Java-aware databases. Provided that the database supports a Java-object type, we can read the Account object just like any primitive type:

ResultSet rs = stmt.executeQuery("SELECT ACCOUNT FROM ACCOUNTS"); rs.next( ); Account a = (Account)rs.getObject(1);

To store an object, we use a PreparedStatement and the setObject( ) method:

Account a = new Account( ); // Fill in appropriate fields in Account object PreparedStatement stmt = con.prepareStatement( "INSERT INTO ACCOUNTS (ACCOUNT) VALUE (?)"); stmt.setObject(1, a); stmt.executeUpdate( );

A column that stores a Java object has a type of Types.JAVA_OBJECT. The JDBC API doesn't take any special steps to locate the bytecodes associated with any particular class, so you should make sure that any necessary objects can be instantiated with a call to Class.forName( ).

Категории