[ Team LiB ] Previous Section Next Section

Basic JDBC Features

In this chapter, we look at the core functionality provided by JDBC. WebLogic Server 8.1 supports the JDBC Specification version 2.0. We look first at the basic features offered by JDBC, and then examine the advanced functions in the subsequent sections.

We'll be working with the PointBase database as we look at the features offered by JDBC.

Most of the classes and interfaces that constitute the JDBC API can be found under the package java.sql. In this section, we look at how to use some of these classes. Any classes that are referred in the following sections without being qualified by a package name should be assumed to be in the java.sql package; for example, references to Connection imply the interface java.sql.Connection.

Connecting to the Database

The first step in performing any database task is connecting to the database. The phrases connecting to the database and obtaining a database connection amount to obtaining an object that implements the interface Connection in your Java code. You can obtain database connections locally (on the client side) using the DriverManager object.

Getting a Connection Using the DriverManager

As part of the initialization process, the DriverManager tries to load the driver classes provided to it using an environment variable called jdbc.drivers. This variable can contain one or more drivers for use with multiple data sources if necessary. Each driver class is loaded by the DriverManager class, which uses the appropriate driver to issue connections when its getConnection method is called subsequently. The driver used will depend on the url string passed to the DriverManager when you request a connection.


jdbc.drivers=com.pointbase.jdbc.jdbcUniversalDriver: jdbc.odbc.JdbcOdbcDriver

You may also load the driver object in your code by invoking the Class.forName() method and passing it the name of your driver class as a String. You don't need to instantiate the driver. The following line of code will initialize the DriverManager in your JVM with the driver for the PointBase database. For this to work, the driver class referenced should be available in your application CLASSPATH.


Class.forName( "com.pointbase.jdbc.jdbcUniversalDriver" );

As you can see, this call returns a class; however, you don't have to do anything with that returned class. Just by making the call, the driver class gets loaded into the JVM, and registers itself with the driver manager for the URL type that it can handle. In our case, the jdbcUniversalDriver class registers itself with the driver manager for all URLs beginning with jdbc:pointbase:server.

After the driver has been loaded, the DriverManager class can now be requested for the database connection by invoking its getConnection method. The getConnection method is provided three parameters: the URL to connect to, the user ID, and the password. The getConnection method signature is



public static Connection getConnection( String url, String userId, String password) throws
graphics/ccc.gif SQLException;

The url parameter that's passed to this method is of the form jdbc:subprotocol:subname. The subprotocol part of the url identifies the driver that will be used to connect to the database. The driver documentation generally tells you what the subprotocol should be in your url string for the driver manager to use that driver. The driver manager uses this parameter to identify which driver will satisfy the request.

The subname part of the url will also depend on the driver being used. The subname is generally passed to the driver by the DriverManager while obtaining a connection, and will provide information to the driver about the database or the data source to connect to. For instance, while using the JDBC-ODBC Bridge, the subprotocol is odbc. While accessing a database registered under the name test, using this protocol, you would pass the url string as jdbc:odbc:test.

While connecting to a PointBase database server using the driver com.pointbase.jdbc.jdbcUniversalDriver, the subprotocol string passed should be pointbase. The subname part of the url string identifies the server to connect to by using a string of the form server://<host>[:<port>]/<db>. As discussed earlier in this chapter, <host> is the host on which the server is running (for example, localhost). <port> is the port on which the server is listening. This is an optional value and must be specified only if the server has been started on a port other than 9092. <db> is the name of your database (for example, demo). Therefore, a complete url string to connect to a PointBase database named demo using a PointBase database server running on the default port in your localhost would be jdbc:pointbase:server://localhost/demo. Note that your PointBase server must be running for a connection to be returned successfully. The getConnection method will throw an exception of type SQLException if for any reason a connection cannot be obtained.

The following code snippet will obtain a connection for the PointBase database, assuming that host, port, dbName, userid, and password are passed in as parameters to this function, and that the PointBase server is running in the specified host:


Connection conn = null;
try
{
 Class.forName( "com.pointbase.jdbc.jdbcUniversalDriver" );
 String url = "jdbc:pointbase:server://" + host + ":" +
           port + "/" + dbName ;
 System.out.println("Using url string " + url );
 conn = DriverManager.getConnection
             (url, userid, password);
 System.out.println("Connection Obtained");
}
catch (Throwable t)
{
  t.printStackTrace();
  conn = null;
}
The Connection Interface

So, what is this Connection interface after all? It's an interface that provides you with methods that represent the operations you can perform using a database connection. The Connection object is the core object that enables you to work with the database.

Using a Connection object, you can create a new SQL statement, commit or roll back your database updates, and get and set several attributes of the connection such as auto-commit, transaction isolation level, and so forth. While performing operations on a connection, the database might generate SQL warnings. You can get a hold of these warnings by using the appropriate methods in the Connection object. This object also enables you to close the connection when you're done with it, thereby releasing the connection for use by other clients. You can also access the database's schema (metadata) by using the Connection object.

The program com.wlsunleashed.jdbc.Example1.java demonstrates the process of opening a connection and printing a few basic parameters of the connection. You can execute this class in your command line after starting the PointBase database server. Make sure to include the pbclient.jar file in your CLASSPATH while executing this example. It contains the PointBase JDBC driver. Remember to bring up the PointBase server instance prior to executing this example! You can execute the startPointBase.cmd file present under ${POINTBASE_HOME}/tools to start the demo database packaged with WebLogic Server.

Executing SQL Statements in a Database

After you establish a connection with the database, you can execute SQL statements on the database. The database engine executes your SQL statements. To send a SQL statement to the database engine, you must create a Statement object. A Statement object can be created by using an active Connection object. The following code snippet creates a Statement from a Connection object, which has been obtained from the DriverManager as described in the previous section:


Statement stmt = aConnection.createStatement();

After a Statement object has been created, you can execute your SQL statement. A SQL statement can be of two types: queries and updates. Depending on the type of SQL statement, you will use the appropriate method in the Statement object to execute it.

NOTE

Inserts, deletes, and data definition language statements qualify as updates.


Updating the Database

Let's first try to insert a new row into the ITEM table with attributes ID = 1, description = "Candy", stock = 50, and unit price = 2.00. To do this, you have to execute the following SQL statement:


INSERT INTO "XYZCONF"."ITEM"
 VALUES ( 1, "Candy", 50, 2.0 )

This statement can be executed in the database by invoking the executeUpdate method of the Statement object. Note that SQL statements executed using this method should not be terminated with a semicolon (;). You can execute both data definition (CREATE, ALTER TABLE, and so on) as well as data manipulation (INSERT, UPDATE, DELETE, and so on) language statements using the executeUpdate method. Assuming that the SQL statement has been stored in the string variable named anSQLStmt, the following code snippet will insert the values into the ITEM table:


stmt.executeUpdate( anSQLStmt );

The executeUpdate method returns the number of database rows affected as a result of executing the given statement. While executing DDL statements or other statements that do not affect any row, this method returns a zero. This method throws an exception of type SQLException if anything fails while executing the update. Your program might also want to scan any SQLWarnings that might have been generated by looking for them in the Connection object.

The sample file com.wlsunleashed.jdbc.Example2.java demonstrates inserting rows into the ITEM table by using the executeUpdate method. This example inserts five rows into the ITEM table. Execute this class in the XYZCONF schema created earlier. Make sure to include the pbclient.jar in your CLASSPATH while executing this class. Remember, you must have already created the schema using the schema.sql file, and the PointBase server should be up and running before you can invoke this class.

Querying from the Database

Now let's try to query the data that we inserted into the ITEM table in the previous section. The executeQuery method of the Statement object is used to execute a query. The difference between a query and an update SQL statement is that the query SQL returns data. This data is returned by the executeQuery method as a ResultSet.

Let's try to query the ITEM table for all the records in the table. This can be done by using the following SQL statement:


SELECT * FROM XYZCONF.ITEM

You can execute this SQL statement and retrieve the results by using the following code snippet, assuming that the SQL Statement has been stored in the variable called anSQLStmt:


ResultSet rs = stmt.executeQuery( anSQLStmt );

The executeQuery method returns the result from the query as a ResultSet. Like the executeUpdate method, this method also throws an exception of type SQLException when it is unable to execute the query for some reason. We will look at manipulating data using a ResultSet in the following section.

Processing Results from a Query

When a query has been executed using the executeQuery method, you get an object of type ResultSet that contains the result of the query in a tabular format—meaning rows and columns. We use the ResultSet object to simply iterate through the rows. Later, you'll see how to access a row by its position and how to iterate backward.

The ResultSet has a cursor that's initially positioned before the first row. To read the next row in the result set, you must advance the cursor by calling the next method on the ResultSet object, thus allowing you to read that row. However, columns within a row may be read in any order.

In the previous section, we executed the SELECT statement that fetched all the items in the ITEM table. Let's try to iterate through the rows fetched as a result of this SELECT and print them out. We will assume that we've already obtained the connection and we have an active connection in the conn variable. Consider the following code snippet:


1. Statement stmt = conn.createStatement();
2. ResultSet rs = stmt.executeQuery(anSQLStmt);
3. int row=1;
4. while (rs.next())
5. {
6.   System.out.println( "Row #" + row++ +
7.            ", id = " + rs.getInt(1) +
8.            ", desc = " + rs.getString(2) +
9.            ", stock = " + rs.getDouble(3) +
10.           ", unit price = " + rs.getDouble(4));
11. }
12. stmt.close();

The output of this code snippet looks like this:


Row #1, id = 1, desc = Candy, stock = 50.0, unit price = 2.0
Row #2, id = 2, desc = Crunchies, stock = 24.0, unit price = 1.4
Row #3, id = 3, desc = Snickers, stock = 40.0, unit price = 2.3
Row #4, id = 4, desc = Polo, stock = 55.0, unit price = 2.0
Row #5, id = 5, desc = Wafers, stock = 40.0, unit price = 2.55

As you can see, there are five rows in the ITEM table, which has been printed out by this code. In line 2, we execute the query and accept the output of the execution in our local variable rs, which is of type ResultSet. In line 4, we advance the cursor of rs to the first row in order to access that row. We do this in a while loop to make sure that we get to each row in the ResultSet. The call to rs.next() is inside the conditional block of the while loop. This is because the next method returns a boolean, indicating whether the cursor was successfully positioned on the next row. If the ResultSet runs out of rows, the next method returns false, thereby quitting from the loop.

After advancing the cursor to the first row, you can access the columns by using a variety of getXXX methods offered by the ResultSet interface, passing to it the index of the column. Unlike Java arrays, this index starts from 1. This can be seen in lines 7 through 10 in the code block. These indexes are the column numbers in the ResultSet, and not in the original table. For instance, if you had constructed your SELECT clause to query only for ITEM_DESCRIPTION and the UNIT_PRICE, you would access the item description by calling getString(1) and the unit price by calling getDouble(2).

There's also a second flavor of each of these getXXX methods that can be accessed by passing in the names of the columns. The column names that you pass to these methods are not case sensitive. These forms of the methods are meant to be used when you explicitly specify the column names in your SELECT clause. Thus, line 8 in the code, which retrieves the Item description, can be rewritten as follows:


", desc = " + rs.getString("ITEM_DESCRIPTION");

NOTE

Sun Microsystems' documentation states that for maximum portability, you must access the ResultSet columns within each row in the left-to-right order, and each column should be read only once.


These get methods convert the database type into the requested Java data type; for example, the method getString converts the value stored in the field into a java.lang.String and returns the converted value. Similarly, getDouble converts the double value stored in the database column and returns it as Java's double data type.

The ResultSet interface offers several get methods. JDBC offers lots of flexibility on which flavor of the get method you use to retrieve the data. For instance, you can invoke the method getString on a column, which is defined in the database as a double. You can also invoke the getDouble method on a VARCHAR column in the database, provided the data stored in the column can be legally cast into a double. If the method is unable to cast it legally, it throws an SQLException. In cases where you attempt to down cast the data, which results in data loss (for example, using a getInt on a column of type double when the data stored in the column is too big to fit in an int), the method throws an exception. All the database data types (except the SQL3 data types) can be legally fetched as a String by using the getString method.

The ResultSet interface also provides with a wasNull method, which tells your program whether the last column read from the ResultSet using a getXXX method had a special value of SQL NULL. The ResultSet interface also provides you with a getMetaData method, which returns an object of type ResultSetMetaData that provides information about the number, types, and properties of the columns within this ResultSet.

The ResultSet interface provides you with a close method, which you can call to explicitly close the ResultSet when you're done with it. However, you aren't required to do so. The ResultSet is automatically closed when the Statement object is either closed or used to execute another SQL statement.

CAUTION

Because a ResultSet is closed automatically when a Statement object is closed, it is important to remember that a design that uses a generic method to execute an SQL statement passed to it and returns the ResultSet for processing outside that method will not work if the Statement object is closed within that method.


The class file com.wlsunleashed.jdbc.Example3.java demonstrates the process of querying from the ITEM table, scanning the ResultSet, and printing out the values as discussed in this section.

Using PreparedStatement for Faster SQL Executions

In earlier sections, we saw how to execute updates in the database table by calling the executeUpdate(String) method. When you execute this call, the SQL statement is sent into the database engine, which compiles the SQL to bring it to a format that it can understand. Obviously, this compilation process is an overhead. For updates that don't happen very often, this might not be too bad. Consider the scenario in which XYZ Confectionaries increases the stock of all its items at regular intervals. Obviously, in such a case, the overhead of SQL compilation will definitely be felt because we have to execute UPDATE statements for each item, every time new stock is brought in.

JDBC offers a solution to this problem by way of PreparedStatements. A PreparedStatement is a precompiled SQL statement that can be executed without the overhead of compilation. The DBMS simply executes the prepared statement, thus providing faster responses. Prepared statements enable you to provide parameters that will be plugged into the statement at runtime. You can also execute queries using PreparedStatements.

Creating a PreparedStatement

As with the Statement object, the PreparedStatement object is created using an active Connection object. The difference between creating the two objects is that in the case of the PreparedStatement, you must specify the SQL statement at the time of creation rather than execution. To do this, you'll use the prepareStatement method of the Connection object. The following code snippet shows how a PreparedStatement can be obtained from a Connection object. We first define an SQL statement to update the stock in the ITEM table and then prepare the statement in line 3.


1. String anSQLStmt = "UPDATE XYZCONF.ITEM SET STOCK_QTY = " +
2.           " STOCK_QTY + ? WHERE ITEM_ID = ? " ;
3. PreparedStatement prepStmt = aConnection.prepareStatement( anSQLStmt );

As you can see in line 2, we increment the STOCK_QTY by ? for the item represented by ITEM_ID ?. So, what does that mean? This is how you tell the PreparedStatement object that you'll supply these values at runtime. The question mark (?) acts as a placeholder for the actual data, which will be supplied at runtime. When you call the prepareStatement method with this SQL, it provisions for the runtime data, compiles the SQL statement, and stores it in the prepStmt variable.

Executing a PreparedStatement

After a PreparedStatement has been created, you can execute it. But before you execute it, you must supply the actual data for the placeholders defined while creating the PreparedStatement. You can do so by calling corresponding setXXX methods in the PreparedStatement interface. If the value you want to substitute is an int, you'll call the setInt method of the interface. You'll normally find a setXXX method for each Java type. To substitute a placeholder with SQL NULL, you use the setNull method in the PreparedStatement interface.

Let's substitute the prepared statement stored in the variable prepStmt to increase the stock for the item with ID = 1 by 7.0. To do this, we'll use the following code fragment:


prepStmt.setDouble( 1, 7.0 ) ;
prepStmt.setInt( 2, 1 ) ;

The first argument to the setXXX method indicates the index of the placeholder that you want to replace. In the first line, we replaced the first placeholder to 7. In the second line, we replaced the second placeholder to 1. Now, the variable prepStmt represents the compiled version of the following SQL statement:


"UPDATE XYZCONF.ITEM SET STOCK_QTY = " +
" STOCK_QTY + 7.0 WHERE ITEM_ID = 1 " ;

All that's left is to execute this statement to send it to the RDBMS. This can be done by calling the executeUpdate method on the PreparedStatement. Unlike in the case of executing a Statement, we don't need to pass anything to the executeUpdate method. The following code fragment will execute this PreparedStatement for us:


prepStmt.executeUpdate() ;

Let's now increment the stock of item 3 by 7.0. Look at the following code fragment, which will do it for us:


prepStmt.setInt( 2, 3 ) ;
prepStmt.executeUpdate() ;

Notice in this code fragment that we didn't replace the placeholder for the STOCK_QTY field. This is because the prepared statement retains the value of each of the substitutions even after the statement has been executed. Because the increment value (7) was the same for item 3 as for item 1 earlier, we need not explicitly substitute the placeholder. In some cases, this behavior might not suit you. You can clear all the previous substitutions by calling the clearParameters method of the PreparedStatement interface.

The executeUpdate method returns the number of rows affected by executing the update. To execute a query using a PreparedStatement, use the executeQuery method. Again, this method takes no parameters and returns an object of type ResultSet. PreparedStatements need to be closed by calling the close method.

The following code fragment enables us to update the stock of each item in the ITEM table using a PreparedStatement. The array stockArray is passed as a parameter, which consists of the same number of rows as there are in the ITEM table. Each entry in this array is a double value. The stock of the item corresponding to the index of this value is incremented by this value.


String anSQLStmt = " UPDATE XYZCONF.ITEM SET " +
          " STOCK_QTY = STOCK_QTY + ? " +
          " WHERE ITEM_ID = ? " ;
try
{
   PreparedStatement stmt = conn.prepareStatement(anSQLStmt);
   for (int i = 0;i < stockArray.length; i++)
   {
     stmt.setDouble(1, stockArray[i]);
     stmt.setInt(2, i+1);
     System.out.println(" # Rows affected = " + stmt.executeUpdate());
   }
   stmt.close();
}
catch (SQLException t)
{
   t.printStackTrace();
}

Transactions

Assume that a customer walks into XYZ Confectionaries and wants to buy eight Snickers bars. Our billing system will check whether we have sufficient stock of this item to satisfy the customer's request. If the company has the stock, we'll enter a new bill in the system and reduce the stock of the item by eight. All these operations put together are known as a transaction. If any operation that's part of our transaction fails, we'll be unable to process the customer's request. In such a case, we'll have to undo any updates that we might have already made, and revert the data in the different tables to its original state to maintain integrity of the company's data.

A transaction is a set of one or more operations that are executed as an atomic unit so that we can guarantee that, at the end of the transaction, either all operations executed successfully or none of them did. In the case of the billing system for XYZ Confectionaries, we obviously don't want to reduce the stock of the item when the bill couldn't be entered into the system successfully, and vice versa.

Chapter 9, "Processing Transactions with the Java Transaction API," introduced you to the ACID properties of a transaction. These properties should be maintained at all times by any application that uses transactions. You can also read about local and global transactions in the same chapter.

JDBC provides mechanisms by which you can use transactions in your code while updating databases. As a matter of fact, no update operation that you perform in your code can be outside of a transaction. Of course, we didn't do anything in the inserts and updates in the previous sections that had anything to do with transactions—or did we?

When a connection to a database is created, the connection is set in what is known as an auto-commit mode; that is, each SQL statement that's executed using this connection is treated as a transaction in its own right and is committed after the statement has executed successfully. Thus, every insert or update that you execute is committed immediately, irrespective of what happens to any inserts or updates that might follow. However, as we've seen, this default behavior will not suit us all the time. To override this default behavior, we'll first have to disable the auto-commit mode of a connection before we can begin a transaction. This can be done by calling the setAutoCommit method on the Connection object as demonstrated in this code fragment:


aConnection.setAutoCommit( false );

This is technically the point at which your logical unit of work begins and you can continue to perform all the operations on the database that constitute your transaction. When you're done with all the operations, you can call the commit method on the Connection object if you're satisfied with the outcome of each participant of your transaction. If you feel any part of the transaction hasn't completed to satisfaction, you can rollback the entire transaction by calling the rollback method on the Connection object. After you either commit or roll back your transaction, the connection can be used to begin another transaction, if necessary. Irrespective of how the second transaction ends, the first transaction's changes are not affected.

NOTE

If you want to return the connection to auto-commit mode, you must explicitly call the setAutoCommit method and set it to true.


NOTE

The usage of setAutoCommit commit and rollback methods is sufficient for local transactions. However, for global transactions, you must use the JTA APIs. You can learn more about the JTA APIs in Chapter 9.


Thus, in our billing system example, the following code fragment will perform this transaction for us:


try
{
   aConnection.setAutoCommit( false );
   // check if we have sufficient stock of the item
   // insert a new bill
   // insert into the BILL_ITEM table
   // Update the stock of the item
   aConnection.commit();
}
catch (SQLException sqle)
{
   // handle the SQL exception
   aConnection.rollback();
}
finally
{
   // close the connection here
}

The source file com.wlsunleashed.jdbc.Example4.java demonstrates the process of adding a new bill into the billing system. It makes use of PreparedStatements for performing many of the updates. The items that are part of the bill are passed as parameters to the buyItem method of this class. The function checks the stock of each item, ensures enough quantity is available, inserts records into the BILL and BILL_ITEM tables, and finally updates the stock of each item by the quantity purchased.

Transaction Isolation Levels

The isolation property of transactions (the "I" in "ACID") warrants a little more discussion. There are a few scenarios that arise out of a database when multiple transactions are active at the same time. Let's take a look at these scenarios in this section.

  • Dirty reads— Consider that new stock for several items arrives in XYZ Confectionaries. An employee is updating the stock for each item. In the meanwhile, the company owner is printing a report of the current stock of different items. At the same time, the new stock is found to have some problems and the Purchasing department decides to return the entire stock to the supplier. Because of this, the employee entering the new stock information rolls back the work by calling the rollback method on the Connection object. Now the owner's report can possibly indicate an incorrect stock of the items, based on whether the stock value was queried after it was updated, but before the rollback was issued. This is known as a dirty read.

  • Nonrepeatable reads— Consider the following scenario. Two customers walk into XYZ Confectionaries and talk to two employees at the same time. Both require eight Snickers bars. Both employees begin their respective transactions and check the stock of Snickers bars, and find the stock to be 50. The first employee now enters the sale into the system and commits it. The second employee is still working on her transaction and queries the stock a second time. This time, she gets the stock as 42, which is different from the first time she queried the stock. Note that this is not a dirty read—the first employee has already committed her transaction. This is known as a nonrepeatable read.

  • Phantom reads— Phantom reads occur when new rows are added or rows are deleted from the table, but the side effects are still seen by other transactions. The Finance department of XYZ Confectionaries is trying to tally the sale for the day of all items with the stock in the ITEM table. The transaction is done totaling the sale amount from the BILL_ITEMS table. In the meanwhile, another sale occurs. Now when the stock from the ITEM table is totaled, it falls short because the new sale doesn't show up in the total of the BILL_ITEMS table. This is known as a phantom read.

These situations are possible, but can be prevented by the database by using locks. These locks can be set at different levels based on a setting known as the transaction isolation level. You can set the transaction isolation level to five different values based on how you want the database to behave in such situations. All values of the transaction isolation levels might not be supported by all databases. You must refer to the documentation of your database to see which levels are supported.

  • TRANSACTION_NONE— Transactions are not supported.

  • TRANSACTION_READ_UNCOMMITTED— This is the lowest level of isolation for databases that support transactions. This level of isolation allows dirty reads, nonrepeatable reads, and phantom reads, but offers the highest performance.

  • TRANSACTION_READ_COMMITTED— This guarantees that all queries return committed data (in other words, this level does not allow dirty reads). This level of transaction isolation is sufficient for most applications for normal functioning. This level of isolation does not guarantee repeatable reads and allows phantom reads.

  • TRANSACTION_REPEATABLE_READ— This isolation level prevents dirty reads and guarantees repeatable reads. However, this level does not prevent phantom reads.

  • TRANSACTION_SERIALIZABLE— This level of transaction isolation prevents dirty and phantom reads. It also guarantees repeatable reads. This offers the highest level of transaction isolation, but it also offers the lowest performance.

The transaction isolation level can be set in the Connection object by calling the method setTransactionIsolation as soon as you obtain the connection. You can also check the default value of the transaction isolation level for your database by calling the method getTransactionIsolation on the Connection object. The values of the different transaction isolation levels are available for use as public static constants in the Connection class. For example, the following code snippet sets the transaction isolation level to TRANSACTION_READ_COMMITTED:


aConnection.setTransactionIsolation( Connection.TRANSACTION_READ_COMMITTED );

SQL Error Handling

As you've already seen, any error that occurs while processing your SQL statements are reported to you as an SQLException. Each SQLException contains several pieces of information that can define the exact error for you. The SQLException object contains the following information:

  • A string that describes the error. You can get this string by calling the getMessage method of the exception object.

  • An SQLState string that follows the XOPEN SQLState specifications. You can access this string using the getSQLState method on the exception.

  • A vendor-specific error code. This code is usually the actual error code that's returned from the underlying database. You can access this code by calling the getErrorCode method on the exception object.

  • A chain to another SQLException object. You can access this exception by calling getNextException on the exception object. You can also set another SQLException object by calling the setNextException method to pass it up the call hierarchy.

Apart from exceptions, SQL executions can also result in warnings. For example, SQL can return a data truncation warning when data is truncated on an update or an insert. Such warnings indicate that the operation was successful, but the database wants you to be aware of something that happened that might affect your logic. These aren't reported as exceptions. These warnings are quietly tagged along with the appropriate object that caused this warning to be generated in the first place. Objects such as Connection, Statement, and ResultSet can all return SQLWarnings.

You can access the SQL warnings by calling the getWarnings method on the appropriate object. This returns the last created instance of SQLWarning. It's always a good idea to examine the warning list before you close the object. The SQLWarning class extends from SQLException; therefore, you have access to the same methods as when you get back exceptions. The only difference is that, in the case of warnings, you can get chained warnings by calling getNextWarning instead of the getNextException method.

    [ Team LiB ] Previous Section Next Section