Java Database
Java Database
Getting Started
The sample code that comes with this tutorial creates a database that is used by a proprietor of a small coffee house called The Coffee Break, where coffee beans are sold by the pound and brewed coffee is sold by the cup.
2
The following steps configure a JDBC development environment with which you can compile and run the tutorial samples: 1. Install the latest version of the Java SE SDK on your computer 2. Install your database management system (DBMS) if needed 3. Install a JDBC driver from the vendor of your database 4. Install Apache Ant 5. Install Apache Xalan 6. Download the sample code 7. Modify the build.xml file 8. Modify the tutorial properties file 9. Compile and package the samples 10.Create databases, tables, and populate tables 11.Run the samples
3
Embedded driver and a Network Client Driver. MySQL Connector/J is a Type 4 driver. Installing a JDBC driver generally consists of copying the driver to your computer, then adding the location of it to your class path. In addition, many JDBC drivers other than Type 4 drivers require you to install a client-side API. No other special configuration is usually needed.
4
JdbcRowSetSample.java JDBCTutorialUtilities.java JoinSample.java ProductInformationTable.java RSSFeedsTable.java StateFilter.java StoredProcedureJavaDBSample.java StoredProcedureMySQLSample.java SuppliersTable.java WebRowSetSample.java txt colombian-description.txt xml rss-coffee-industry-news.xml rss-the-coffee-break-blog.xml build.xml Create a directory to contain all the files of the sample. These steps refer to this directory as <JDBC tutorial directory>. Unzip the contents of JDBCTutorial.zip into <JDBC tutorial directory>.
Modify build.xml
In the build.xml file, modify the property ANTPROPERTIES to refer to either properties/javadb-build-properties.xml or properties/mysql-buildproperties.xml, depending on your DBMS. For example, if you are using Java DB, your build.xml file would contain this:
<property name="ANTPROPERTIES" value="properties/javadb-build-properties.xml"/> <import file="${ANTPROPERTIES}"/>
Similarly, if you are using MySQL, your build.xml file would contain this:
<property name="ANTPROPERTIES" value="properties/mysql-build-properties.xml"/> <import file="${ANTPROPERTIES}"/>
5
Property JAVAC JAVA Description The full path name of your Java compiler, javac The full path name of your Java runtime executable, java The name of the properties file, either properties/javadbPROPERTIESFILE sample-properties.xml or properties/mysql-sampleproperties.xml The full path name of your MySQL driver. For Connector/J, this is typically <Connector/J installation MYSQLDRIVER directory>/mysql-connector-java-versionnumber.jar. The full path name of your Java DB driver. This is typically <Java JAVADBDRIVER DB installation directory>/lib/derby.jar. XALANDIRECTORY The full path name of the directory that contains Apache Xalan. The class path that the JDBC tutorial uses. You do not need to change CLASSPATH this value. XALAN The full path name of the file xalan.jar. A value of either derby or mysql depending on whether you are using Java DB or MySQL, respectively. The tutorial uses this value to DB.VENDOR construct the URL required to connect to the DBMS and identify DBMS-specific code and SQL statements. The fully qualified class name of the JDBC driver. For Java DB, this is DB.DRIVER org.apache.derby.jdbc.EmbeddedDriver. For MySQL, this is com.mysql.jdbc.Driver. DB.HOST The host name of the computer hosting your DBMS. DB.PORT The port number of the computer hosting your DBMS. DB.SID The name of the database the tutorial creates and uses. DB.URL.NEWDATABASE The connection URL used to connect to your DBMS when creating a new database. You do not need to change this value. The connection URL used to connect to your DBMS. You do not need DB.URL to change this value. DB.USER The name of the user that has access to create databases in the DBMS. DB.PASSWORD The password of the user specified in DB.USER. The character used to separate SQL statements. Do not change this DB.DELIMITER value. It should be the semicolon character (;).
6
jar_file driver The full path name of the JAR file that contains all the class files of this tutorial. The fully qualified class name of the JDBC driver. For Java DB, this is org.apache.derby.jdbc.EmbeddedDriver. For MySQL, this is com.mysql.jdbc.Driver.
database_n The name of the database the tutorial creates and uses. ame user_name The name of the user that has access to create databases in the DBMS. password The password of the user specified in user_name. server_nam The host name of the computer hosting your DBMS. e port_numbe The port number of the computer hosting your DBMS. r Note: For simplicity in demonstrating the JDBC API, the JDBC tutorial sample code does not perform the password management techniques that a deployed system normally uses. In a production environment, you can follow the Oracle Database password management guidelines and disable any sample accounts. See the section Securing Passwords in Application Design in Managing Security for Application Developers in Oracle Database Security Guide for password management guidelines and other security recommendations.
Note: No corresponding Ant target exists in the build.xml file that creates a database for Java DB. The database URL for Java DB, which is used to establish a database connection, includes the option to create the database (if it does not already exist). See Establishing a Connection for more information. If you are using either Java DB or MySQL, then from the same directory, run the following command to delete existing sample database tables, recreate the tables, and populate them. For Java DB, this command also creates the database if it does not already exist:
ant setup
Note: You should run the command ant setup every time before you run one of the Java classes in the sample. Many of these samples expect specific data in the contents of the sample's database tables.
7
mysql-create-procedure run runct runst runjrs runcrs runjoin mysql/createprocedures.sql. JDBCTutorialUtilities CoffeesTable SuppliersTable JdbcRowSetSample CachedRowSetSample, ExampleRowSetListener JoinSample No other required files No other required classes JDBCTutorialUtilities JDBCTutorialUtilities JDBCTutorialUtilities JDBCTutorialUtilities
JDBCTutorialUtilities JDBCTutorialUtilities, runfrs FilteredRowSetSample CityFilter, StateFilter runwrs WebRowSetSample JDBCTutorialUtilities JDBCTutorialUtilities, runclob ClobSample txt/colombiandescription.txt JDBCTutorialUtilities, runrss RSSFeedsTable XML files contained in the x directory rundl DatalinkSample JDBCTutorialUtilities JDBCTutorialUtilities, runspjavadb StoredProcedureJavaDBSample SuppliersTable, CoffeesTable JDBCTutorialUtilities, runspmysql StoredProcedureMySQLSample SuppliersTable, CoffeesTable JDBCTutorialUtilities, runframe CoffeesFrame CoffeesTableModel For example, to run the class CoffeesTable, change the current directory to <JDBC tutorial directory>, and from this directory, run the following command:
ant runct
8
"SALES, TOTAL " + "from " + dbName + ".COFFEES";
try { stmt = con.createStatement(); ResultSet rs = stmt.executeQuery(query); while (rs.next()) { String coffeeName = rs.getString("COF_NAME"); int supplierID = rs.getInt("SUP_ID"); float price = rs.getFloat("PRICE"); int sales = rs.getInt("SALES"); int total = rs.getInt("TOTAL"); System.out.println(coffeeName + "\t" + supplierID + "\t" + price + "\t" + sales + "\t" + total); } } catch (SQLException e ) { JDBCTutorialUtilities.printSQLException(e); } finally { if (stmt != null) { stmt.close(); } }
Establishing Connections
First, establish a connection with the data source you want to use. A data source can be a DBMS, a legacy file system, or some other source of data with a corresponding JDBC driver. This connection is represented by a Connection object. See Establishing a Connection for more information.
Creating Statements
A Statement is an interface that represents a SQL statement. You execute Statement objects, and they generate ResultSet objects, which is a table of data representing a database result set. You need a Connection object to create a Statement object. For example, CoffeesTables.viewTable creates a Statement object with the following code:
stmt = con.createStatement();
There are three different kinds of statements: Statement: Used to implement simple SQL statements with no parameters. PreparedStatement: (Extends Statement.) Used for precompiling SQL statements that might contain input parameters. See Using Prepared Statements for more information. CallableStatement: (Extends PreparedStatement.) Used to execute stored procedures that may contain both input and output parameters. See Stored Procedures for more information.
Executing Queries
To execute a query, call an execute method from Statement such as the following: execute: Returns true if the first object that the query returns is a ResultSet object. Use this method if the query could return one or more ResultSet objects. Retrieve the ResultSet objects returned from the query by repeatedly calling Statement.getResultSet. executeQuery: Returns one ResultSet object. executeUpdate: Returns an integer representing the number of rows affected by the SQL statement. Use this method if you are using INSERT, DELETE, or UPDATE SQL statements. For example, CoffeesTables.viewTable executed a Statement object with the following
9
code:
ResultSet rs = stmt.executeQuery(query);
See Retrieving and Modifying Values from Result Sets for more information.
See Retrieving and Modifying Values from Result Sets for more information.
Closing Connections
When you are finished using a Statement, call the method Statement.close to immediately release the resources it is using. When you call this method, its ResultSet objects are closed. For example, the method CoffeesTables.viewTable ensures that the Statement object is closed at the end of the method, regardless of any SQLException objects thrown, by wrapping it in a finally block:
} finally { if (stmt != null) { stmt.close(); } }
JDBC throws an SQLException when it encounters an error during an interaction with a data source. See Handling SQL Exceptions for more information. In JDBC 4.1, which is available in Java SE release 7 and later, you can use a try-with-resources statement to automatically close Connection, Statement, and ResultSet objects, regardless of whether an SQLException has been thrown. An automatic resource statement consists of a try statement and one or more declared resources. For example, you can modify CoffeesTables.viewTable so that its Statement object closes automatically, as follows:
public static void viewTable(Connection con) throws SQLException { String query = "select COF_NAME, SUP_ID, PRICE, " + "SALES, TOTAL " + "from COFFEES"; try (Statement stmt = con.createStatement()) { ResultSet rs = stmt.executeQuery(query);
10
while (rs.next()) { String coffeeName = rs.getString("COF_NAME"); int supplierID = rs.getInt("SUP_ID"); float price = rs.getFloat("PRICE"); int sales = rs.getInt("SALES"); int total = rs.getInt("TOTAL"); System.out.println(coffeeName + ", " + supplierID + ", " + price + ", " + sales + ", " + total); } } catch (SQLException e) { JDBCTutorialUtilities.printSQLException(e); }
The following statement is an try-with-resources statement, which declares one resource, stmt, that will be automatically closed when the try block terminates:
try (Statement stmt = con.createStatement()) { // ... }
See The try-with-resources Statement in the Essential Classes trail for more information.
Establishing a Connection
First, you need to establish a connection with the data source you want to use. A data source can be a DBMS, a legacy file system, or some other source of data with a corresponding JDBC driver. Typically, a JDBC application connects to a target data source using one of two classes: DriverManager: This fully implemented class connects an application to a data source, which is specified by a database URL. When this class first attempts to establish a connection, it automatically loads any JDBC 4.0 drivers found within the class path. Note that your application must manually load any JDBC drivers prior to version 4.0. DataSource: This interface is preferred over DriverManager because it allows details about the underlying data source to be transparent to your application. A DataSource object's properties are set so that it represents a particular data source. See Connecting with DataSource Objects for more information. For more information about developing applications with the DataSource class, see the latest The Java EE Tutorial. Note: The samples in this tutorial use the DriverManager class instead of the DataSource class because it is easier to use and the samples do not require the features of the DataSource class. This page covers the following topics: Using the DriverManager Class Specifying Database Connection URLs
11
if (this.dbms.equals("mysql")) { conn = DriverManager.getConnection( "jdbc:" + this.dbms + "://" + this.serverName + ":" + this.portNumber + "/", connectionProps); } else if (this.dbms.equals("derby")) { conn = DriverManager.getConnection( "jdbc:" + this.dbms + ":" + this.dbName + ";create=true", connectionProps); } System.out.println("Connected to database"); return conn; }
The method DriverManager.getConnection establishes a database connection. This method requires a database URL, which varies depending on your DBMS. The following are some examples of database URLs: 1. MySQL: jdbc:mysql://localhost:3306/, where localhost is the name of the server hosting your database, and 3306 is the port number 2. Java DB: jdbc:derby:testdb;create=true, where testdb is the name of the database to connect to, and create=true instructs the DBMS to create the database. Note: This URL establishes a database connection with the Java DB Embedded Driver. Java DB also includes a Network Client Driver, which uses a different URL. This method specifies the user name and password required to access the DBMS with a Properties object. Note: Typically, in the database URL, you also specify the name of an existing database to which you want to connect. For example, the URL jdbc:mysql://localhost:3306/mysql represents the database URL for the MySQL database named mysql. The samples in this tutorial use a URL that does not specify a specific database because the samples create a new database. In previous versions of JDBC, to obtain a connection, you first had to initialize your JDBC driver by calling the method Class.forName. This methods required an object of type java.sql.Driver. Each JDBC driver contains one or more classes that implements the interface java.sql.Driver. The drivers for Java DB are org.apache.derby.jdbc.EmbeddedDriver and org.apache.derby.jdbc.ClientDriver, and the one for MySQL Connector/J is com.mysql.jdbc.Driver. See the documentation of your DBMS driver to obtain the name of the class that implements the interface java.sql.Driver. Any JDBC 4.0 drivers that are found in your class path are automatically loaded. (However, you must manually load any drivers prior to JDBC 4.0 with the method Class.forName.) The method returns a Connection object, which represents a connection with the DBMS or a specific database. Query the database through this object.
subsubprotocol specifies where Java DB should search for the database, either in a directory, in memory, in a class path, or in a JAR file. It is typically omitted. databaseName is the name of the database to connect to. attribute=value represents an optional, semicolon-separated list of attributes. These attributes enable you to instruct Java DB to perform various tasks, including the following: Create the database specified in the connection URL. Encrypt the database specified in the connection URL. Specify directories to store logging and trace information. Specify a user name and password to connect to the database. See Java DB Developer's Guide and Java DB Reference Manual from Java DB Technical Documentation for more information.
host:port is the host name and port number of the computer hosting your database. If not specified, the default values of host and port are 127.0.0.1 and 3306, respectively. database is the name of the database to connect to. If not specified, a connection is made with no default database. failover is the name of a standby database (MySQL Connector/J supports failover). propertyName=propertyValue represents an optional, ampersand-separated list of properties. These attributes enable you to instruct MySQL Connector/J to perform various tasks. See MySQL Reference Manual for more information.
13
Deploying Other DataSource Implementations Getting and Using Pooled Connections Deploying Distributed Transactions Using Connections for Distributed Transactions
14
1. Creating an instance of the DataSource class 2. Setting its properties 3. Registering it with a naming service that uses the Java Naming and Directory Interface (JNDI) API First, consider the most basic case, which is to use a basic implementation of the DataSource interface, that is, one that does not support connection pooling or distributed transactions. In this case there is only one DataSource object that needs to be deployed. A basic implementation of DataSource produces the same kind of connections that the DriverManager class produces.
The variable ds now represents the database CUSTOMER_ACCOUNTS installed on the server. Any connection produced by the BasicDataSource object ds will be a connection to the database CUSTOMER_ACCOUNTS.
Registering DataSource Object with Naming Service That Uses JNDI API
With the properties set, the system administrator can register the BasicDataSource object with a JNDI (Java Naming and Directory Interface) naming service. The particular naming service that is used is usually determined by a system property, which is not shown here. The following code excerpt registers the BasicDataSource object and binds it with the logical name jdbc/billingDB:
Context ctx = new InitialContext(); ctx.bind("jdbc/billingDB", ds);
This code uses the JNDI API. The first line creates an InitialContext object, which serves as the starting point for a name, similar to root directory in a file system. The second line associates, or binds, the BasicDataSource object ds to the logical name jdbc/billingDB. In the next code excerpt, you give the naming service this logical name, and it returns the BasicDataSource object. The logical name can be any string. In this case, the company decided to use the name billingDB as the logical name for the CUSTOMER_ACCOUNTS database. In the previous example, jdbc is a subcontext under the initial context, just as a directory under the root directory is a subdirectory. The name jdbc/billingDB is like a path name, where the last item in the path is analogous to a file name. In this case, billingDB is the logical name that is given to the BasicDataSource object ds. The subcontext jdbc is reserved for logical names to be bound to DataSource objects, so jdbc will always be the first part of a logical name for a data source.
15
programmer to use. This means that a programmer can give the logical data source name that was bound to an instance of a DataSource class, and the JNDI naming service will return an instance of that DataSource class. The method getConnection can then be called on that DataSource object to get a connection to the data source it represents. For example, a programmer might write the following two lines of code to get a DataSource object that produces a connection to the database CUSTOMER_ACCOUNTS.
Context ctx = new InitialContext(); DataSource ds = (DataSource)ctx.lookup("jdbc/billingDB");
The first line of code gets an initial context as the starting point for retrieving a DataSource object. When you supply the logical name jdbc/billingDB to the method lookup, the method returns the DataSource object that the system administrator bound to jdbc/billingDB at deployment time. Because the return value of the method lookup is a Java Object, we must cast it to the more specific DataSource type before assigning it to the variable ds. The variable ds is an instance of the class com.dbaccess.BasicDataSource that implements the DataSource interface. Calling the method ds.getConnection produces a connection to the CUSTOMER_ACCOUNTS database.
Connection con = ds.getConnection("fernanda","brewed");
The getConnection method requires only the user name and password because the variable ds has the rest of the information necessary for establishing a connection with the CUSTOMER_ACCOUNTS database, such as the database name and location, in its properties.
16
method DataSource.getConnection on the DataSource object and get a pooled connection. This connection will be to the data source specified in the ConnectionPoolDataSource object's properties. The following example describes how a system administrator for The Coffee Break would deploy a DataSource object implemented to provide pooled connections. The system administrator would typically use a deployment tool, so the code fragments shown in this section are the code that a deployment tool would execute. To get better performance, The Coffee Break company has bought a JDBC driver from DB Access, Inc. that includes the class com.dbaccess.ConnectionPoolDS, which implements the ConnectionPoolDataSource interface. The system administrator creates create an instance of this class, sets its properties, and registers it with a JNDI naming service. The Coffee Break has bought its DataSource class, com.applogic.PooledDataSource, from its EJB server vendor, Application Logic, Inc. The class com.applogic.PooledDataSource implements connection pooling by using the underlying support provided by the ConnectionPoolDataSource class com.dbaccess.ConnectionPoolDS. The ConnectionPoolDataSource object must be deployed first. The following code creates an instance of com.dbaccess.ConnectionPoolDS and sets its properties:
com.dbaccess.ConnectionPoolDS cpds = new com.dbaccess.ConnectionPoolDS(); cpds.setServerName("creamer"); cpds.setDatabaseName("COFFEEBREAK"); cpds.setPortNumber(9040); cpds.setDescription("Connection pooling for " + "COFFEEBREAK DBMS");
After the ConnectionPoolDataSource object has been deployed, the system administrator deploys the DataSource object. The following code registers the com.dbaccess.ConnectionPoolDS object cpds with a JNDI naming service. Note that the logical name being associated with the cpds variable has the subcontext pool added under the subcontext jdbc, which is similar to adding a subdirectory to another subdirectory in a hierarchical file system. The logical name of any instance of the class com.dbaccess.ConnectionPoolDS will always begin with jdbc/pool. Oracle recommends putting all ConnectionPoolDataSource objects under the subcontext jdbc/pool:
Context ctx = new InitialContext(); ctx.bind("jdbc/pool/fastCoffeeDB", cpds);
Next, the DataSource class that is implemented to interact with the cpds variable and other instances of the com.dbaccess.ConnectionPoolDS class is deployed. The following code creates an instance of this class and sets its properties. Note that only two properties are set for this instance of com.applogic.PooledDataSource. The description property is set because it is always required. The other property that is set, dataSourceName, gives the logical JNDI name for cpds, which is an instance of the com.dbaccess.ConnectionPoolDS class. In other words, cpds represents the ConnectionPoolDataSource object that will implement connection pooling for the DataSource object. The following code, which would probably be executed by a deployment tool, creates a PooledDataSource object, sets its properties, and binds it to the logical name jdbc/fastCoffeeDB:
com.applogic.PooledDataSource ds = new com.applogic.PooledDataSource(); ds.setDescription("produces pooled connections to COFFEEBREAK"); ds.setDataSourceName("jdbc/pool/fastCoffeeDB"); Context ctx = new InitialContext(); ctx.bind("jdbc/fastCoffeeDB", ds);
17
At this point, a DataSource object is deployed from which an application can get pooled connections to the database COFFEEBREAK.
The variable ds represents a DataSource object that produces pooled connections to the database COFFEEBREAK. You need to retrieve this DataSource object only once because you can use it to produce as many pooled connections as needed. Calling the method getConnection on the ds variable automatically produces a pooled connection because the DataSource object that the ds variable represents was configured to produce pooled connections. Connection pooling is generally transparent to the programmer. There are only two things you need to do when you are using pooled connections: 1. Use a DataSource object rather than the DriverManager class to get a connection. In the following line of code, ds is a DataSource object implemented and deployed so that it will create pooled connections and username and password are variables that represent the credentials of the user that has access to the database:
Connection con = ds.getConnection(username, password);
2. Use a finally statement to close a pooled connection. The following finally block would appear after the try/catch block that applies to the code in which the pooled connection was used:
try { Connection con = ds.getConnection(username, password); // ... code to use the pooled // connection con } catch (Exception ex { // ... code to handle exceptions } finally { if (con != null) con.close(); }
Otherwise, an application using a pooled connection is identical to an application using a regular connection. The only other thing an application programmer might notice when connection pooling is being done is that performance is better. The following sample code gets a DataSource object that produces connections to the database COFFEEBREAK and uses it to update a price in the table COFFEES:
18
import import import import java.sql.*; javax.sql.*; javax.ejb.*; javax.naming.*;
public class ConnectionPoolingBean implements SessionBean { // ... public void ejbCreate() throws CreateException { ctx = new InitialContext(); ds = (DataSource)ctx.lookup("jdbc/fastCoffeeDB"); } public void updatePrice(float price, String cofName, String username, String password) throws SQLException{ Connection con; PreparedStatement pstmt; try { con = ds.getConnection(username, password); con.setAutoCommit(false); pstmt = con.prepareStatement("UPDATE COFFEES " + "SET PRICE = ? " + "WHERE COF_NAME = ?"); pstmt.setFloat(1, price); pstmt.setString(2, cofName); pstmt.executeUpdate(); con.commit(); pstmt.close(); } finally { if (con != null) con.close(); }
The connection in this code sample participates in connection pooling because the following are true: An instance of a class implementing ConnectionPoolDataSource has been deployed. An instance of a class implementing DataSource has been deployed, and the value set for its dataSourceName property is the logical name that was bound to the previously deployed ConnectionPoolDataSource object. Note that although this code is very similar to code you have seen before, it is different in the following ways: It imports the javax.sql, javax.ejb, and javax.naming packages in addition to java.sql. The DataSource and ConnectionPoolDataSource interfaces are in the javax.sql package, and the JNDI constructor InitialContext and method Context.lookup are part of the javax.naming package. This particular example code is in the form of an EJB component that uses API from the javax.ejb package. The
19
purpose of this example is to show that you use a pooled connection the same way you use a nonpooled connection, so you need not worry about understanding the EJB API. It uses a DataSource object to get a connection instead of using the DriverManager facility. It uses a finally block to ensure that the connection is closed. Getting and using a pooled connection is similar to getting and using a regular connection. When someone acting as a system administrator has deployed a ConnectionPoolDataSource object and a DataSource object properly, an application uses that DataSource object to get a pooled connection. An application should, however, use a finally block to close the pooled connection. For simplicity, the preceding example used a finally block but no catch block. If an exception is thrown by a method in the try block, it will be thrown by default, and the finally clause will be executed in any case.
The following code registers the com.dbaccess.XATransactionalDS object xads with a JNDI naming service. Note that the logical name being associated with xads has the subcontext xa added under jdbc. Oracle recommends that the logical name of any instance of the class com.dbaccess.XATransactionalDS always begin with jdbc/xa.
Context ctx = new InitialContext(); ctx.bind("jdbc/xa/distCoffeeDB", xads);
Next, the DataSource object that is implemented to interact with xads and other XADataSource objects is deployed. Note that the DataSource class, com.applogic.TransactionalDS, can work with an XADataSource class from any JDBC driver vendor. Deploying the DataSource object involves creating an instance of the com.applogic.TransactionalDS class and setting its properties. The dataSourceName property is set to jdbc/xa/distCoffeeDB, the logical name associated with com.dbaccess.XATransactionalDS. This is the XADataSource class that implements the distributed transaction capability for the DataSource class. The following code deploys an instance of the DataSource class:
com.applogic.TransactionalDS ds = new com.applogic.TransactionalDS(); ds.setDescription("Produces distributed transaction " +
20
"connections to COFFEEBREAK"); ds.setDataSourceName("jdbc/xa/distCoffeeDB"); Context ctx = new InitialContext(); ctx.bind("jdbc/distCoffeeDB", ds);
Now that instances of the classes com.applogic.TransactionalDS and com.dbaccess.XATransactionalDS have been deployed, an application can call the method getConnection on instances of the TransactionalDS class to get a connection to the COFFEEBREAK database that can be used in distributed transactions.
There are some minor but important restrictions on how this connection is used while it is part of a distributed transaction. A transaction manager controls when a distributed transaction begins and when it is committed or rolled back; therefore, application code should never call the methods Connection.commit or Connection.rollback. An application should likewise never call Connection.setAutoCommit(true), which enables the auto-commit mode, because that would also interfere with the transaction manager's control of the transaction boundaries. This explains why a new connection that is created in the scope of a distributed transaction has its autocommit mode disabled by default. Note that these restrictions apply only when a connection is participating in a distributed transaction; there are no restrictions while the connection is not part of a distributed transaction. For the following example, suppose that an order of coffee has been shipped, which triggers updates to two tables that reside on different DBMS servers. The first table is a new INVENTORY table, and the second is the COFFEES table. Because these tables are on different DBMS servers, a transaction that involves both of them will be a distributed transaction. The code in the following example, which obtains a connection, updates the COFFEES table, and closes the connection, is the second part of a distributed transaction. Note that the code does not explicitly commit or roll back the updates because the scope of the distributed transaction is being controlled by the middle tier server's underlying system infrastructure. Also, assuming that the connection used for the distributed transaction is a pooled connection, the application uses a finally block to close the connection. This guarantees that a valid connection will be closed even if an exception is thrown, thereby ensuring that the connection is returned to the connection pool to be recycled. The following code sample illustrates an enterprise Bean, which is a class that implements the methods that can be called by a client computer. The purpose of this example is to demonstrate that application code for a distributed transaction is no different from other code except that it does not call the Connection methods commit, rollback, or setAutoCommit(true). Therefore, you do not need to worry about understanding the EJB API that is used.
import import import import java.sql.*; javax.sql.*; javax.ejb.*; javax.naming.*;
21
public class DistributedTransactionBean implements SessionBean { // ... public void ejbCreate() throws CreateException { ctx = new InitialContext(); ds = (DataSource)ctx.lookup("jdbc/distCoffeesDB"); } public void updateTotal(int incr, String cofName, String username, String password) throws SQLException { Connection con; PreparedStatement pstmt; try { con = ds.getConnection(username, password); pstmt = con.prepareStatement("UPDATE COFFEES " + "SET TOTAL = TOTAL + ? " + "WHERE COF_NAME = ?"); pstmt.setInt(1, incr); pstmt.setString(2, cofName); pstmt.executeUpdate(); stmt.close(); } finally { if (con != null) con.close(); }
Handling SQLExceptions
This page covers the following topics: Overview of SQLException Retrieving Exceptions Retrieving Warnings Categorized SQLExceptions Other Subclasses of SQLException
Overview of SQLException
When JDBC encounters an error during an interaction with a data source, it throws an instance of SQLException as opposed to Exception. (A data source in this context represents the database to which a Connection object is connected.) The SQLException instance contains the following information that can help you determine the cause of the error: A description of the error. Retrieve the String object that contains this description by calling the method SQLException.getMessage. A SQLState code. These codes and their respective meanings have been standardized by ISO/ANSI and Open Group (X/Open), although some codes have been reserved for database vendors to define for themselves. This String object consists of five alphanumeric characters. Retrieve this code by calling the method SQLException.getSQLState.
22
An error code. This is an integer value identifying the error that caused the SQLException instance to be thrown. Its value and meaning are implementation-specific and might be the actual error code returned by the underlying data source. Retrieve the error by calling the method SQLException.getErrorCode. A cause. A SQLException instance might have a causal relationship, which consists of one or more Throwable objects that caused the SQLException instance to be thrown. To navigate this chain of causes, recursively call the method SQLException.getCause until a null value is returned. A reference to any chained exceptions. If more than one error occurs, the exceptions are referenced through this chain. Retrieve these exceptions by calling the method SQLException.getNextException on the exception that was thrown.
Retrieving Exceptions
The following method, JDBCTutorialUtilities.printSQLException outputs the SQLState, error code, error description, and cause (if there is one) contained in the SQLException as well as any other exception chained to it:
public static void printSQLException(SQLException ex) { for (Throwable e : ex) { if (e instanceof SQLException) { if (ignoreSQLException( ((SQLException)e). getSQLState()) == false) { e.printStackTrace(System.err); System.err.println("SQLState: " + ((SQLException)e).getSQLState()); System.err.println("Error Code: " + ((SQLException)e).getErrorCode()); System.err.println("Message: " + e.getMessage()); Throwable t = ex.getCause(); while(t != null) { System.out.println("Cause: " + t); t = t.getCause(); } } } } }
For example, if you call the method CoffeesTable.dropTable with Java DB as your DBMS, the table COFFEES does not exist, and you remove the call to JDBCTutorialUtilities.ignoreSQLException, the output will be similar to the following:
SQLState: 42Y55 Error Code: 30000 Message: 'DROP TABLE' cannot be performed on 'TESTDB.COFFEES' because it does not exist.
Instead of outputting SQLException information, you could instead first retrieve the SQLState then process the SQLException accordingly. For example, the method JDBCTutorialUtilities.ignoreSQLException returns true if the SQLState> is equal to code 42Y55 (and you are using Java DB as your DBMS), which causes
23
JDBCTutorialUtilities.printSQLException to ignore the SQLException:
public static boolean ignoreSQLException(String sqlState) { if (sqlState == null) { System.out.println("The SQL state is not defined!"); return false; } // X0Y32: Jar file already exists in schema if (sqlState.equalsIgnoreCase("X0Y32")) return true; // 42Y55: Table already exists in schema if (sqlState.equalsIgnoreCase("42Y55")) return true; } return false;
Retrieving Warnings
SQLWarning objects are a subclass of SQLException that deal with database access warnings. Warnings do not stop the execution of an application, as exceptions do; they simply alert the user that something did not happen as planned. For example, a warning might let you know that a privilege you attempted to revoke was not revoked. Or a warning might tell you that an error occurred during a requested disconnection. A warning can be reported on a Connection object, a Statement object (including PreparedStatement and CallableStatement objects), or a ResultSet object. Each of these classes has a getWarnings method, which you must invoke in order to see the first warning reported on the calling object. If getWarnings returns a warning, you can call the SQLWarning method getNextWarning on it to get any additional warnings. Executing a statement automatically clears the warnings from a previous statement, so they do not build up. This means, however, that if you want to retrieve warnings reported on a statement, you must do so before you execute another statement. The following methods from JDBCTutorialUtilities illustrate how to get complete information about any warnings reported on Statement or ResultSet objects:
public static void getWarningsFromResultSet(ResultSet rs) throws SQLException { JDBCTutorialUtilities.printWarnings(rs.getWarnings()); } public static void getWarningsFromStatement(Statement stmt) throws SQLException { JDBCTutorialUtilities.printWarnings(stmt.getWarnings()); } public static void printWarnings(SQLWarning warning) throws SQLException { if (warning != null) { System.out.println("\n---Warning---\n"); while (warning != null) { System.out.println("Message: " + warning.getMessage()); System.out.println("SQLState: " + warning.getSQLState()); System.out.print("Vendor error code: "); System.out.println(warning.getErrorCode());
24
System.out.println(""); warning = warning.getNextWarning();
} }
The most common warning is a DataTruncation warning, a subclass of SQLWarning. All DataTruncation objects have a SQLState of 01004, indicating that there was a problem with reading or writing data. DataTruncation methods let you find out in which column or parameter data was truncated, whether the truncation was on a read or write operation, how many bytes should have been transferred, and how many bytes were actually transferred.
Categorized SQLExceptions
Your JDBC driver might throw a subclass of SQLException that corresponds to a common SQLState or a common error state that is not associated with a specific SQLState class value. This enables you to write more portable error-handling code. These exceptions are subclasses of one of the following classes: SQLNonTransientException SQLTransientException SQLRecoverableException See the latest Javadoc of the java.sql package or the documentation of your JDBC driver for more information about these subclasses.
Setting Up Tables
This page describes all the tables used in the JDBC tutorial and how to create them: COFFEES Table SUPPLIERS Table COF_INVENTORY Table MERCH_INVENTORY Table COFFEE_HOUSES Table DATA_REPOSITORY Table Creating Tables Populating Tables
COFFEES Table
The COFFEES table stores information about the coffees available for sale at The Coffee Break: COF_NAME SUP_ID PRICE SALES TOTAL Colombian 101 7.99 0 0 French_Roast 49 8.99 0 0
25
Espresso 150 9.99 0 0 Colombian_Decaf 101 8.99 0 0 French_Roast_Decaf 49 9.99 0 0 The following describes each of the columns in the COFFEES table: COF_NAME: Stores the coffee name. Holds values with a SQL type of VARCHAR with a maximum length of 32 characters. Because the names are different for each type of coffee sold, the name uniquely identifies a particular coffee and serves as the primary key. SUP_ID: Stores a number identifying the coffee supplier. Holds values with a SQL type of INTEGER. It is defined as a foreign key that references the column SUP_ID in the SUPPLIERS table. Consequently, the DBMS will enforce that each value in this column matches one of the values in the corresponding column in the SUPPLIERS table. PRICE: Stores the cost of the coffee per pound. Holds values with a SQL type of FLOAT because it needs to hold values with decimal points. (Note that money values would typically be stored in a SQL type DECIMAL or NUMERIC, but because of differences among DBMSs and to avoid incompatibility with earlier versions of JDBC, the tutorial uses the more standard type FLOAT.) SALES: Stores the number of pounds of coffee sold during the current week. Holds values with a SQL type of INTEGER. TOTAL: Stores the number of pounds of coffee sold to date. Holds values with a SQL type of INTEGER.
SUPPLIERS Table
The SUPPLIERS stores information about each of the suppliers: SUP_ID SUP_NAME STREET CITY STATE ZIP 101 Acme, Inc. 99 Market Street Groundsville CA 95199 49 Superior Coffee 1 Party Place Mendocino CA 95460 150 The High Ground 100 Coffee Lane Meadows CA 93966 The following describes each of the columns in the SUPPLIERS table: SUP_ID: Stores a number identifying the coffee supplier. Holds values with a SQL type of INTEGER. It is the primary key in this table. SUP_NAME: Stores the name of the coffee supplier. STREET, CITY, STATE, and ZIP: These columns store the address of the coffee supplier.
COF_INVENTORY Table
The table COF_INVENTORY stores information about the amount of coffee stored in each warehouse: WAREHOUSE_ID COF_NAME SUP_ID QUAN DATE_VAL 1234 House_Blend 49 0 2006_04_01 1234 House_Blend_Decaf 49 0 2006_04_01 1234 Colombian 101 0 2006_04_01 1234 French_Roast 49 0 2006_04_01 1234 Espresso 150 0 2006_04_01 1234 Colombian_Decaf 101 0 2006_04_01 The following describes each of the columns in the COF_INVENTORY table: WAREHOUSE_ID: Stores a number identifying a warehouse.
26
COF_NAME: Stores the name of a particular type of coffee. SUP_ID: Stores a number identifying a supplier. QUAN: Stores a number indicating the amount of coffee available. DATE: Stores a timestamp value indicating the last time the row was updated.
MERCH_INVENTORY Table
The table MERCH_INVENTORY stores information about the amount of non-coffee merchandise in stock: ITEM_ID ITEM_NAME SUP_ID QUAN DATE 00001234 Cup_Large 00456 28 2006_04_01 00001235 Cup_Small 00456 36 2006_04_01 00001236 Saucer 00456 64 2006_04_01 00001287 Carafe 00456 12 2006_04_01 00006931 Carafe 00927 3 2006_04_01 00006935 PotHolder 00927 88 2006_04_01 00006977 Napkin 00927 108 2006_04_01 00006979 Towel 00927 24 2006_04_01 00004488 CofMaker 08732 5 2006_04_01 00004490 CofGrinder 08732 9 2006_04_01 00004495 EspMaker 08732 4 2006_04_01 00006914 Cookbook 00927 12 2006_04_01 The following describes each of the columns in the MERCH_INVENTORY table: ITEM_ID: Stores a number identifying an item. ITEM_NAME: Stores the name of an item. SUP_ID: Stores a number identifying a supplier. QUAN: Stores a number indicating the amount of that item available. DATE: Stores a timestamp value indicating the last time the row was updated.
COFFEE_HOUSES Table
The table COFFEE_HOUSES stores locations of coffee houses: STORE_ID CITY COFFEE MERCH TOTAL 10023 Mendocino 3450 2005 5455 33002 Seattle 4699 3109 7808 10040 SF 5386 2841 8227 32001 Portland 3147 3579 6726 10042 SF 2863 1874 4710 10024 Sacramento 1987 2341 4328 10039 Carmel 2691 1121 3812 10041 LA 1533 1007 2540 33005 Olympia 2733 1550 4283 33010 Seattle 3210 2177 5387 10035 SF 1922 1056 2978 10037 LA 2143 1876 4019
27
10034 San_Jose 1234 1032 2266 32004 Eugene 1356 1112 2468 The following describes each of the columns in the COFFEE_HOUSES table: STORE_ID: Stores a number identifying a coffee house. It indicates, among other things, the state in which the coffee house is located. A value beginning with 10, for example, means that the state is California. STORE_ID values beginning with 32 indicate Oregon, and those beginning with 33 indicate the state of Washington. CITY: Stores the name of the city in which the coffee house is located. COFFEE: Stores a number indicating the amount of coffee sold. MERCH: Stores a number indicating the amount of coffee sold. TOTAL: Stores a number indicating the total amount of coffee and merchandise sold.
DATA_REPOSITORY Table
The table DATA_REPOSITORY stores URLs that reference documents and other data of interest to The Coffee Break. The script populate_tables.sql does not add any data to this table. The following describes each of the columns in this table: DOCUMENT_NAME: Stores a string that identifies the URL. URL: Stores a URL.
Creating Tables
You can create tables with Apache Ant or JDBC API.
This command runs several Ant targets, including the following, build-tables (from the build.xml file):
<target name="build-tables" description="Create database tables"> <sql driver="${DB.DRIVER}" url="${DB.URL}" userid="${DB.USER}" password="${DB.PASSWORD}" classpathref="CLASSPATH" delimiter="${DB.DELIMITER}" autocommit="false" onerror="abort"> <transaction src= "./sql/${DB.VENDOR}/create-tables.sql"/> </sql> </target>
The sample specifies values for the following sql Ant task parameters: Parameter Description Fully qualified class name of your JDBC driver. This sample uses driver org.apache.derby.jdbc.EmbeddedDriver for Java DB and com.mysql.jdbc.Driver for MySQL Connector/J. Database connection URL that your DBMS JDBC driver uses to connect to a url database. userid Name of a valid user in your DBMS.
28
password Password of the user specified in userid classpathref Full path name of the JAR file that contains the class specified in driver String or character that separates SQL statements. This sample uses the delimiter semicolon (;). Boolean value; if set to false, all SQL statements are executed as one autocommit transaction. Action to perform when a statement fails; possible values are continue, onerror stop, and abort. The value abort specifies that if an error occurs, the transaction is aborted. The sample stores the values of these parameters in a separate file. The build file build.xml retrieves these values with the import task:
<import file="${ANTPROPERTIES}"/>
The transaction element specifies a file that contains SQL statements to execute. The file create-tables.sql contains SQL statements that create all the tables described on this page. For example, the following excerpt from this file creates the tables SUPPLIERS and COFFEES:
create table SUPPLIERS (SUP_ID integer NOT NULL, SUP_NAME varchar(40) NOT NULL, STREET varchar(40) NOT NULL, CITY varchar(20) NOT NULL, STATE char(2) NOT NULL, ZIP char(5), PRIMARY KEY (SUP_ID)); create table COFFEES (COF_NAME varchar(32) NOT NULL, SUP_ID int NOT NULL, PRICE numeric(10,2) NOT NULL, SALES integer NOT NULL, TOTAL integer NOT NULL, PRIMARY KEY (COF_NAME), FOREIGN KEY (SUP_ID) REFERENCES SUPPLIERS (SUP_ID));
Note: The file build.xml contains another target named drop-tables that deletes the tables used by the tutorial. The setup target runs drop-tables before running the build-tables target.
29
stmt.executeUpdate(createString); } catch (SQLException e) { JDBCTutorialUtilities.printSQLException(e); } finally { if (stmt != null) { stmt.close(); } }
In both methods, con is a Connection object and dbName is the name of the database in which you are creating the table. To execute the SQL query, such as those specified by the String createString, use a Statement object. To create a Statement object, call the method Connection.createStatement from an existing Connection object. To execute a SQL query, call the method Statement.executeUpdate. All Statement objects are closed when the connection that created them is closed. However, it is good coding practice to explicitly close Statement objects as soon as you are finished with them. This allows any external resources that the statement is using to be released immediately. Close a statement by calling the method Statement.close. Place this statement in a finally to ensure that it closes even if the normal program flow is interrupted because an exception (such as SQLException) is thrown. Note: You must create the SUPPLIERS table before the COFFEES because COFFEES contains a foreign key, SUP_ID that references SUPPLIERS.
Populating Tables
Similarly, you can insert data into tables with Apache Ant or JDBC API.
30
insert into SUPPLIERS values( 49, 'Superior Coffee', '1 Party Place', 'Mendocino', 'CA', '95460'); insert into SUPPLIERS values( 101, 'Acme, Inc.', '99 Market Street', 'Groundsville', 'CA', '95199'); insert into SUPPLIERS values( 150, 'The High Ground', '100 Coffee Lane', 'Meadows', 'CA', '93966'); insert into COFFEES values( 'Colombian', 00101, 7.99, 0, 0); insert into COFFEES values( 'French_Roast', 00049, 8.99, 0, 0); insert into COFFEES values( 'Espresso', 00150, 9.99, 0, 0); insert into COFFEES values( 'Colombian_Decaf', 00101, 8.99, 0, 0); insert into COFFEES values( 'French_Roast_Decaf', 00049, 9.99, 0, 0);
31
stmt.executeUpdate( "insert into " + dbName + ".COFFEES " + "values('Colombian', 00101, " + "7.99, 0, 0)"); stmt.executeUpdate( "insert into " + dbName + ".COFFEES " + "values('French_Roast', " + "00049, 8.99, 0, 0)"); stmt.executeUpdate( "insert into " + dbName + ".COFFEES " + "values('Espresso', 00150, 9.99, 0, 0)"); stmt.executeUpdate( "insert into " + dbName + ".COFFEES " + "values('Colombian_Decaf', " + "00101, 8.99, 0, 0)"); stmt.executeUpdate( "insert into " + dbName + ".COFFEES " + "values('French_Roast_Decaf', " + "00049, 9.99, 0, 0)"); } catch (SQLException e) { JDBCTutorialUtilities.printSQLException(e); } finally { if (stmt != null) { stmt.close(); } } }
32
int total = rs.getInt("TOTAL"); System.out.println(coffeeName + "\t" + supplierID + "\t" + price + "\t" + sales + "\t" + total);
A ResultSet object is a table of data representing a database result set, which is usually generated by executing a statement that queries the database. For example, the CoffeeTables.viewTable method creates a ResultSet, rs, when it executes the query through the Statement object, stmt. Note that a ResultSet object can be created through any object that implements the Statement interface, including PreparedStatement, CallableStatement, and RowSet. You access the data in a ResultSet object through a cursor. Note that this cursor is not a database cursor. This cursor is a pointer that points to one row of data in the ResultSet. Initially, the cursor is positioned before the first row. The method ResultSet.next moves the cursor to the next row. This method returns false if the cursor is positioned after the last row. This method repeatedly calls the ResultSet.next method with a while loop to iterate through all the data in the ResultSet. This page covers the following topics: ResultSet Interface Retrieving Column Values from Rows Cursors Updating Rows in ResultSet Objects Using Statement Objects for Batch Updates Inserting Rows in ResultSet Objects
ResultSet Interface
The ResultSet interface provides methods for retrieving and manipulating the results of executed queries, and ResultSet objects can have different functionality and characteristics. These characteristics are type, concurrency, and cursor holdability.
ResultSet Types
The type of a ResultSet object determines the level of its functionality in two areas: the ways in which the cursor can be manipulated, and how concurrent changes made to the underlying data source are reflected by the ResultSet object. The sensitivity of a ResultSet object is determined by one of three different ResultSet types: TYPE_FORWARD_ONLY: The result set cannot be scrolled; its cursor moves forward only, from before the first row to after the last row. The rows contained in the result set depend on how the underlying database generates the results. That is, it contains the rows that satisfy the query at either the time the query is executed or as the rows are retrieved. TYPE_SCROLL_INSENSITIVE: The result can be scrolled; its cursor can move both forward and backward relative to the current position, and it can move to an absolute position. The result set is insensitive to changes made to the underlying data source while it is open. It contains the rows that satisfy the query at either the time the query is executed or as the rows are retrieved.
33
TYPE_SCROLL_SENSITIVE: The result can be scrolled; its cursor can move both forward and backward relative to the current position, and it can move to an absolute position. The result set reflects changes made to the underlying data source while the result set remains open. The default ResultSet type is TYPE_FORWARD_ONLY. Note: Not all databases and JDBC drivers support all ResultSet types. The method DatabaseMetaData.supportsResultSetType returns true if the specified ResultSet type is supported and false otherwise.
ResultSet Concurrency
The concurrency of a ResultSet object determines what level of update functionality is supported. There are two concurrency levels: CONCUR_READ_ONLY: The ResultSet object cannot be updated using the ResultSet interface. CONCUR_UPDATABLE: The ResultSet object can be updated using the ResultSet interface. The default ResultSet concurrency is CONCUR_READ_ONLY. Note: Not all JDBC drivers and databases support concurrency. The method DatabaseMetaData.supportsResultSetConcurrency returns true if the specified concurrency level is supported by the driver and false otherwise. The method CoffeesTable.modifyPrices demonstrates how to use a ResultSet object whose concurrency level is CONCUR_UPDATABLE.
Cursor Holdability
Calling the method Connection.commit can close the ResultSet objects that have been created during the current transaction. In some cases, however, this may not be the desired behavior. The ResultSet property holdability gives the application control over whether ResultSet objects (cursors) are closed when commit is called. The following ResultSet constants may be supplied to the Connection methods createStatement, prepareStatement, and prepareCall: HOLD_CURSORS_OVER_COMMIT: ResultSet cursors are not closed; they are holdable: they are held open when the method commit is called. Holdable cursors might be ideal if your application uses mostly read-only ResultSet objects. CLOSE_CURSORS_AT_COMMIT: ResultSet objects (cursors) are closed when the commit method is called. Closing cursors when this method is called can result in better performance for some applications. The default cursor holdability varies depending on your DBMS. Note: Not all JDBC drivers and databases support holdable and non-holdable cursors. The following method, JDBCTutorialUtilities.cursorHoldabilitySupport, outputs the default cursor holdability of ResultSet objects and whether HOLD_CURSORS_OVER_COMMIT and CLOSE_CURSORS_AT_COMMIT are supported:
public static void cursorHoldabilitySupport(Connection conn) throws SQLException { DatabaseMetaData dbMetaData = conn.getMetaData(); System.out.println("ResultSet.HOLD_CURSORS_OVER_COMMIT = " + ResultSet.HOLD_CURSORS_OVER_COMMIT); System.out.println("ResultSet.CLOSE_CURSORS_AT_COMMIT = " +
34
ResultSet.CLOSE_CURSORS_AT_COMMIT); System.out.println("Default cursor holdability: " + dbMetaData.getResultSetHoldability()); System.out.println("Supports HOLD_CURSORS_OVER_COMMIT? " + dbMetaData.supportsResultSetHoldability( ResultSet.HOLD_CURSORS_OVER_COMMIT)); System.out.println("Supports CLOSE_CURSORS_AT_COMMIT? " + dbMetaData.supportsResultSetHoldability( ResultSet.CLOSE_CURSORS_AT_COMMIT));
Strings used as input to getter methods are case-insensitive. When a getter method is called with a string and more than one column has the same alias or name as the string, the value of the first matching column is returned. The option to use a string as opposed to an integer is designed to be used when column aliases and names are used in the SQL query that generated the result set. For columns that are not explicitly named in the query (for example, select * from COFFEES) it is best to use column numbers. If column names are used, the developer should guarantee that they uniquely refer to the intended columns by using column aliases. A column alias effectively renames the column of a result set. To specify a column alias, use the SQL AS clause in the SELECT
35
statement. The getter method of the appropriate type retrieves the value in each column. For example, in the method CoffeeTables.viewTable, the first column in each row of the ResultSet rs is COF_NAME, which stores a value of SQL type VARCHAR. The method for retrieving a value of SQL type VARCHAR is getString. The second column in each row stores a value of SQL type INTEGER, and the method for retrieving values of that type is getInt. Note that although the method getString is recommended for retrieving the SQL types CHAR and VARCHAR, it is possible to retrieve any of the basic SQL types with it. Getting all values with getString can be very useful, but it also has its limitations. For instance, if it is used to retrieve a numeric type, getString converts the numeric value to a Java String object, and the value has to be converted back to a numeric type before it can be operated on as a number. In cases where the value is treated as a string anyway, there is no drawback. Furthermore, if you want an application to retrieve values of any standard SQL type other than SQL3 types, use the getString method.
Cursors
As mentioned previously, you access the data in a ResultSet object through a cursor, which points to one row in the ResultSet object. However, when a ResultSet object is first created, the cursor is positioned before the first row. The method CoffeeTables.viewTable moves the cursor by calling the ResultSet.next method. There are other methods available to move the cursor: next: Moves the cursor forward one row. Returns true if the cursor is now positioned on a row and false if the cursor is positioned after the last row. previous: Moves the cursor backward one row. Returns true if the cursor is now positioned on a row and false if the cursor is positioned before the first row. first: Moves the cursor to the first row in the ResultSet object. Returns true if the cursor is now positioned on the first row and false if the ResultSet object does not contain any rows. last:: Moves the cursor to the last row in the ResultSet object. Returns true if the cursor is now positioned on the last row and false if the ResultSet object does not contain any rows. beforeFirst: Positions the cursor at the start of the ResultSet object, before the first row. If the ResultSet object does not contain any rows, this method has no effect. afterLast: Positions the cursor at the end of the ResultSet object, after the last row. If the ResultSet object does not contain any rows, this method has no effect. relative(int rows): Moves the cursor relative to its current position. absolute(int row): Positions the cursor on the row specified by the parameter row. Note that the default sensitivity of a ResultSet is TYPE_FORWARD_ONLY, which means that it cannot be scrolled; you cannot call any of these methods that move the cursor, except next, if your ResultSet cannot be scrolled. The method CoffeesTable.modifyPrices, described in the following section, demonstrates how you can move the cursor of a ResultSet.
36
public void modifyPrices(float percentage) throws SQLException { Statement stmt = null; try { stmt = con.createStatement(); stmt = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE); ResultSet uprs = stmt.executeQuery( "SELECT * FROM " + dbName + ".COFFEES"); while (uprs.next()) { float f = uprs.getFloat("PRICE"); uprs.updateFloat( "PRICE", f * percentage); uprs.updateRow(); } } catch (SQLException e ) { JDBCTutorialUtilities.printSQLException(e); } finally { if (stmt != null) { stmt.close(); } } }
The field ResultSet.TYPE_SCROLL_SENSITIVE creates a ResultSet object whose cursor can move both forward and backward relative to the current position and to an absolute position. The field ResultSet.CONCUR_UPDATABLE creates a ResultSet object that can be updated. See the ResultSet Javadoc for other fields you can specify to modify the behavior of ResultSet objects. The method ResultSet.updateFloat updates the specified column (in this example, PRICE with the specified float value in the row where the cursor is positioned. ResultSet contains various updater methods that enable you to update column values of various data types. However, none of these updater methods modifies the database; you must call the method ResultSet.updateRow to update the database.
37
"INSERT INTO COFFEES " + "VALUES('Amaretto', 49, 9.99, 0, 0)"); stmt.addBatch( "INSERT INTO COFFEES " + "VALUES('Hazelnut', 49, 9.99, 0, 0)"); stmt.addBatch( "INSERT INTO COFFEES " + "VALUES('Amaretto_decaf', 49, " + "10.99, 0, 0)"); stmt.addBatch( "INSERT INTO COFFEES " + "VALUES('Hazelnut_decaf', 49, " + "10.99, 0, 0)"); int [] updateCounts = stmt.executeBatch(); this.con.commit(); } catch(BatchUpdateException b) { JDBCTutorialUtilities.printBatchUpdateException(b); } catch(SQLException ex) { JDBCTutorialUtilities.printSQLException(ex); } finally { if (stmt != null) { stmt.close(); } this.con.setAutoCommit(true); }
The following line disables auto-commit mode for the Connection object con so that the transaction will not be automatically committed or rolled back when the method executeBatch is called.
this.con.setAutoCommit(false);
To allow for correct error handling, you should always disable auto-commit mode before beginning a batch update. The method Statement.addBatch adds a command to the list of commands associated with the Statement object stmt. In this example, these commands are all INSERT INTO statements, each one adding a row consisting of five column values. The values for the columns COF_NAME and PRICE are the name of the coffee and its price, respectively. The second value in each row is 49 because that is the identification number for the supplier, Superior Coffee. The last two values, the entries for the columns SALES and TOTAL, all start out being zero because there have been no sales yet. (SALES is the number of pounds of this row's coffee sold in the current week; TOTAL is the total of all the cumulative sales of this coffee.) The following line sends the four SQL commands that were added to its list of commands to the database to be executed as a batch:
int [] updateCounts = stmt.executeBatch();
Note that stmt uses the method executeBatch to send the batch of insertions, not the method executeUpdate, which sends only one command and returns a single update count. The DBMS executes the commands in the order in which they were added to the list of commands, so it will first add the row of values for Amaretto, then add the row for Hazelnut, then Amaretto decaf, and finally Hazelnut decaf. If all four commands execute successfully, the DBMS will return an update count for each command in the order in which it was executed. The update counts that indicate how many rows were affected by each command are stored in the array updateCounts. If all four of the commands in the batch are executed successfully, updateCounts will contain
38
four values, all of which are 1 because an insertion affects one row. The list of commands associated with stmt will now be empty because the four commands added previously were sent to the database when stmt called the method executeBatch. You can at any time explicitly empty this list of commands with the method clearBatch. The Connection.commit method makes the batch of updates to the COFFEES table permanent. This method needs to be called explicitly because the auto-commit mode for this connection was disabled previously. The following line enables auto-commit mode for the current Connection object.
this.con.setAutoCommit(true);
Now each statement in the example will automatically be committed after it is executed, and it no longer needs to invoke the method commit.
39
array will contain five numbers: the first one being the update count for the first command, the second one being the update count for the second command, and so on. BatchUpdateException is derived from SQLException. This means that you can use all of the methods available to an SQLException object with it. The following method, JDBCTutorialUtilities.printBatchUpdateException prints all of the SQLException information plus the update counts contained in a BatchUpdateException object. Because BatchUpdateException.getUpdateCounts returns an array of int, the code uses a for loop to print each of the update counts:
public static void printBatchUpdateException(BatchUpdateException b) { System.err.println("----BatchUpdateException----"); System.err.println("SQLState: " + b.getSQLState()); System.err.println("Message: " + b.getMessage()); System.err.println("Vendor: " + b.getErrorCode()); System.err.print("Update counts: "); int [] updateCounts = b.getUpdateCounts(); for (int i = 0; i < updateCounts.length; i++) { System.err.print(updateCounts[i] + " "); }
40
}
This example calls the Connection.createStatement method with two arguments, ResultSet.TYPE_SCROLL_SENSITIVE and ResultSet.CONCUR_UPDATABLE. The first value enables the cursor of the ResultSet object to be moved both forward and backward. The second value, ResultSet.CONCUR_UPDATABLE, is required if you want to insert rows into a ResultSet object; it specifies that it can be updatable. The same stipulations for using strings in getter methods also apply to updater methods. The method ResultSet.moveToInsertRow moves the cursor to the insert row. The insert row is a special row associated with an updatable result set. It is essentially a buffer where a new row can be constructed by calling the updater methods prior to inserting the row into the result set. For example, this method calls the method ResultSet.updateString to update the insert row's COF_NAME column to Kona. The method ResultSet.insertRow inserts the contents of the insert row into the ResultSet object and into the database. Note: After inserting a row with the ResultSet.insertRow, you should move the cursor to a row other than the insert row. For example, this example moves it to before the first row in the result set with the method ResultSet.beforeFirst. Unexpected results can occur if another part of your application uses the same result set and the cursor is still pointing to the insert row.
41
PreparedStatement updateTotal = null; String updateString = "update " + dbName + ".COFFEES " + "set SALES = ? where COF_NAME = ?"; String updateStatement = "update " + dbName + ".COFFEES " + "set TOTAL = TOTAL + ? " + "where COF_NAME = ?"; try { con.setAutoCommit(false); updateSales = con.prepareStatement(updateString); updateTotal = con.prepareStatement(updateStatement); for (Map.Entry<String, Integer> e : salesForWeek.entrySet()) { updateSales.setInt(1, e.getValue().intValue()); updateSales.setString(2, e.getKey()); updateSales.executeUpdate(); updateTotal.setInt(1, e.getValue().intValue()); updateTotal.setString(2, e.getKey()); updateTotal.executeUpdate(); con.commit(); } } catch (SQLException e ) { JDBCTutorialUtilities.printSQLException(e); if (con != null) { try { System.err.print("Transaction is being rolled back"); con.rollback(); } catch(SQLException excep) { JDBCTutorialUtilities.printSQLException(excep); } } } finally { if (updateSales != null) { updateSales.close(); } if (updateTotal != null) { updateTotal.close(); } con.setAutoCommit(true); }
42
updateSales.setString(2, e.getKey());
The first argument for each of these setter methods specifies the question mark placeholder. In this example, setInt specifies the first placeholder and setString specifies the second placeholder. After a parameter has been set with a value, it retains that value until it is reset to another value, or the method clearParameters is called. Using the PreparedStatement object updateSales, the following code fragment illustrates reusing a prepared statement after resetting the value of one of its parameters and leaving the other one the same:
// changes SALES column of French Roast //row to 100 updateSales.setInt(1, 100); updateSales.setString(2, "French_Roast"); updateSales.executeUpdate(); // changes SALES column of Espresso row to 100 // (the first parameter stayed 100, and the second // parameter was reset to "Espresso") updateSales.setString(2, "Espresso"); updateSales.executeUpdate();
The method CoffeesTable.updateCoffeeSales takes one argument, HashMap. Each element in the HashMap argument contains the name of one type of coffee and the number of pounds of that type of coffee sold during the current week. The for-each loop iterates through each element of the HashMap argument and sets the appropriate question mark placeholders in updateSales and updateTotal.
43
No arguments are supplied to executeUpdate when they are used to execute updateSales and updateTotals; both PreparedStatement objects already contain the SQL statement to be executed. Note: At the beginning of CoffeesTable.updateCoffeeSales, the auto-commit mode is set to false:
con.setAutoCommit(false);
Consequently, no SQL statements are committed until the method commit is called. For more information about the auto-commit mode, see Transactions.
The table COFFEES is updated; the value 50 replaces the value in the column SALES in the row for Espresso. That update affects one row in the table, so n is equal to 1. When the method executeUpdate is used to execute a DDL (data definition language) statement, such as in creating a table, it returns the int value of 0. Consequently, in the following code fragment, which executes the DDL statement used to create the table COFFEES, n is assigned a value of 0:
// n = 0 int n = executeUpdate(createTableCoffees);
Note that when the return value for executeUpdate is 0, it can mean one of two things: The statement executed was an update statement that affected zero rows. The statement executed was a DDL statement.
Using Transactions
There are times when you do not want one statement to take effect unless another one completes. For example, when the proprietor of The Coffee Break updates the amount of coffee sold each week, the proprietor will also want to update the total amount sold to date. However, the amount sold per week and the total amount sold should be updated at the same time; otherwise, the data will be inconsistent. The way to be sure that either both actions occur or neither action occurs is to use a transaction. A transaction is a set of one or more statements that is executed as a unit, so either all of the statements are executed, or none of the statements is executed. This page covers the following topics Disabling Auto-Commit Mode Committing Transactions Using Transactions to Preserve Data Integrity Setting and Rolling Back to Savepoints Releasing Savepoints When to Call Method rollback
44
is executed. A statement is completed when all of its result sets and update counts have been retrieved. In almost all cases, however, a statement is completed, and therefore committed, right after it is executed.) The way to allow two or more statements to be grouped into a transaction is to disable the autocommit mode. This is demonstrated in the following code, where con is an active connection:
con.setAutoCommit(false);
Committing Transactions
After the auto-commit mode is disabled, no SQL statements are committed until you call the method commit explicitly. All statements executed after the previous call to the method commit are included in the current transaction and committed together as a unit. The following method, CoffeesTable.updateCoffeeSales, in which con is an active connection, illustrates a transaction:
public void updateCoffeeSales(HashMap<String, Integer> salesForWeek) throws SQLException { PreparedStatement updateSales = null; PreparedStatement updateTotal = null; String updateString = "update " + dbName + ".COFFEES " + "set SALES = ? where COF_NAME = ?"; String updateStatement = "update " + dbName + ".COFFEES " + "set TOTAL = TOTAL + ? " + "where COF_NAME = ?"; try { con.setAutoCommit(false); updateSales = con.prepareStatement(updateString); updateTotal = con.prepareStatement(updateStatement); for (Map.Entry<String, Integer> e : salesForWeek.entrySet()) { updateSales.setInt(1, e.getValue().intValue()); updateSales.setString(2, e.getKey()); updateSales.executeUpdate(); updateTotal.setInt(1, e.getValue().intValue()); updateTotal.setString(2, e.getKey()); updateTotal.executeUpdate(); con.commit(); } } catch (SQLException e ) { JDBCTutorialUtilities.printSQLException(e); if (con != null) { try { System.err.print("Transaction is being rolled back"); con.rollback(); } catch(SQLException excep) { JDBCTutorialUtilities.printSQLException(excep); } } } finally { if (updateSales != null) { updateSales.close(); } if (updateTotal != null) { updateTotal.close();
45
} con.setAutoCommit(true);
} }
In this method, the auto-commit mode is disabled for the connection con, which means that the two prepared statements updateSales and updateTotal are committed together when the method commit is called. Whenever the commit method is called (either automatically when autocommit mode is enabled or explicitly when it is disabled), all changes resulting from statements in the transaction are made permanent. In this case, that means that the SALES and TOTAL columns for Colombian coffee have been changed to 50 (if TOTAL had been 0 previously) and will retain this value until they are changed with another update statement. The statement con.setAutoCommit(true); enables auto-commit mode, which means that each statement is once again committed automatically when it is completed. Then, you are back to the default state where you do not have to call the method commit yourself. It is advisable to disable the auto-commit mode only during the transaction mode. This way, you avoid holding database locks for multiple statements, which increases the likelihood of conflicts with other users.
46
Reads TRANSACTION_NONE Not supported Not applicable Not applicable Reads Not applicable
TRANSACTION_READ_COMM Supported Prevented Allowed Allowed ITTED TRANSACTION_READ_UNCO Supported Allowed Allowed Allowed MMITTED TRANSACTION_REPEATABL Supported Prevented Prevented Allowed E_READ TRANSACTION_SERIALIZA Supported Prevented Prevented Prevented BLE A non-repeatable read occurs when transaction A retrieves a row, transaction B subsequently updates the row, and transaction A later retrieves the same row again. Transaction A retrieves the same row twice but sees different data. A phantom read occurs when transaction A retrieves a set of rows satisfying a given condition, transaction B subsequently inserts or updates a row such that the row now meets the condition in transaction A, and transaction A later repeats the conditional retrieval. Transaction A now sees an additional row. This row is referred to as a phantom. Usually, you do not need to do anything about the transaction isolation level; you can just use the default one for your DBMS. The default transaction isolation level depends on your DBMS. For example, for Java DB, it is TRANSACTION_READ_COMMITTED. JDBC allows you to find out what transaction isolation level your DBMS is set to (using the Connection method getTransactionIsolation) and also allows you to set it to another level (using the Connection method setTransactionIsolation). Note: A JDBC driver might not support all transaction isolation levels. If a driver does not support the isolation level specified in an invocation of setTransactionIsolation, the driver can substitute a higher, more restrictive transaction isolation level. If a driver cannot substitute a higher transaction level, it throws a SQLException. Use the method DatabaseMetaData.supportsTransactionIsolationLevel to determine whether or not the driver supports a given level.
47
"SELECT COF_NAME, PRICE FROM COFFEES " + "WHERE COF_NAME = '" + coffeeName + "'"; try { Savepoint save1 = con.setSavepoint(); getPrice = con.createStatement( ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); updatePrice = con.createStatement(); if (!getPrice.execute(query)) { System.out.println( "Could not find entry " + "for coffee named " + coffeeName); } else { rs = getPrice.getResultSet(); rs.first(); float oldPrice = rs.getFloat("PRICE"); float newPrice = oldPrice + (oldPrice * priceModifier); System.out.println( "Old price of " + coffeeName + " is " + oldPrice); System.out.println( "New price of " + coffeeName + " is " + newPrice); System.out.println( "Performing update..."); updatePrice.executeUpdate( "UPDATE COFFEES SET PRICE = " + newPrice + " WHERE COF_NAME = '" + coffeeName + "'"); System.out.println( "\nCOFFEES table after " + "update:"); CoffeesTable.viewTable(con); if (newPrice > maximumPrice) { System.out.println( "\nThe new price, " + newPrice + ", is greater than the " + "maximum price, " + maximumPrice + ". Rolling back the " + "transaction..."); con.rollback(save1); System.out.println( "\nCOFFEES table " + "after rollback:"); CoffeesTable.viewTable(con);
48
} con.commit();
} } catch (SQLException e) { JDBCTutorialUtilities.printSQLException(e); } finally { if (getPrice != null) { getPrice.close(); } if (updatePrice != null) { updatePrice.close(); } con.setAutoCommit(true); }
The following statement specifies that the cursor of the ResultSet object generated from the getPrice query is closed when the commit method is called. Note that if your DBMs does not support ResultSet.CLOSE_CURSORS_AT_COMMIT, then this constant is ignored:
getPrice = con.prepareStatement(query, ResultSet.CLOSE_CURSORS_AT_COMMIT);
The method checks if the new price is greater than the maximumPrice value. If so, the method rolls back the transaction with the following statement:
con.rollback(save1);
Consequently, when the method commits the transaction by calling the Connection.commit method, it will not commit any rows whose associated Savepoint has been rolled back; it will commit all the other updated rows.
Releasing Savepoints
The method Connection.releaseSavepoint takes a Savepoint object as a parameter and removes it from the current transaction. After a savepoint has been released, attempting to reference it in a rollback operation causes a SQLException to be thrown. Any savepoints that have been created in a transaction are automatically released and become invalid when the transaction is committed, or when the entire transaction is rolled back. Rolling a transaction back to a savepoint automatically releases and makes invalid any other savepoints that were created after the savepoint in question.
49
standard reference are available for these RowSet interfaces. In this tutorial you will learn how to use these reference implementations. These versions of the RowSet interface and their implementations have been provided as a convenience for programmers. Programmers are free write their own versions of the javax.sql.RowSet interface, to extend the implementations of the five RowSet interfaces, or to write their own implementations. However, many programmers will probably find that the standard reference implementations already fit their needs and will use them as is. This section introduces you to the RowSet interface and the following interfaces that extend this interface: JdbcRowSet CachedRowSet WebRowSet JoinRowSet FilteredRowSet The following topics are covered: What Can RowSet Objects Do? Kinds of RowSet Objects
50
rs.addListener(bg);
Now bg will be notified each time the cursor moves, a row is changed, or all of rs gets new data.
51
Write itself as an XML document Read an XML document that describes a WebRowSet object A JoinRowSet object has all the capabilities of a WebRowSet object (and therefore also those of a CachedRowSet object) plus it can also do the following: Form the equivalent of a SQL JOIN without having to connect to a data source A FilteredRowSet object likewise has all the capabilities of a WebRowSet object (and therefore also a CachedRowSet object) plus it can also do the following: Apply filtering criteria so that only selected data is visible. This is equivalent to executing a query on a RowSet object without having to use a query language or connect to a data source.
52
ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE); rs = stmt.executeQuery("select * from COFFEES"); jdbcRs = new JdbcRowSetImpl(rs);
A JdbcRowSet object created with a ResultSet object serves as a wrapper for the ResultSet object. Because the RowSet object rs is scrollable and updatable, jdbcRs is also scrollable and updatable. If you have run the method createStatement without any arguments, rs would not be scrollable or updatable, and neither would jdbcRs.
The object jdbcRs contains no data until you specify a SQL statement with the method setCommand, then run the method execute. The object jdbcRs is scrollable and updatable; by default, JdbcRowSet and all other RowSet objects are scrollable and updatable unless otherwise specified. See Default JdbcRowSet Objects for more information about JdbcRowSet properties you can specify.
The object jdbcRs contains no data until you specify a SQL statement with the method setCommand, specify how the JdbcResultSet object connects the database, and then run the method execute. All of the reference implementation constructors assign the default values for the properties listed in the section Default JdbcRowSet Objects.
53
jdbcRs.setUrl("jdbc:myDriver:myAttribute"); jdbcRs.setUsername(username); jdbcRs.setPassword(password); jdbcRs.setCommand("select * from COFFEES"); jdbcRs.execute(); } } // ...
The following statement creates the RowSetProvider object myRowSetFactory with the default RowSetFactory implementation, com.sun.rowset.RowSetFactoryImpl:
myRowSetFactory = RowSetProvider.newFactory();
Alternatively, if your JDBC driver has its own RowSetFactory implementation, you may specify it as an argument of the newFactory method. The following statements create the JdbcRowSet object jdbcRs and configure its database connection properties:
jdbcRs = myRowSetFactory.createJdbcRowSet(); jdbcRs.setUrl("jdbc:myDriver:myAttribute"); jdbcRs.setUsername(username); jdbcRs.setPassword(password);
The RowSetFactory interface contains methods to create the different types of RowSet implementations available in RowSet 1.1 and later: createCachedRowSet createFilteredRowSet createJdbcRowSet createJoinRowSet createWebRowSet
54
Setting Properties
The section Default JdbcRowSet Objects lists the properties that are set by default when a new JdbcRowSet object is created. If you use the default constructor, you must set some additional properties before you can populate your new JdbcRowSet object with data. In order to get its data, a JdbcRowSet object first needs to connect to a database. The following four properties hold information used in obtaining a connection to a database. username: the name a user supplies to a database as part of gaining access password: the user's database password url: the JDBC URL for the database to which the user wants to connect datasourceName: the name used to retrieve a DataSource object that has been registered with a JNDI naming service Which of these properties you set depends on how you are going to make a connection. The preferred way is to use a DataSource object, but it may not be practical for you to register a DataSource object with a JNDI naming service, which is generally done by a system administrator. Therefore, the code examples all use the DriverManager mechanism to obtain a connection, for which you use the url property and not the datasourceName property. Another property that you must set is the command property. This property is the query that determines what data the JdbcRowSet object will hold. For example, the following line of code sets the command property with a query that produces a ResultSet object containing all the data in the table COFFEES:
jdbcRs.setCommand("select * from COFFEES");
After you have set the command property and the properties necessary for making a connection, you are ready to populate the jdbcRs object with data by calling the execute method.
jdbcRs.execute();
The execute method does many things for you in the background: It makes a connection to the database using the values you assigned to the url, username, and password properties. It executes the query you set in the command property. It reads the data from the resulting ResultSet object into the jdbcRs object.
55
Updating Column Values Inserting Rows Deleting Rows
The method previous is analogous to the method next in that it can be used in a while loop to traverse all of the rows in order. The difference is that you must move the cursor to a position after the last row, and previous moves the cursor toward the beginning.
The code moves the cursor to the third row and changes the value for the column PRICE to 10.99, and then updates the database with the new price. Calling the method updateRow updates the database because jdbcRs has maintained its connection to the database. For disconnected RowSet objects, the situation is different.
Inserting Rows
If the owner of the Coffee Break chain wants to add one or more coffees to what he offers, the owner will need to add one row to the COFFEES table for each new coffee, as is done in the following code fragment from JdbcRowSetSample. Notice that because the jdbcRs object is always connected to the database, inserting a row into a JdbcRowSet object is the same as inserting a row into a ResultSet object: You move to the cursor to the insert row, use the appropriate updater method to set a value for each column, and call the method insertRow:
jdbcRs.moveToInsertRow(); jdbcRs.updateString("COF_NAME", "HouseBlend"); jdbcRs.updateInt("SUP_ID", 49); jdbcRs.updateFloat("PRICE", 7.99f); jdbcRs.updateInt("SALES", 0); jdbcRs.updateInt("TOTAL", 0); jdbcRs.insertRow(); jdbcRs.moveToInsertRow(); jdbcRs.updateString("COF_NAME", "HouseDecaf"); jdbcRs.updateInt("SUP_ID", 49); jdbcRs.updateFloat("PRICE", 8.99f); jdbcRs.updateInt("SALES", 0); jdbcRs.updateInt("TOTAL", 0); jdbcRs.insertRow();
When you call the method insertRow, the new row is inserted into the jdbcRs object and is
56
also inserted into the database. The preceding code fragment goes through this process twice, so two new rows are inserted into the jdbcRs object and the database.
Deleting Rows
As is true with updating data and inserting a new row, deleting a row is just the same for a JdbcRowSet object as for a ResultSet object. The owner wants to discontinue selling French Roast decaffeinated coffee, which is the last row in the jdbcRs object. In the following lines of code, the first line moves the cursor to the last row, and the second line deletes the last row from the jdbcRs object and from the database:
jdbcRs.last(); jdbcRs.deleteRow();
Code Sample
The sample JdbcRowSetSample does the following: Creates a new JdbcRowSet object initialized with the ResultSet object that was produced by the execution of a query that retrieves all the rows in the COFFEES table Moves the cursor to the third row of the COFFEES table and updates the PRICE column in that row Inserts two new rows, one for HouseBlend and one for HouseDecaf Moves the cursor to the last row and deletes it
Using CachedRowSetObjects
A CachedRowSet object is special in that it can operate without being connected to its data source, that is, it is a disconnected RowSet object. It gets its name from the fact that it stores (caches) its data in memory so that it can operate on its own data rather than on the data stored in a database. The CachedRowSet interface is the superinterface for all disconnected RowSet objects, so everything demonstrated here also applies to WebRowSet, JoinRowSet, and FilteredRowSet objects. Note that although the data source for a CachedRowSet object (and the RowSet objects derived from it) is almost always a relational database, a CachedRowSet object is capable of getting data from any data source that stores its data in a tabular format. For example, a flat file or spreadsheet could be the source of data. This is true when the RowSetReader object for a disconnected RowSet object is implemented to read data from such a data source. The reference implementation of the CachedRowSet interface has a RowSetReader object that reads data from a relational database, so in this tutorial, the data source is always a database. The following topics are covered: Setting Up CachedRowSet Objects Populating CachedRowSet Objects What Reader Does Updating CachedRowSet Objects Updating Data Sources What Writer Does Notifying Listeners Sending Large Amounts of Data
57
Setting CachedRowSet Properties Setting Key Columns
The object crs has the same default values for its properties that a JdbcRowSet object has when it is first created. In addition, it has been assigned an instance of the default SyncProvider implementation, RIOptimisticProvider. A SyncProvider object supplies a RowSetReader object (a reader) and a RowSetWriter object (a writer), which a disconnected RowSet object needs in order to read data from its data source or to write data back to its data source. What a reader and writer do is explained later in the sections What Reader Does and What Writer Does. One thing to keep in mind is that readers and writers work entirely in the background, so the explanation of how they work is for your information only. Having some background on readers and writers should help you understand what some of the methods defined in the CachedRowSet interface do in the background.
58
public void setConnectionProperties( String username, String password) { crs.setUsername(username); crs.setPassword(password); crs.setUrl("jdbc:mySubprotocol:mySubname"); // ...
Another property that you must set is the command property. In the reference implementation, data is read into a RowSet object from a ResultSet object. The query that produces that ResultSet object is the value for the command property. For example, the following line of code sets the command property with a query that produces a ResultSet object containing all the data in the table MERCH_INVENTORY:
crs.setCommand("select * from MERCH_INVENTORY");
The first column in the table MERCH_INVENTORY is ITEM_ID. It can serve as the key column because every item identifier is different and therefore uniquely identifies one row and only one row in the table MERCH_INVENTORY. In addition, this column is specified as a primary key in the definition of the MERCH_INVENTORY table. The method setKeyColumns takes an array to allow for the fact that it may take two or more columns to identify a row uniquely. As a point of interest, the method setKeyColumns does not set a value for a property. In this case, it sets the value for the field keyCols. Key columns are used internally, so after setting them, you do nothing more with them. You will see how and when key columns are used in the section Using SyncResolver Objects.
The data in crs is the data in the ResultSet object produced by executing the query in the command property. What is different is that the CachedRowSet implementation for the execute method does a lot more than the JdbcRowSet implementation. Or more correctly, the CachedRowSet object's reader, to which the method execute delegates its tasks, does a lot more. Every disconnected RowSet object has a SyncProvider object assigned to it, and this SyncProvider object is what provides the RowSet object's reader (a RowSetReader object). When the crs object was created, it was used as the default CachedRowSetImpl constructor, which, in addition to setting default values for properties, assigns an instance of the RIOptimisticProvider implementation as the default SyncProvider object.
59
behind the scenes to populate the RowSet object with data. A newly created CachedRowSet object is not connected to a data source and therefore must obtain a connection to that data source in order to get data from it. The reference implementation of the default SyncProvider object (RIOptimisticProvider) provides a reader that obtains a connection by using the values set for the user name, password, and either the JDBC URL or the data source name, whichever was set more recently. Then the reader executes the query set for the command. It reads the data in the ResultSet object produced by the query, populating the CachedRowSet object with that data. Finally, the reader closes the connection.
60
crs.updateString("ITEM_NAME", "TableCloth"); crs.updateInt("SUP_ID", 927); crs.updateInt("QUAN", 14); Calendar timeStamp; timeStamp = new GregorianCalendar(); timeStamp.set(2006, 4, 1); crs.updateTimestamp( "DATE_VAL", new Timestamp(timeStamp.getTimeInMillis())); crs.insertRow(); crs.moveToCurrentRow();
If headquarters has decided to stop stocking a particular item, it would probably remove the row for that coffee itself. However, in the scenario, a warehouse employee using a PDA also has the capability of removing it. The following code fragment finds the row where the value in the ITEM_ID column is 12345 and deletes it from the CachedRowSet crs:
while (crs.next()) { if (crs.getInt("ITEM_ID") == 12345) { crs.deleteRow(); break; } }
61
few, if any, conflicts and therefore sets no database locks. The writer checks to see if there are any conflicts, and if there is none, it writes the changes made to the crs object to the database, and those changes become persistent. If there are any conflicts, the default is not to write the new RowSet values to the database. In the scenario, the default behavior works very well. Because no one at headquarters is likely to change the value in the QUAN column of COF_INVENTORY, there will be no conflicts. As a result, the values entered into the crs object at the warehouse will be written to the database and thus will be persistent, which is the desired outcome.
The object resolver is a RowSet object that replicates the crs object except that it contains only the values in the database that caused a conflict. All other column values are null. With the resolver object, you can iterate through its rows to locate the values that are not null and are therefore values that caused a conflict. Then you can locate the value at the same position in the crs object and compare them. The following code fragment retrieves resolver and uses the SyncResolver method nextConflict to iterate through the rows that have conflicting values. The object resolver gets the status of each conflicting value, and if it is UPDATE_ROW_CONFLICT, meaning that the crs was attempting an update when the conflict occurred, the resolver object gets the row number of that value. Then the code moves the cursor for the crs object to the same row. Next, the code finds the column in that row of the resolver object that contains a conflicting value, which will be a value that is not null. After retrieving the value in that column from both the resolver and crs objects, you can compare the two and decide which one you want to become persistent. Finally, the code sets that value in both the crs object and the database using the method setResolvedValue, as shown in the following code:
try { crs.acceptChanges(); } catch (SyncProviderException spe) { SyncResolver resolver = spe.getSyncResolver(); // value in crs Object crsValue; // value in the SyncResolver object Object resolverValue; // value to be persistent Object resolvedValue;
62
while (resolver.nextConflict()) { if (resolver.getStatus() == SyncResolver.UPDATE_ROW_CONFLICT) { int row = resolver.getRow(); crs.absolute(row); int colCount = crs.getMetaData().getColumnCount(); for (int j = 1; j <= colCount; j++) { if (resolver.getConflictValue(j) != null) { crsValue = crs.getObject(j); resolverValue = resolver.getConflictValue(j); // // // // // ... compare crsValue and resolverValue to determine the value to be persistent
Notifying Listeners
Being a JavaBeans component means that a RowSet object can notify other components when certain things happen to it. For example, if data in a RowSet object changes, the RowSet object can notify interested parties of that change. The nice thing about this notification mechanism is that, as an application programmer, all you have to do is add or remove the components that will be notified. This section covers the following topics: Setting Up Listeners How Notification Works
Setting Up Listeners
A listener for a RowSet object is a component that implements the following methods from the RowSetListener interface: cursorMoved: Defines what the listener will do, if anything, when the cursor in the RowSet object moves. rowChanged: Defines what the listener will do, if anything, when one or more column values in a row have changed, a row has been inserted, or a row has been deleted. rowSetChanged: Defines what the listener will do, if anything, when the RowSet object has been populated with new data. An example of a component that might want to be a listener is a BarGraph object that graphs the data in a RowSet object. As the data changes, the BarGraph object can update itself to reflect the new data. As an application programmer, the only thing you must do to take advantage of the notification mechanism is to add or remove listeners. The following line of code means that every time the cursor for the crs objects moves, values in crs are changed, or crs as a whole gets new data, the
63
BarGraph object bar will be notified:
crs.addRowSetListener(bar);
You can also stop notifications by removing a listener, as is done in the following line of code:
crs.removeRowSetListener(bar);
Using the Coffee Break scenario, assume that headquarters checks with the database periodically to get the latest price list for the coffees it sells online. In this case, the listener is the PriceList object priceList at the Coffee Break web site, which must implement the RowSetListener methods cursorMoved, rowChanged, and rowSetChanged. The implementation of the cursorMoved method could be to do nothing because the position of the cursor does not affect the priceList object. The implementations for the rowChanged and rowSetChanged methods, on the other hand, must ascertain what changes have been made and update priceList accordingly.
The variable jrs holds nothing until RowSet objects are added to it. Note: Alternatively, you can use the constructor from the JoinRowSet implementation of your JDBC driver. However, implementations of the RowSet interface will differ from the reference implementation. These implementations will have different names and constructors. For example, the Oracle JDBC driver's implementation of the JoinRowSet interface is named oracle.jdbc.rowset.OracleJoinRowSet.
64
of a JOIN by being added to a JoinRowSet object. The point of providing a JoinRowSet implementation is to make it possible for disconnected RowSet objects to become part of a JOIN relationship. The owner of The Coffee Break chain of coffee houses wants to get a list of the coffees he buys from Acme, Inc. In order to do this, the owner will have to get information from two tables, COFFEES and SUPPLIERS. In the database world before RowSet technology, programmers would send the following query to the database:
String query = "SELECT COFFEES.COF_NAME " + "FROM COFFEES, SUPPLIERS " + "WHERE SUPPLIERS.SUP_NAME = Acme.Inc. " + "and " + "SUPPLIERS.SUP_ID = COFFEES.SUP_ID";
In the world of RowSet technology, you can accomplish the same result without having to send a query to the data source. You can add RowSet objects containing the data in the two tables to a JoinRowSet object. Then, because all the pertinent data is in the JoinRowSet object, you can perform a query on it to get the desired data. The following code fragment from JoinSample.testJoinRowSet creates two CachedRowSet objects, coffees populated with the data from the table COFFEES, and suppliers populated with the data from the table SUPPLIERS. The coffees and suppliers objects have to make a connection to the database to execute their commands and get populated with data, but after that is done, they do not have to reconnect again in order to form a JOIN.
coffees = new CachedRowSetImpl(); coffees.setCommand("SELECT * FROM COFFEES"); coffees.setUsername(settings.userName); coffees.setPassword(settings.password); coffees.setUrl(settings.urlString); coffees.execute();
suppliers = new CachedRowSetImpl(); suppliers.setCommand("SELECT * FROM SUPPLIERS"); suppliers.setUsername(settings.userName); suppliers.setPassword(settings.password); suppliers.setUrl(settings.urlString); suppliers.execute();
This line of code adds the coffees CachedRowSet to the jrs object and sets the second column of coffees (SUP_ID) as the match column. The line of code could also have used the column name rather that the column number.
jrs.addRowSet(coffees, "SUP_ID");
65
At this point, jrs has only coffees in it. The next RowSet object added to jrs will have to be able to form a JOIN with coffees, which is true of suppliers because both tables have the column SUP_ID. The following line of code adds suppliers to jrs and sets the column SUP_ID as the match column.
jrs.addRowSet(suppliers, 1);
Now jrs contains a JOIN between coffees and suppliers from which the owner can get the names of the coffees supplied by Acme, Inc. Because the code did not set the type of JOIN, jrs holds an inner JOIN, which is the default. In other words, a row in jrs combines a row in coffees and a row in suppliers. It holds the columns in coffees plus the columns in suppliers for rows in which the value in the COFFEES.SUP_ID column matches the value in SUPPLIERS.SUP_ID. The following code prints out the names of coffees supplied by Acme, Inc., where the String supplierName is equal to Acme, Inc. Note that this is possible because the column SUP_NAME, which is from suppliers, and COF_NAME, which is from coffees, are now both included in the JoinRowSet object jrs.
System.out.println("Coffees bought from " + supplierName + ": "); while (jrs.next()) { if (jrs.getString("SUP_NAME").equals(supplierName)) { String coffeeName = jrs.getString(1); System.out.println(" " + coffeeName); } }
The JoinRowSet interface provides constants for setting the type of JOIN that will be formed, but currently the only type that is implemented is JoinRowSet.INNER_JOIN.
66
following capabilities: Ability to limit the rows that are visible according to set criteria Ability to select which data is visible without being connected to a data source The following topics are covered: Defining Filtering Criteria in Predicate Objects Creating FilteredRowSet Objects Creating and Setting Predicate Objects Setting FilteredRowSet Objects with New Predicate Objects to Filter Data Further Updating FilteredRowSet Objects Inserting or Updating Rows Removing All Filters so All Rows Are Visible Deleting Rows
public StateFilter(int lo, int hi, int colNumber) { this.lo = lo; this.hi = hi; this.colNumber = colNumber; }
67
public StateFilter(int lo, int hi, String colName) { this.lo = lo; this.hi = hi; this.colName = colName; } public boolean evaluate(Object value, String columnName) { boolean evaluation = true; if (columnName.equalsIgnoreCase(this.colName)) { int columnValue = ((Integer)value).intValue(); if ((columnValue >= this.lo) && (columnValue <= this.hi)) { evaluation = true; } else { evaluation = false; } } return evaluation; } public boolean evaluate(Object value, int columnNumber) { boolean evaluation = true; if (this.colNumber == columnNumber) { int columnValue = ((Integer)value).intValue(); if ((columnValue >= this.lo) && (columnValue <= this.hi)) { evaluation = true; } else { evaluation = false; } } return evaluation; } public boolean evaluate(RowSet rs) { CachedRowSet frs = (CachedRowSet)rs; boolean evaluation = false; try { int columnValue = -1; if (this.colNumber > 0) { columnValue = frs.getInt(this.colNumber); } else if (this.colName != null) { columnValue = frs.getInt(this.colName); } else { return false; } if ((columnValue >= this.lo) && (columnValue <= this.hi)) { evaluation = true;
68
} } catch (SQLException e) { JDBCTutorialUtilities.printSQLException(e); return false; } catch (NullPointerException npe) { System.err.println("NullPointerException caught"); return false; } return evaluation; } }
This is a very simple implementation that checks the value in the column specified by either colName or colNumber to see if it is in the range of lo to hi, inclusive. The following line of code, from FilteredRowSetSample, creates a filter that allows only the rows where the STORE_ID column value indicates a value between 10000 and 10999, which indicates a California location:
StateFilter myStateFilter = new StateFilter(10000, 10999, 1);
Note that the StateFilter class just defined applies to one column. It is possible to have it apply to two or more columns by making each of the parameters arrays instead of single values. For example, the constructor for a Filter object could look like the following:
public Filter2(Object [] lo, Object [] hi, Object [] colNumber) { this.lo = lo; this.hi = hi; this.colNumber = colNumber; }
The first element in the colNumber object gives the first column in which the value will be checked against the first element in lo and the first element in hi. The value in the second column indicated by colNumber will be checked against the second elements in lo and hi, and so on. Therefore, the number of elements in the three arrays should be the same. The following code is what an implementation of the method evaluate(RowSet rs) might look like for a Filter2 object, in which the parameters are arrays:
public boolean evaluate(RowSet rs) { CachedRowSet crs = (CachedRowSet)rs; boolean bool1; boolean bool2; for (int i = 0; i < colNumber.length; i++) { if ((rs.getObject(colNumber[i] >= lo [i]) && (rs.getObject(colNumber[i] <= hi[i]) { bool1 = true; } else { bool2 = true; } if (bool2) { return false; } else { return true; } } }
The advantage of using a Filter2 implementation is that you can use parameters of any Object type and can check one column or multiple columns without having to write another implementation. However, you must pass an Object type, which means that you must convert a primitive type to its Object type. For example, if you use an int value for lo and hi, you must
69
convert the int value to an Integer object before passing it to the constructor. String objects are already Object types, so you do not have to convert them.
The implementation extends the BaseRowSet abstract class, so the frs object has the default properties defined in BaseRowSet. This means that frs is scrollable, updatable, does not show deleted rows, has escape processing turned on, and so on. Also, because the FilteredRowSet interface is a subinterface of CachedRowSet, Joinable, and WebRowSet, the frs object has the capabilities of each. It can operate as a disconnected RowSet object, can be part of a JoinRowSet object, and can read and write itself in XML format. Note: Alternatively, you can use the constructor from the WebRowSet implementation of your JDBC driver. However, implementations of the RowSet interface will differ from the reference implementation. These implementations will have different names and constructors. For example, the Oracle JDBC driver's implementation of the WebRowSet interface is named oracle.jdbc.rowset.OracleWebRowSet. You can use an instance of RowSetFactory, which is created from the class RowSetProvider, to create a FilteredRowSet object. See Using the RowSetFactory Interface in Using JdbcRowSet Objects for more information. Like other disconnected RowSet objects, the frs object must populate itself with data from a tabular data source, which is a relational database in the reference implementation. The following code fragment from FilteredRowSetSample sets the properties necessary to connect to a database to execute its command. Note that this code uses the DriverManager class to make a connection, which is done for convenience. Usually, it is better to use a DataSource object that has been registered with a naming service that implements the Java Naming and Directory Interface (JNDI):
frs.setCommand("SELECT * FROM COFFEE_HOUSES"); frs.setUsername(settings.userName); frs.setPassword(settings.password); frs.setUrl(settings.urlString);
The following line of code populates the frs objectwith the data stored in the COFFEE_HOUSE table:
frs.execute();
The method execute does all kinds of things in the background by calling on the RowSetReader object for frs, which creates a connection, executes the command for frs, populates frs with the data from the ResultSet object that is produced, and closes the connection. Note that if the table COFFEE_HOUSES had more rows than the frs object could hold in memory at one time, the CachedRowSet paging methods would have been used. In the scenario, the Coffee Break owner would have done the preceding tasks in the office and then imported or downloaded the information stored in the frs object to the coffee house comparison application. From now on, the frs object will operate independently without the benefit of a connection to the data source.
70
The following line of code uses the StateFilter class defined previously to create the object myStateFilter, which checks the column STORE_ID to determine which stores are in California (a store is in California if its ID number is between 10000 and 10999, inclusive):
StateFilter myStateFilter = new StateFilter(10000, 10999, 1);
To do the actual filtering, you call the method next, which in the reference implementation calls the appropriate version of the Predicate.evaluate method that you have implemented previously. If the return value is true, the row will be visible; if the return value is false, the row will not be visible.
Setting FilteredRowSet Objects with New Predicate Objects to Filter Data Further
You set multiple filters serially. The first time you call the method setFilter and pass it a Predicate object, you have applied the filtering criteria in that filter. After calling the method next on each row, which makes visible only those rows that satisfy the filter, you can call setFilter again, passing it a different Predicate object. Even though only one filter is set at a time, the effect is that both filters apply cumulatively. For example, the owner has retrieved a list of the Coffee Break stores in California by setting stateFilter as the Predicate object for frs. Now the owner wants to compare the stores in two California cities, San Francisco (SF in the table COFFEE_HOUSES) and Los Angeles (LA in the table). The first thing to do is to write a Predicate implementation that filters for stores in either SF or LA:
public class CityFilter implements Predicate { private String[] cities; private String colName = null; private int colNumber = -1; public CityFilter(String[] citiesArg, String colNameArg) { this.cities = citiesArg; this.colNumber = -1; this.colName = colNameArg; } public CityFilter(String[] citiesArg, int colNumberArg) { this.cities = citiesArg; this.colNumber = colNumberArg; this.colName = null; } public boolean evaluate Object valueArg, String colNameArg) { if (colNameArg.equalsIgnoreCase(this.colName)) { for (int i = 0; i < this.cities.length; i++) { if (this.cities[i].equalsIgnoreCase((String)valueArg)) { return true; } } } return false;
71
public boolean evaluate(Object valueArg, int colNumberArg) { if (colNumberArg == this.colNumber) { for (int i = 0; i < this.cities.length; i++) { if (this.cities[i].equalsIgnoreCase((String)valueArg)) { return true; } } } return false;
public boolean evaluate(RowSet rs) { if (rs == null) return false; try { for (int i = 0; i < this.cities.length; i++) { String cityName = null; if (this.colNumber > 0) { cityName = (String)rs.getObject(this.colNumber); } else if (this.colName != null) { cityName = (String)rs.getObject(this.colName); } else { return false; } if (cityName.equalsIgnoreCase(cities[i])) { return true; } } } catch (SQLException e) { return false; } return false;
} }
The following code fragment from FilteredRowSetSample sets the new filter and iterates through the rows in frs, printing out the rows where the CITY column contains either SF or LA. Note that frs currently contains only rows where the store is in California, so the criteria of the Predicate object state are still in effect when the filter is changed to another Predicate object. The code that follows sets the filter to the CityFilter object city. The CityFilter implementation uses arrays as parameters to the constructors to illustrate how that can be done:
public void testFilteredRowSet() { FilteredRowSet frs = null; StateFilter myStateFilter = new StateFilter(10000, 10999, 1); String[] cityArray = { "SF", "LA" }; CityFilter myCityFilter = new CityFilter(cityArray, 2); try { frs = new FilteredRowSetImpl(); frs.setCommand("SELECT * FROM COFFEE_HOUSES");
72
frs.setUsername(settings.userName); frs.setPassword(settings.password); frs.setUrl(settings.urlString); frs.execute(); System.out.println("\nBefore filter:"); FilteredRowSetSample.viewTable(this.con); System.out.println("\nSetting state filter:"); frs.beforeFirst(); frs.setFilter(myStateFilter); this.viewFilteredRowSet(frs); System.out.println("\nSetting city filter:"); frs.beforeFirst(); frs.setFilter(myCityFilter); this.viewFilteredRowSet(frs); } catch (SQLException e) { JDBCTutorialUtilities.printSQLException(e); }
The output should contain a row for each store that is in San Francisco, California or Los Angeles, California. If there were a row in which the CITY column contained LA and the STORE_ID column contained 40003, it would not be included in the list because it had already been filtered out when the filter was set to state. (40003 is not in the range of 10000 to 10999.)
73
frs.moveToCurrentRow();
If you were to iterate through the frs object using the method next, you would find a row for the new coffee house in San Francisco, California, but not for the store in San Francisco, Washington.
Deleting Rows
If the owner decides to close down or sell one of the Coffee Break coffee houses, the owner will want to delete it from the COFFEE_HOUSES table. The owner can delete the row for the underperforming coffee house as long as the row is visible. For example, given that the method setFilter has just been called with the argument null, there is no filter set on the frs object. This means that all rows are visible and can therefore be deleted. However, after the StateFilter object myStateFilter was set, which filtered out any state other than California, only stores located in California could be deleted. When the CityFilter object myCityFilter was set for the frs object, only coffee houses in San Francisco, California or Los Angeles, California could be deleted because they were in the only rows visible.
Although the priceList object has no data yet, it has the default properties of a BaseRowSet object. Its SyncProvider object is at first set to the RIOptimisticProvider implementation, which is the default for all disconnected RowSet objects. However, the WebRowSet implementation resets the SyncProvider object to be the RIXMLProvider implementation. You can use an instance of RowSetFactory, which is created from the RowSetProvider
74
class, to create a WebRowSet object. See Using the RowSetFactory Interface in Using JdbcRowSet Objects for more information. The Coffee Break headquarters regularly sends price list updates to its web site. This information on WebRowSet objects will show one way you can send the latest price list in an XML document. The price list consists of the data in the columns COF_NAME and PRICE from the table COFFEES. The following code fragment sets the properties needed and populates the priceList object with the price list data:
public void getPriceList(String username, String password) { priceList.setCommand("SELECT COF_NAME, PRICE FROM COFFEES"); priceList.setURL("jdbc:mySubprotocol:myDatabase"); priceList.setUsername(username); priceList.setPassword(password); priceList.execute(); // ... }
At this point, in addition to the default properties, the priceList object contains the data in the COF_NAME and PRICE columns from the COFFEES table and also the metadata about these two columns.
The following code writes the XML document representing priceList to the FileWriter object writer instead of to an OutputStream object. The FileWriter class is a convenience class for writing characters to a file.
java.io.FileWriter writer = new java.io.FileWriter("priceList.xml"); priceList.writeXml(writer);
The other two versions of the method writeXml let you populate a WebRowSet object with the contents of a ResultSet object before writing it to a stream. In the following line of code, the method writeXml reads the contents of the ResultSet object rs into the priceList object and then writes priceList to the FileOutputStream object oStream as an XML document.
priceList.writeXml(rs, oStream);
In the next line of code, the writeXml methodpopulates priceList with the contents of rs, but it writes the XML document to a FileWriter object instead of to an OutputStream object:
priceList.writeXml(rs, writer);
Note that you can read the XML description into a new WebRowSet object or into the same WebRowSet object that called the writeXml method. In the scenario, where the price list information is being sent from headquarters to the Web site, you would use a new WebRowSet object, as shown in the following lines of code:
WebRowSet recipient = new WebRowSetImpl(); java.io.FileReader reader = new java.io.FileReader("priceList.xml"); recipient.readXml(reader);
76 Properties
Calling the method writeXml on the priceList object would produce an XML document describing priceList. The properties section of this XML document would look like the following:
<properties> <command> select COF_NAME, PRICE from COFFEES </command> <concurrency>1008</concurrency> <datasource><null/></datasource> <escape-processing>true</escape-processing> <fetch-direction>1000</fetch-direction> <fetch-size>0</fetch-size> <isolation-level>2</isolation-level> <key-columns> <column>1</column> </key-columns> <map> </map> <max-field-size>0</max-field-size> <max-rows>0</max-rows> <query-timeout>0</query-timeout> <read-only>true</read-only> <rowset-type> ResultSet.TYPE_SCROLL_INSENSITIVE </rowset-type> <show-deleted>false</show-deleted> <table-name>COFFEES</table-name> <url>jdbc:mysql://localhost:3306/testdb</url> <sync-provider> <sync-provider-name> com.sun.rowset.providers.RIOptimisticProvider </sync-provider-name> <sync-provider-vendor> Sun Microsystems Inc. </sync-provider-vendor> <sync-provider-version> 1.0 </sync-provider-version> <sync-provider-grade> 2 </sync-provider-grade> <data-source-lock>1</data-source-lock> </sync-provider> </properties>
Notice that some properties have no value. For example, the datasource property is indicated with the <datasource/> tag, which is a shorthand way of saying <datasource></datasource>. No value is given because the url property is set. Any connections that are established will be done using this JDBC URL, so no DataSource object needs to be set. Also, the username and password properties are not listed because they must remain secret.
Metadata
The metadata section of the XML document describing a WebRowSet object contains information about the columns in that WebRowSet object. The following shows what this section looks like for the WebRowSet object priceList. Because the priceList object has two columns, the XML
77
document describing it has two <column-definition> elements. Each <columndefinition> element has subelements giving information about the column being described.
<metadata> <column-count>2</column-count> <column-definition> <column-index>1</column-index> <auto-increment>false</auto-increment> <case-sensitive>false</case-sensitive> <currency>false</currency> <nullable>0</nullable> <signed>false</signed> <searchable>true</searchable> <column-display-size> 32 </column-display-size> <column-label>COF_NAME</column-label> <column-name>COF_NAME</column-name> <schema-name></schema-name> <column-precision>32</column-precision> <column-scale>0</column-scale> <table-name>coffees</table-name> <catalog-name>testdb</catalog-name> <column-type>12</column-type> <column-type-name> VARCHAR </column-type-name> </column-definition> <column-definition> <column-index>2</column-index> <auto-increment>false</auto-increment> <case-sensitive>true</case-sensitive> <currency>false</currency> <nullable>0</nullable> <signed>true</signed> <searchable>true</searchable> <column-display-size> 12 </column-display-size> <column-label>PRICE</column-label> <column-name>PRICE</column-name> <schema-name></schema-name> <column-precision>10</column-precision> <column-scale>2</column-scale> <table-name>coffees</table-name> <catalog-name>testdb</catalog-name> <column-type>3</column-type> <column-type-name> DECIMAL </column-type-name> </column-definition> </metadata>
From this metadata section, you can see that there are two columns in each row. The first column is COF_NAME, which holds values of type VARCHAR. The second column is PRICE, which holds values of type REAL, and so on. Note that the column types are the data types used in the data source, not types in the Java programming language. To get or update values in the COF_NAME column, you use the methods getString or updateString, and the driver makes the conversion to the VARCHAR type, as it usually does.
78 Data
The data section gives the values for each column in each row of a WebRowSet object. If you have populated the priceList object and not made any changes to it, the data element of the XML document will look like the following. In the next section you will see how the XML document changes when you modify the data in the priceList object. For each row there is a <currentRow> element, and because priceList has two columns, each <currentRow> element contains two <columnValue> elements.
<data> <currentRow> <columnValue>Colombian</columnValue> <columnValue>7.99</columnValue> </currentRow> <currentRow> <columnValue> Colombian_Decaf </columnValue> <columnValue>8.99</columnValue> </currentRow> <currentRow> <columnValue>Espresso</columnValue> <columnValue>9.99</columnValue> </currentRow> <currentRow> <columnValue>French_Roast</columnValue> <columnValue>8.99</columnValue> </currentRow> <currentRow> <columnValue>French_Roast_Decaf</columnValue> <columnValue>9.99</columnValue> </currentRow> </data>
Inserting Rows
If the owner of the Coffee Break chain wants to add a new coffee to the price list, the code might look like this:
priceList.absolute(3); priceList.moveToInsertRow(); priceList.updateString(COF_NAME, "Kona"); priceList.updateFloat(PRICE, 8.99f); priceList.insertRow(); priceList.moveToCurrentRow();
In the reference implementation, an insertion is made immediately following the current row. In the preceding code fragment, the current row is the third row, so the new row would be added after the third row and become the new fourth row. To reflect this insertion, the XML document would have the following <insertRow> element added to it after the third <currentRow> element in the <data> element.
79
The <insertRow> element will look similar to the following.
<insertRow> <columnValue>Kona</columnValue> <columnValue>8.99</columnValue> </insertRow>
Deleting Rows
The owner decides that Espresso is not selling enough and should be removed from the coffees sold at The Coffee Break shops. The owner therefore wants to delete Espresso from the price list. Espresso is in the third row of the priceList object, so the following lines of code delete it:
priceList.absolute(3); priceList.deleteRow();
The following <deleteRow> element will appear after the second row in the data section of the XML document, indicating that the third row has been deleted.
<deleteRow> <columnValue>Espresso</columnValue> <columnValue>9.99</columnValue> </deleteRow>
Modifying Rows
The owner further decides that the price of Colombian coffee is too expensive and wants to lower it to .99 a pound. The following code sets the new price for Colombian coffee, which is in the first row, to .99 a pound:
priceList.first(); priceList.updateFloat(PRICE, 6.99);
The XML document will reflect this change in an <updateRow> element that gives the new value. The value for the first column did not change, so there is an <updateValue> element only for the second column:
<currentRow> <columnValue>Colombian</columnValue> <columnValue>7.99</columnValue> <updateRow>6.99</updateRow> </currentRow>
At this point, with the insertion of a row, the deletion of a row, and the modification of a row, the XML document for the priceList object would look like the following:
<data> <insertRow> <columnValue>Kona</columnValue> <columnValue>8.99</columnValue> </insertRow> <currentRow> <columnValue>Colombian</columnValue> <columnValue>7.99</columnValue> <updateRow>6.99</updateRow> </currentRow> <currentRow> <columnValue> Colombian_Decaf </columnValue> <columnValue>8.99</columnValue> </currentRow> <deleteRow> <columnValue>Espresso</columnValue> <columnValue>9.99</columnValue> </deleteRow> <currentRow>
80
<columnValue>French_Roast</columnValue> <columnValue>8.99</columnValue> </currentRow> <currentRow> <columnValue> French_Roast_Decaf </columnValue> <columnValue>9.99</columnValue> </currentRow> </data>
DISTINCT type: User-defined type based on a built-in type; for example: Constructed types: New types based on a given base type: REF(structured-type): Pointer that persistently denotes an instance of a structured type that resides in the database base-type ARRAY[n]: Array of n base-type elements Locators: Entities that are logical pointers to data that resides on the database server. A locator exists in the client computer and is a transient, logical pointer to data on the server. A locator typically refers to data that is too large to materialize on the client, such as images or audio. (Materialized views are query results that have been stored or "materialized" in advance as schema objects.) There are operators defined at the SQL level to retrieve randomly accessed pieces of the data denoted by the locator: LOCATOR(structured-type): Locator to a structured instance in the server LOCATOR(array): Locator to an array in the server LOCATOR(blob): Locator to a binary large object in the server
81
LOCATOR(clob): Locator to a character large object in the server Datalink: Type for managing data external to the data source. Datalink values are part of SQL MED (Management of External Data), a part of the SQL ANSI/ISO standard specification.
82
the Java type on which the DISTINCT data type is based. See Using DISTINCT Data Type for more information. For example, the following code fragment retrieves a SQL ARRAY value. For this example, suppose that the column SCORES in the table STUDENTS contains values of type ARRAY. The variable stmt is a Statement object.
ResultSet rs = stmt.executeQuery( "SELECT SCORES FROM STUDENTS " + "WHERE ID = 002238"); rs.next(); Array scores = rs.getArray("SCORES");
The variable scores is a logical pointer to the SQL ARRAY object stored in the table STUDENTS in the row for student 002238. If you want to store a value in the database, you use the appropriate set method. For example, the following code fragment, in which rs is a ResultSet object, stores a Clob object:
Clob notes = rs.getClob("NOTES"); PreparedStatement pstmt = con.prepareStatement( "UPDATE MARKETS SET COMMENTS = ? " + "WHERE SALES < 1000000"); pstmt.setClob(1, notes); pstmt.executeUpdate();
This code sets notes as the first parameter in the update statement being sent to the database. The Clob value designated by notes will be stored in the table MARKETS in column COMMENTS in every row where the value in the column SALES is less than one million.
83
Clob myClob = this.con.createClob(); Writer clobWriter = myClob.setCharacterStream(1); String str = this.readFile(fileName, clobWriter); System.out.println("Wrote the following: " + clobWriter.toString()); if (this.settings.dbms.equals("mysql")) { System.out.println( "MySQL, setting String in Clob " + "object with setString method"); myClob.setString(1, str); } System.out.println("Length of Clob: " + myClob.length()); String sql = "INSERT INTO COFFEE_DESCRIPTIONS " + "VALUES(?,?)"; pstmt = this.con.prepareStatement(sql); pstmt.setString(1, coffeeName); pstmt.setClob(2, myClob); pstmt.executeUpdate(); } catch (SQLException sqlex) { JDBCTutorialUtilities.printSQLException(sqlex); } catch (Exception ex) { System.out.println("Unexpected exception: " + ex.toString()); } finally { if (pstmt != null)pstmt.close(); }
The following line retrieves a stream (in this case a Writer object named clobWriter) that is used to write a stream of characters to the Clob Java object myClob. The method ClobSample.readFile writes this stream of characters; the stream is from the file specified by the String fileName. The method argument 1 indicates that the Writer object will start writing the stream of characters at the beginning of the Clob value:
Writer clobWriter = myClob.setCharacterStream(1);
The ClobSample.readFile method reads the file line-by-line specified by the file fileName and writes it to the Writer object specified by writerArg:
private String readFile(String fileName, Writer writerArg) throws FileNotFoundException, IOException { BufferedReader br = new BufferedReader(new FileReader(fileName)); String nextLine = ""; StringBuffer sb = new StringBuffer(); while ((nextLine = br.readLine()) != null) { System.out.println("Writing: " + nextLine); writerArg.write(nextLine); sb.append(nextLine); } // Convert the content into to a string String clobData = sb.toString(); // Return the data. return clobData; }
The following excerpt creates a PreparedStatement object pstmt that inserts the Clob Java
84
object myClob into COFFEE_DESCRIPTIONS:
PreparedStatement pstmt = null; // ... String sql = "INSERT INTO COFFEE_DESCRIPTIONS VALUES(?,?)"; pstmt = this.con.prepareStatement(sql); pstmt.setString(1, coffeeName); pstmt.setClob(2, myClob); pstmt.executeUpdate();
The following line retrieves the Clob Java value from the ResultSet object rs:
myClob = rs.getClob(1);
The following line retrieves a substring from the myClob object. The substring begins at the first character of the value of myClob and has up to the number of consecutive characters specified in numChar, where numChar is an integer.
description = myClob.getSubString(1, numChar);
85
SQLXML objects remain valid for at least the duration of the transaction in which they are created, unless their free method is invoked.
86
InputStream or a Reader object that can be passed directly to an XML parser. The following excerpt obtains an InputStream object from an SQLXML Object and then processes the stream using a DOM (Document Object Model) parser:
SQLXML sqlxml = rs.getSQLXML(column); InputStream binaryStream = sqlxml.getBinaryStream(); DocumentBuilder parser = DocumentBuilderFactory.newInstance().newDocumentBuilder(); Document result = parser.parse(binaryStream);
The getSource method returns a javax.xml.transform.Source object. Sources are used as input to XML parsers and XSLT transformers. The following excerpt retrieves and parses the data from a SQLXML object using the SAXSource object returned by invoking the getSource method:
SQLXML xmlVal= rs.getSQLXML(1); SAXSource saxSource = sqlxml.getSource(SAXSource.class); XMLReader xmlReader = saxSource.getXMLReader(); xmlReader.setContentHandler(myHandler); xmlReader.parse(saxSource.getInputSource());
The updateSQLXML method can be used to update a column value in an updatable result set. If the java.xml.transform.Result, Writer, or OutputStream object for the SQLXML object has not been closed prior to calling setSQLXML or updateSQLXML, a SQLException will be thrown.
The following excerpt uses the setCharacterStream method to obtain a java.io.Writer object in order to initialize a SQLXML object:
SQLXML sqlxml = con.createSQLXML(); Writer out= sqlxml.setCharacterStream(); BufferedReader in = new BufferedReader(new FileReader("xml/foo.xml")); String line = null; while((line = in.readLine() != null) {
87
out.write(line); }
Similarly, the SQLXML setString method can be used to initialize a SQLXML object. If an attempt is made to call the setString, setBinaryStream, setCharacterStream, and setResult methods on a SQLXML object that has previously been initialized, a SQLException will be thrown. If more than one call to the methods setBinaryStream, setCharacterStream, and setResult occurs for the same SQLXML object, a SQLException is thrown and the previously returned javax.xml.transform.Result, Writer, or OutputStream object is not affected.
Sample Code
MySQL and Java DB and their respective JDBC drivers do not fully support the SQLXML JDBC data type as described on in this section. However, the sample RSSFeedsTable demonstrates how to handle XML data with MySQL and Java DB. The owner of The Coffee Break follows several RSS feeds from various web sites that cover restaurant and beverage industry news. An RSS (Really Simple Syndication or Rich Site Summary) feed is an XML document that contains a series of articles and associated metadata, such as the date of publication and author for each article. The owner would like to store these RSS feeds into a database table, including the RSS feed from The Coffee Break's blog. The file rss-the-coffee-break-blog.xml is an example RSS feed from The Coffee Break's blog.
MySQL does not support the XML data type. Instead, this sample stores XML data in a column of type LONGTEXT, which is a CLOB SQL data type. MySQL has four CLOB data types; the LONGTEXT data type holds the greatest amount of characters among the four. The method RSSFeedsTable.addRSSFeed adds an RSS feed to the RSS_FEEDS table. The first statements of this method converts the RSS feed (which is represented by an XML file in this sample) into an object of type org.w3c.dom.Document, which represents a DOM (Document Object Model) document. This class, along with classes and interfaces contained in the package javax.xml, contain methods that enable you to manipulate XML data content. For example, the following statement uses an XPath expression to retrieve the title of the RSS feed from the Document object:
Node titleElement = (Node)xPath.evaluate("/rss/channel/title[1]", doc, XPathConstants.NODE);
88
The XPath expression /rss/channel/title[1] retrieves the contents of the first <title> element. For the file rss-the-coffee-break-blog.xml, this is the string The Coffee Break Blog. The following statements add the RSS feed to the table RSS_FEEDS:
// // // // For databases that support the SQLXML data type, this creates a SQLXML object from org.w3c.dom.Document.
System.out.println("Adding XML file " + fileName); String insertRowQuery = "insert into RSS_FEEDS " + "(RSS_NAME, RSS_FEED_XML) values " + "(?, ?)"; insertRow = con.prepareStatement(insertRowQuery); insertRow.setString(1, titleString); System.out.println("Creating SQLXML object with MySQL"); rssData = con.createSQLXML(); System.out.println("Creating DOMResult object"); DOMResult dom = (DOMResult)rssData.setResult(DOMResult.class); dom.setNode(doc); insertRow.setSQLXML(2, rssData); System.out.println("Running executeUpdate()"); insertRow.executeUpdate();
The RSSFeedsTable.viewTable method retrieves the contents of RSS_FEEDS. For each row, the method creates an object of type org.w3c.dom.Document named doc in which to store the XML content in the column RSS_FEED_XML. The method retrieves the XML content and stores it in an object of type SQLXML named rssFeedXML. The contents of rssFeedXML are parsed and stored in the doc object.
Java DB supports the XML data type, but it does not support the SQLXML JDBC data type. Consequently, you must convert any XML data to a character format, and then use the Java DB operator XMLPARSE to convert it to the XML data type. The RSSFeedsTable.addRSSFeed method adds an RSS feed to the RSS_FEEDS table. The first statements of this method convert the RSS feed (which is represented by an XML file in this sample) into an object of type org.w3c.dom.Document. This is described in the section Working with XML Data in MySQL. The RSSFeedsTable.addRSSFeed method converts the RSS feed to a String object with the method JDBCTutorialUtilities.convertDocumentToString. Java DB has an operator named XMLPARSE that parses a character string representation into a Java DB XML value, which is demonstrated by the following excerpt:
String insertRowQuery = "insert into RSS_FEEDS " +
89
"(RSS_NAME, RSS_FEED_XML) values " + "(?, xmlparse(document cast " + "(? as clob) preserve whitespace))";
The XMLPARSE operator requires that you convert the character representation of the XML document into a string data type that Java DB recognizes. In this example, it converts it into a CLOB data type. In addition, the XMLPARSE operator requires that Apache Xalan be listed in your Java class path. See Getting Started and the Java DB documentation for more information about Apache Xalan and Java DB requirements. The method RSSFeedsTable.viewTable retrieves the contents of RSS_FEEDS. Because Java DB does not support the JDBC data type SQLXML you must retrieve the XML content as a string. Java DB has an operator named XMLSERIALIZE that converts an XML type to a character type:
String query = "select RSS_NAME, " + "xmlserialize " + "(RSS_FEED_XML as clob) " + "from RSS_FEEDS";
As with the XMLPARSE operator, the XMLSERIALIZE operator requires that Apache Xalan be listed in your Java class path.
The Oracle Database JDBC driver implements the java.sql.Array interface with the oracle.sql.ARRAY class.
90
objects without having to bring all of their data from the database server to your client computer. An Array object materializes the SQL ARRAY it represents as either a result set or a Java array. The following excerpt retrieves the SQL ARRAY value in the column ZIPS and assigns it to the java.sql.Array object z object. The excerpt retrieves the contents of z and stores it in zips, a Java array that contains objects of type String. The excerpt iterates through the zips array and checks that each postal (zip) code is valid. This code assumes that the class ZipCode has been defined previously with the method isValid returning true if the given zip code matches one of the zip codes in a master list of valid zip codes:
ResultSet rs = stmt.executeQuery( "SELECT region_name, zips FROM REGIONS"); while (rs.next()) { Array z = rs.getArray("ZIPS"); String[] zips = (String[])z.getArray(); for (int i = 0; i < zips.length; i++) { if (!ZipCode.isValid(zips[i])) { // ... // Code to display warning } } }
In the following statement, the ResultSet method getArray returns the value stored in the column ZIPS of the current row as the java.sql.Array object z:
Array z = rs.getArray("ZIPS");
The variable z contains a locator, which is a logical pointer to the SQL ARRAY on the server; it does not contain the elements of the ARRAY itself. Being a logical pointer, z can be used to manipulate the array on the server. In the following line, getArray is the Array.getArray method, not the ResultSet.getArray method used in the previous line. Because the Array.getArray method returns an Object in the Java programming language and because each zip code is a String object, the result is cast to an array of String objects before being assigned to the variable zips.
String[] zips = (String[])z.getArray();
The Array.getArray method materializes the SQL ARRAY elements on the client as an array of String objects. Because, in effect, the variable zips contains the elements of the array, it is possible to iterate through zips in a for loop, looking for zip codes that are not valid.
Similarly, use the methods PreparedStatement.updateArray and PreparedStatement.updateObject to update a column in a table with an Array value.
91
Some databases use an alternate syntax for creating a DISTINCT data type, which is shown in the following line of code:
CREATE DISTINCT TYPE STATE AS CHAR(2);
If one syntax does not work, you can try the other. Alternatively, you can check the documentation for your driver to see the exact syntax it expects. These statements create a new data type, STATE, which can be used as a column value or as the value for an attribute of a SQL structured type. Because a value of type STATE is in reality a value that is two CHAR types, you use the same method to retrieve it that you would use to retrieve a CHAR value, that is, getString. For example, assuming that the fourth column of ResultSet rs stores values of type STATE, the following line of code retrieves its value:
String state = rs.getString(4);
Similarly, you would use the method setString to store a STATE value in the database and the method updateString to modify its value.
92
In this statement, the new type ADDRESS has five attributes, which are analogous to fields in a Java class. The attribute NUM is an INTEGER, the attribute STREET is a VARCHAR(40), the attribute CITY is a VARCHAR(40), the attribute STATE is a CHAR(2), and the attribute ZIP is a CHAR(5). The following excerpt, in which con is a valid Connection object, sends the definition of ADDRESS to the database:
String createAddress = "CREATE TYPE ADDRESS " + "(NUM INTEGER, STREET VARCHAR(40), " + "CITY VARCHAR(40), STATE CHAR(2), ZIP CHAR(5))"; Statement stmt = con.createStatement(); stmt.executeUpdate(createAddress);
Now the ADDRESS structured type is registered with the database as a data type, and the owner can use it as the data type for a table column or an attribute of a structured type.
93
Or, as noted earlier, for some drivers the definition might look like this:
CREATE DISTINCT TYPE PHONE_NO AS CHAR(10);
A DISTINCT type is always based on another data type, which must be a predefined type. In other words, a DISTINCT type cannot be based on a user-defined type (UDT). To retrieve or set a value that is a DISTINCT type, use the appropriate method for the underlying type (the type on which it is based). For example, to retrieve an instance of PHONE_NO, which is based on a CHAR type, you would use the method getString because that is the method for retrieving a CHAR. Assuming that a value of type PHONE_NO is in the fourth column of the current row of the ResultSet object rs, the following line of code retrieves it:
String phoneNumber = rs.getString(4);
Similarly, the following line of code sets an input parameter that has type PHONE_NO for a prepared statement being sent to the database:
pstmt.setString(1, phoneNumber);
Adding on to the previous code fragment, the definition of PHONE_NO will be sent to the database with the following line of code:
stmt.executeUpdate( "CREATE TYPE PHONE_NO AS CHAR(10)");
After registering the type PHONE_NO with the database, the owner can use it as a column type in a table or as the data type for an attribute in a structured type. The definition of MANAGER in the following SQL statement uses PHONE_NO as the data type for the attribute PHONE:
CREATE TYPE MANAGER ( MGR_ID INTEGER, LAST_NAME VARCHAR(40), FIRST_NAME VARCHAR(40), PHONE PHONE_NO );
Reusing stmt, defined previously, the following code fragment sends the definition of the structured type MANAGER to the database:
String createManager = "CREATE TYPE MANAGER " + "(MGR_ID INTEGER, LAST_NAME " + "VARCHAR(40), " + "FIRST_NAME VARCHAR(40), " + "PHONE PHONE_NO)"; stmt.executeUpdate(createManager);
94
type that it references, it is stored in a special table together with its associated instance. A programmer does not create REF types directly but rather creates the table that will store instances of a particular structured type that can be referenced. Every structured type that is to be referenced will have its own table. When you insert an instance of the structured type into the table, the database automatically creates a REF instance. For example, to contain instances of MANAGER that can be referenced, the owner created the following special table using SQL:
CREATE TABLE MANAGERS OF MANAGER (OID REF(MANAGER) VALUES ARE SYSTEM GENERATED);
This statement creates a table with the special column OID, which stores values of type REF(MANAGER). Each time an instance of MANAGER is inserted into the table, the database will generate an instance of REF(MANAGER) and store it in the column OID. Implicitly, an additional column stores each attribute of MANAGER that has been inserted into the table, as well. For example, the following code fragment shows how the entrepreneur created three instances of the MANAGER structured type to represent three managers:
INSERT INTO MANAGERS ( MGR_ID, LAST_NAME, FIRST_NAME, PHONE) VALUES ( 000001, 'MONTOYA', 'ALFREDO', '8317225600' ); INSERT INTO MANAGERS ( MGR_ID, LAST_NAME, FIRST_NAME, PHONE) VALUES ( 000002, 'HASKINS', 'MARGARET', '4084355600' ); INSERT INTO MANAGERS ( MGR_ID, LAST_NAME, FIRST_NAME, PHONE) VALUES ( 000003, 'CHEN', 'HELEN', '4153785600' );
The table MANAGERS will now have three rows, one row for each manager inserted so far. The column OID will contain three unique object identifiers of type REF(MANAGER), one for each instance of MANAGER. These object identifiers were generated automatically by the database and will be permanently stored in the table MANAGERS. Implicitly, an additional column stores each attribute of MANAGER. For example, in the table MANAGERS, one row contains a REF(MANAGER) that references Alfredo Montoya, another row contains a REF(MANAGER) that references Margaret Haskins, and a third row contains a REF(MANAGER) that references Helen Chen. To access a REF(MANAGER) instance, you select it from its table. For example, the owner retrieved the reference to Alfredo Montoya, whose ID number is 000001, with the following code fragment:
95
String selectMgr = "SELECT OID FROM MANAGERS " + "WHERE MGR_ID = 000001"; ResultSet rs = stmt.executeQuery(selectMgr); rs.next(); Ref manager = rs.getRef("OID");
Now the variable manager can be used as a column value that references Alfredo Montoya.
96
"'ALFREDO', " + "'8317225600')"; String insertManager2 = "INSERT INTO MANAGERS " + "(MGR_ID, LAST_NAME, " + "FIRST_NAME, PHONE) " + "VALUES " + "(000002, 'HASKINS', " + "'MARGARET', " + "'4084355600')"; String insertManager3 = "INSERT INTO MANAGERS " + "(MGR_ID, LAST_NAME, " + "FIRST_NAME, PHONE) " + "VALUES " + "(000003, 'CHEN', 'HELEN', " + "'4153785600')"; con = myJDBCTutorialUtilities.getConnection(); con.setAutoCommit(false); stmt = con.createStatement(); stmt.executeUpdate(createManagers); stmt.addBatch(insertManager1); stmt.addBatch(insertManager2); stmt.addBatch(insertManager3); int [] updateCounts = stmt.executeBatch(); con.commit(); System.out.println("Update count for: "); for (int i = 0; i < updateCounts.length; i++) { System.out.print(" command " + (i + 1) + " = "); System.out.println(updateCounts[i]); } } catch(BatchUpdateException b) { System.err.println("-----BatchUpdateException-----"); System.err.println("Message: " + b.getMessage()); System.err.println("SQLState: " + b.getSQLState()); System.err.println("Vendor: " + b.getErrorCode()); System.err.print("Update counts for " + "successful commands: int [] rowsUpdated = b.getUpdateCounts(); for (int i = 0; i < rowsUpdated.length; i++) { System.err.print(rowsUpdated[i] + " "); } System.err.println(""); } catch(SQLException ex) { System.err.println("------SQLException------"); System.err.println("Error message: " + ex.getMessage()); System.err.println("SQLState: " + ex.getSQLState()); System.err.println("Vendor: " + ex.getErrorCode()); } finally { if (stmt != null) { stmt.close(); } JDBCTutorialUtilities.closeConnection(con); }
");
} }
97
With the new data types defined, the following SQL statement creates the table STORES:
The following goes through each column and the value inserted into it. This column is type INTEGER, and the number 100001 is an INTEGER type, similar to entries made before in the tables COFFEES and SUPPLIERS.
LOCATION: ADDRESS(888, 'Main_Street', 'Rancho_Alegre', 'CA', '94049')
The type for this column is the structured type ADDRESS, and this value is the constructor for an instance of ADDRESS. When we sent the definition of ADDRESS was sent to the database, one of the things it did was to create a constructor for the new type. The comma-separated values in parentheses are the initialization values for the attributes of the ADDRESS type, and they must appear in the same order in which the attributes were listed in the definition of the ADDRESS type. 888 is the value for the attribute NUM, which is an INTEGER value. "Main_Street" is the value for STREET, and "Rancho_Alegre" is the value for CITY, with both attributes being of type VARCHAR(40). The value for the attribute STATE is "CA", which is of type CHAR(2), and the value for the attribute ZIP is "94049", which is of type CHAR(5).
COF_TYPES: COF_ARRAY( 'Colombian', 'French_Roast', 'Espresso', 'Colombian_Decaf', 'French_Roast_Decaf'),
98
The column COF_TYPES is of type COF_ARRAY with a base type of VARCHAR(40), and the comma-separated values between parentheses are the String objects that are the array elements. The owner defined the type COF_ARRAY as having a maximum of 10 elements. This array has 5 elements because the entrepreneur supplied only 5 String objects for it.
MGR: SELECT OID FROM MANAGERS WHERE MGR_ID = 000001
The column MGR is type REF(MANAGER), which means that a value in this column must be a reference to the structured type MANAGER. All of the instances of MANAGER are stored in the table MANAGERS. All of the instances of REF(MANAGER) are also stored in this table, in the column OID. The manager for the store described in this table row is Alfredo Montoya, and his information is stored in the instance of MANAGER that has 100001 for the attribute MGR_ID. To get the REF(MANAGER) instance associated with the MANAGER object for Alfredo Montoya, select the column OID that is in the row where MGR_ID is 100001 in the table MANAGERS. The value that will be stored in the MGR column of the STORES table (the REF(MANAGER) value) is the value the DBMS generated to uniquely identify this instance of the MANAGER structured type. Send the preceding SQL statement to the database with the following code fragment:
String insertMgr = "INSERT INTO STORES VALUES " + "(100001, " + "ADDRESS(888, 'Main_Street', " + "'Rancho_Alegre', 'CA', " + "'94049'), " + "COF_ARRAY('Colombian', " + "'French_Roast', 'Espresso', " + "'Colombian_Decaf', " + "'French_Roast_Decaf'}, " + "SELECT OID FROM MANAGERS " + "WHERE MGR_ID = 000001)"; stmt.executeUpdate(insertMgr);
However, because you are going to send several INSERT INTO statements, it will be more efficient to send them all together as a batch update, as in the following code example:
package com.oracle.tutorial.jdbc; import java.sql.*; public class InsertStores { public static void main(String args[]) { JDBCTutorialUtilities myJDBCTutorialUtilities; Connection myConnection = null; if (args[0] == null) { System.err.println( "Properties file " + "not specified " + "at command line"); return; } else { try { myJDBCTutorialUtilities = new JDBCTutorialUtilities(args[0]); } catch (Exception e) { System.err.println( "Problem reading " +
99
"properties file " + args[0]); e.printStackTrace(); return;
} }
Connection con = null; Statement stmt = null; try { con = myJDBCTutorialUtilities.getConnection(); con.setAutoCommit(false); stmt = con.createStatement(); String insertStore1 = "INSERT INTO STORES VALUES (" + "100001, " + "ADDRESS(888, 'Main_Street', " + "'Rancho_Alegre', 'CA', " + "'94049'), " + "COF_ARRAY('Colombian', " + "'French_Roast', " + "'Espresso', " + "'Colombian_Decaf', " + "'French_Roast_Decaf'), " + "(SELECT OID FROM MANAGERS " + "WHERE MGR_ID = 000001))"; stmt.addBatch(insertStore1); String insertStore2 = "INSERT INTO STORES VALUES (" + "100002, " + "ADDRESS(1560, 'Alder', " + "'Ochos_Pinos', " + "'CA', '94049'), " + "COF_ARRAY('Colombian', " + "'French_Roast', " + "'Espresso', " + "'Colombian_Decaf', " + "'French_Roast_Decaf', " + "'Kona', 'Kona_Decaf'), " + "(SELECT OID FROM MANAGERS " + "WHERE MGR_ID = 000001))"; stmt.addBatch(insertStore2); String insertStore3 = "INSERT INTO STORES VALUES (" + "100003, " + "ADDRESS(4344, " + "'First_Street', " + "'Verona', " + "'CA', '94545'), " + "COF_ARRAY('Colombian', " + "'French_Roast', " + "'Espresso', " + "'Colombian_Decaf', " +
100
"'French_Roast_Decaf', " + "'Kona', 'Kona_Decaf'), " + "(SELECT OID FROM MANAGERS " + "WHERE MGR_ID = 000002))"; stmt.addBatch(insertStore3); String insertStore4 = "INSERT INTO STORES VALUES (" + "100004, " + "ADDRESS(321, 'Sandy_Way', " + "'La_Playa', " + "'CA', '94544'), " + "COF_ARRAY('Colombian', " + "'French_Roast', " + "'Espresso', " + "'Colombian_Decaf', " + "'French_Roast_Decaf', " + "'Kona', 'Kona_Decaf'), " + "(SELECT OID FROM MANAGERS " + "WHERE MGR_ID = 000002))"; stmt.addBatch(insertStore4); String insertStore5 = "INSERT INTO STORES VALUES (" + "100005, " + "ADDRESS(1000, 'Clover_Road', " + "'Happyville', " + "'CA', '90566'), " + "COF_ARRAY('Colombian', " + "'French_Roast', " + "'Espresso', " + "'Colombian_Decaf', " + "'French_Roast_Decaf'), " + "(SELECT OID FROM MANAGERS " + "WHERE MGR_ID = 000003))"; stmt.addBatch(insertStore5); int [] updateCounts = stmt.executeBatch(); ResultSet rs = stmt.executeQuery( "SELECT * FROM STORES"); System.out.println("Table STORES after insertion:"); System.out.println("STORE_NO " + "LOCATION " + "COF_TYPE " + "MGR"); while (rs.next()) { int storeNo = rs.getInt("STORE_NO"); Struct location = (Struct)rs.getObject("LOCATION"); Object[] locAttrs = location.getAttributes(); Array coffeeTypes = rs.getArray("COF_TYPE"); String[] cofTypes = (String[])coffeeTypes.getArray(); Ref managerRef = rs.getRef("MGR"); PreparedStatement pstmt = con.prepareStatement( "SELECT MANAGER " + "FROM MANAGERS " + "WHERE OID = ?");
101
pstmt.setRef(1, managerRef); ResultSet rs2 = pstmt.executeQuery(); rs2.next(); Struct manager = (Struct)rs2.getObject("MANAGER"); Object[] manAttrs = manager.getAttributes(); System.out.print(storeNo + " System.out.print( locAttrs[0] + " " + locAttrs[1] + " " + locAttrs[2] + ", " + locAttrs[3] + " " + locAttrs[4] + " "); ");
for (int i = 0; i < cofTypes.length; i++) System.out.print( cofTypes[i] + " "); System.out.println( manAttrs[1] + ", " + manAttrs[2]); rs2.close(); pstmt.close();
rs.close(); } catch(BatchUpdateException b) { System.err.println("-----BatchUpdateException-----"); System.err.println("SQLState: " + b.getSQLState()); System.err.println("Message: " + b.getMessage()); System.err.println("Vendor: " + b.getErrorCode()); System.err.print("Update counts: "); int [] updateCounts = b.getUpdateCounts(); for (int i = 0; i < updateCounts.length; i++) { System.err.print(updateCounts[i] + " "); } System.err.println(""); } catch(SQLException ex) { System.err.println("SQLException: " + ex.getMessage()); System.err.println("SQLState: " + ex.getSQLState()); System.err.println("Message: " + ex.getMessage()); System.err.println("Vendor: " + ex.getErrorCode()); } finally { if (stmt != null) { stmt.close(); } JDBCTutorialUtilities.closeConnection(con); } } } }
102
changes to the database. The owner has decided to use a custom mapping for the structured type ADDRESS. This enables the owner to make changes to the Java class that maps the ADDRESS type. The Java class will have a field for each attribute of ADDRESS. The name of the class and the names of its fields can be any valid Java identifier. The following topics are covered: Implementing SQLData Using a Connection's Type Map Using Your Own Type Map
Implementing SQLData
The first thing required for a custom mapping is to create a class that implements the interface SQLData. The SQL definition of the structured type ADDRESS looks like this:
CREATE TYPE ADDRESS ( NUM INTEGER, STREET VARCHAR(40), CITY VARCHAR(40), STATE CHAR(2), ZIP CHAR(5) );
A class that implements the SQLData interface for the custom mapping of the ADDRESS type might look like this:
public class Address implements SQLData { public int num; public String street; public String city; public String state; public String zip; private String sql_type; public String getSQLTypeName() { return sql_type; } public void readSQL(SQLInput stream, String type) throws SQLException { sql_type = type; num = stream.readInt(); street = stream.readString(); city = stream.readString(); state = stream.readString(); zip = stream.readString(); } public void writeSQL(SQLOutput stream) throws SQLException { stream.writeInt(num); stream.writeString(street); stream.writeString(city); stream.writeString(state); stream.writeString(zip); }
103
Whenever you call the getObject method to retrieve an instance of the ADDRESS type, the driver will check the type map associated with the connection and see that it has an entry for ADDRESS. The driver will note the Class object for the Address class, create an instance of it, and do many other things in the background to map ADDRESS to Address. You do not have to do anything more than generate the class for the mapping and then make an entry in a type map to let the driver know that there is a custom mapping. The driver will do all the rest. The situation is similar for storing a structured type that has a custom mapping. When you call the method setObject, the driver will check to see if the value to be set is an instance of a class that implements the interface SQLData. If it is (meaning that there is a custom mapping), the driver will use the custom mapping to convert the value to its SQL counterpart before returning it to the database. Again, the driver does the custom mapping behind the scenes; all you need to do is supply the method setObject with a parameter that has a custom mapping. You will see an example of this later in this section. Look at the difference between working with the standard mapping, a Struct object, and the custom mapping, a class in the Java programming language. The following code fragment shows the standard mapping to a Struct object, which is the mapping the driver uses when there is no entry in the connection's type map.
ResultSet rs = stmt.executeQuery( "SELECT LOCATION " + "WHERE STORE_NO = 100003"); rs.next(); Struct address = (Struct)rs.getObject("LOCATION");
The variable address contains the following attribute values: 4344, "First_Street", "Verona", "CA", "94545". The following code fragment shows what happens when there is an entry for the structured type ADDRESS in the connection's type map. Remember that the column LOCATION stores values of type ADDRESS.
ResultSet rs = stmt.executeQuery( "SELECT LOCATION " + "WHERE STORE_NO = 100003"); rs.next(); Address store_3 = (Address)rs.getObject("LOCATION");
The variable store_3 is now an instance of the class Address, with each attribute value being the current value of one of the fields of Address. Note that you must remember to convert the object retrieved by the getObject method to an Address object before assigning it to store_3. Note also that store_3 must be an Address object. Compare working with the Struct object to working with the instance of the Address class. Suppose the store moved to a better location in the neighboring town and therefore you must update
104
the database. With the custom mapping, reset the fields of store_3, as in the following code fragment:
ResultSet rs = stmt.executeQuery( "SELECT LOCATION " + "WHERE STORE_NO = 100003"); rs.next(); Address store_3 = (Address)rs.getObject("LOCATION"); store_3.num = 1800; store_3.street = "Artsy_Alley"; store_3.city = "Arden"; store_3.state = "CA"; store_3.zip = "94546"; PreparedStatement pstmt = con.prepareStatement( "UPDATE STORES " + "SET LOCATION = ? " + "WHERE STORE_NO = 100003"); pstmt.setObject(1, store_3); pstmt.executeUpdate();
Values in the column LOCATION are instances of the ADDRESS type. The driver checks the connection's type map and sees that there is an entry linking ADDRESS with the class Address and consequently uses the custom mapping indicated in Address. When the code calls the method setObject with the variable store_3 as the second parameter, the driver checks and sees that store_3 represents an instance of the class Address, which implements the interface SQLData for the structured type ADDRESS, and again automatically uses the custom mapping. Without a custom mapping for ADDRESS, the update would look more like this:
PreparedStatement pstmt = con.prepareStatement( "UPDATE STORES " + "SET LOCATION.NUM = 1800, " + "LOCATION.STREET = 'Artsy_Alley', " + "LOCATION.CITY = 'Arden', " + "LOCATION.STATE = 'CA', " + "LOCATION.ZIP = '94546' " + "WHERE STORE_NO = 100003"); pstmt.executeUpdate;
105
URL, uniform resource locator, is a pointer to a resource on the World Wide Web. A resource can be something as simple as a file or a directory, or it can be a reference to a more complicated object, such as a query to a database or to a search engine. The following topics are covered: Storing References to External Data Retrieving References to External Data
106
try { stmt = con.createStatement(); ResultSet rs = stmt.executeQuery(query); if ( rs.next() ) { String documentName = null; java.net.URL url = null; documentName = rs.getString(1); // Retrieve the value as a URL object. url = rs.getURL(2); if (url != null) { // Retrieve the contents // from the URL URLConnection myURLConnection = url.openConnection(proxy); BufferedReader bReader = new BufferedReader( new InputStreamReader( myURLConnection. getInputStream())); System.out.println("Document name: " + documentName); String pageContent = null; while ((pageContent = bReader.readLine()) != null ) { // Print the URL contents System.out.println(pageContent); } } else { System.out.println("URL is null"); } } } catch (SQLException e) { JDBCTutorialUtilities.printSQLException(e); } catch(IOException ioEx) { System.out.println("IOException caught: " + ioEx.toString()); } catch (Exception ex) { System.out.println("Unexpected exception"); ex.printStackTrace(); } finally { if (stmt != null) { stmt.close(); } }
The sample DatalinkSample stores the Oracle URL, http://www.oracle.com in the table DATA_REPOSITORY. Afterward, it displays the contents of all documents referred to by the URLs stored in DATA_REPOSITORY, which includes the Oracle home page, http://www.oracle.com. The sample retrieves the URL from the result set as a java.net.URL object with the following statement:
url = rs.getURL(2);
The sample accesses the data referred to by the URL object with the following statements:
URLConnection myURLConnection = url.openConnection(proxy); BufferedReader bReader = new BufferedReader(
107
new InputStreamReader( myURLConnection.getInputStream())); System.out.println("Document name: " + documentName); String pageContent = null; while ((pageContent = bReader.readLine()) != null ) { // Print the URL contents System.out.println(pageContent); }
The method URLConnection.openConnection can take no arguments, which means that the URLConnection represents a direct connection to the Internet. If you require a proxy server to connect to the Internet, the openConnection method accepts a java.net.Proxy object as an argument. The following statements demonstrate how to create an HTTP proxy with the server name www-proxy.example.com and port number 80:
Proxy myProxy; InetSocketAddress myProxyServer; myProxyServer = new InetSocketAddress("www-proxy.example.com", 80); myProxy = new Proxy(Proxy.Type.HTTP, myProxyServer);
You can also update a column with a specific RowId object in an updatable ResultSet object:
108
A RowId object value is typically not portable between data sources and should be considered as specific to the data source when using the set or update method in PreparedStatement and ResultSet objects, respectively. It is therefore inadvisable to get a RowId object from a ResultSet object with a connection to one data source and then attempt to use the same RowId object in a unrelated ResultSet object with a connection to a different data source.
DatabaseMetaData dbMetaData = conn.getMetaData(); RowIdLifetime lifetime = dbMetaData.getRowIdLifetime(); switch (lifetime) { case ROWID_UNSUPPORTED: System.out.println("ROWID type not supported"); break; case ROWID_VALID_FOREVER: System.out.println("ROWID has unlimited lifetime"); break; case ROWID_VALID_OTHER: System.out.println("ROWID has indeterminate lifetime"); break; case ROWID_VALID_SESSION: System.out.println( "ROWID type has lifetime that " + "is valid for at least the " + "containing session"); break; case ROWID_VALID_TRANSACTION: System.out.println( "ROWID type has lifetime that " + "is valid for at least the " + "containing transaction"); break;
} }
109
Note that stored procedures are supported by most DBMSs, but there is a fair amount of variation in their syntax and capabilities. Consequently, the tutorial contains two classes, StoredProcedureJavaDBSample and StoredProcedureMySQLSample to demonstrate how to create stored procedures in Java DB and MySQL, respectively. This page covers the following topics: Overview of Stored Procedures Examples Parameter Modes Creating Stored Procedures in Java DB Creating Stored Procedures in Java DB with SQL Scripts or JDBC API Creating Stored Procedures in Java DB Package Java Class in JAR File Creating Stored Procedure in MySQL Creating Stored Procedure in MySQL with SQL Scripts or JDBC API Calling Stored Procedures in Java DB and MySQL
GET_SUPPLIER_OF_COFFEE: Prints the name of the supplier supplierName for the coffee coffeeName. It requires the following parameters: IN coffeeName varchar(32): The name of the coffee OUT supplierName varchar(40): The name of the coffee supplier When the example calls this stored procedure with Colombian as the value for coffeeName, the example produces output similar to the following:
Supplier of the coffee Colombian: Acme, Inc.
RAISE_PRICE: Raises the price of the coffee coffeeName to the price newPrice. If the price increase is greater than the percentage maximumPercentage, then the price is raised by that percentage. This procedure will not change the price if the price newPrice is lower than the original price of the coffee. It requires the following parameters: IN coffeeName varchar(32): The name of the coffee IN maximumPercentage float: The maximum percentage to raise the coffee's price INOUT newPrice numeric(10,2): The new price of the coffee. After the RAISE_PRICE stored procedure has been called, this parameter will contain the current price of the coffee coffeeName. When the example calls this stored procedure with Colombian as the value for coffeeName, 0.10 as the value for maximumPercentage, and 19.99 as the value for newPrice, the example produces output similar to the following:
Contents of COFFEES table before calling RAISE_PRICE:
110
Colombian, 101, 7.99, 0, 0 Colombian_Decaf, 101, 8.99, 0, 0 Espresso, 150, 9.99, 0, 0 French_Roast, 49, 8.99, 0, 0 French_Roast_Decaf, 49, 9.99, 0, 0 Calling the procedure RAISE_PRICE Value of newPrice after calling RAISE_PRICE: 8.79 Contents of COFFEES table after calling RAISE_PRICE: Colombian, 101, 8.79, 0, 0 Colombian_Decaf, 101, 8.99, 0, 0 Espresso, 150, 9.99, 0, 0 French_Roast, 49, 8.99, 0, 0 French_Roast_Decaf, 49, 9.99, 0, 0
Parameter Modes
The parameter attributes IN (the default), OUT, and INOUT are parameter modes. They define the action of formal parameters. The following table summarizes the information about parameter modes. Characteristic of IN OUT INOUT Parameter Mode No; if omitted, then Must it be specified in the the parameter mode of stored procedure Must be specified. Must be specified. the formal parameter definition? is IN. Both; passes initial Does the parameter pass a values to a stored value to the stored Passes values to a Returns values to the procedure; returns procedure or return a stored procedure. caller. updated values to the value? caller. Does the formal parameter Formal parameter acts Formal parameter acts act as a constant or a Formal parameter acts like an uninitialized like an initialized variable in the stored like a constant. variable. variable. procedure? Formal parameter Can the formal parameter Formal parameter cannot be used in an Formal parameter must be assigned a value in the cannot be assigned a expression; must be be assigned a value. stored procedure? value. assigned a value. What kinds of actual Actual parameter can parameters (arguments) be a constant, Actual parameter must Actual parameter must can be passed to the stored initialized variable, be a variable. be a variable. procedure? literal, or expression.
111
3. Package the Java class (that contains the public static Java method you created earlier) in a JAR file. 4. Call the stored procedure with the CALL SQL statement. See the section Calling Stored Procedures in Java DB and MySQL.
Connection con = DriverManager.getConnection("jdbc:default:connection"); Statement stmt = null; String query = "select SUPPLIERS.SUP_NAME, " + "COFFEES.COF_NAME " + "from SUPPLIERS, COFFEES " + "where SUPPLIERS.SUP_ID = " + "COFFEES.SUP_ID " + "order by SUP_NAME"; stmt = con.createStatement(); rs[0] = stmt.executeQuery(query);
The SHOW_SUPPLIERS stored procedure takes no arguments. You can specify arguments in a stored procedure by defining them in the method signature of your public static Java method. Note that the method showSuppliers contains a parameter of type ResultSet[]. If your stored procedure returns any number of ResultSet objects, specify one parameter of type ResultSet[] in your Java method. In addition, ensure that this Java method is public and static. Retrieve the Connection object from the URL jdbc:default:connection. This is a convention in Java DB to indicate that the stored procedure will use the currently existing Connection object. Note that the Statement object is not closed in this method. Do not close any Statement objects in the Java method of your stored procedure; if you do so, the ResultSet object will not exist when you issue the CALL statement when you call your stored procedure. In order for the stored procedure to return a generated result set, you must assign the result set to an array component of the ResultSet[] parameter. In this example, the generated result set is assigned to the array component rs[0]. The following method is StoredProcedureJavaDBSample.showSuppliers:
public static void getSupplierOfCoffee(String coffeeName, String[] supplierName) throws SQLException { Connection con = DriverManager.getConnection("jdbc:default:connection"); PreparedStatement pstmt = null; ResultSet rs = null; String query = "select SUPPLIERS.SUP_NAME " + "from SUPPLIERS, COFFEES " + "where " + "SUPPLIERS.SUP_ID = COFFEES.SUP_ID " + "and ? = COFFEES.COF_NAME"; pstmt = con.prepareStatement(query);
112
pstmt.setString(1, coffeeName); rs = pstmt.executeQuery(); if (rs.next()) { supplierName[0] = rs.getString(1); } else { supplierName[0] = null; }
The formal parameter coffeeName has the parameter mode IN. This formal parameter is used like any other parameter in a Java method. Because the formal parameter supplierName has the parameter mode OUT, it must use a one dimensional array data type. Because this method does not produce a result set, the method definition does not contain a parameter of type ResultSet[]. In order to retrieve a value from an OUT formal parameter, you must assign the value to be retrieved to an array component of the OUT formal parameter. In this example, the retrieved name of the coffee supplier is assigned to the array component supplierName[0]. The following is the method signature of the StoredProcedureJavaDBSample.raisePrice method:
public static void raisePrice( String coffeeName, double maximumPercentage, BigDecimal[] newPrice) throws SQLException
Because the formal parameter newPrice has the parameter mode INOUT, it must use a one dimensional array data type. Java DB maps the FLOAT and NUMERIC SQL data types to the double and java.math.BigDecimal Java data types, respectively.
Statement stmtCreateShowSuppliers = null; // ... String queryShowSuppliers = "CREATE PROCEDURE SHOW_SUPPLIERS() " + "PARAMETER STYLE JAVA " + "LANGUAGE JAVA " + "DYNAMIC RESULT SETS 1 " + "EXTERNAL NAME " + "'com.oracle.tutorial.jdbc." + "StoredProcedureJavaDBSample." + "showSuppliers'"; // ... try { System.out.println("Calling CREATE PROCEDURE"); stmtCreateShowSuppliers = con.createStatement(); // ... } catch (SQLException e) {
113
JDBCTutorialUtilities.printSQLException(e); } finally { if (stmtCreateShowSuppliers != null) { stmtCreateShowSuppliers.close(); } // ... } }
The following list describes the procedure elements you can specify in the CREATE PROCEDURE statement: PARAMETER STYLE JAVA: Specifies that the stored procedure uses a parameter-passing convention that conforms to the Java language and the SQL routines specification (currently, JAVA is the only option). LANGUAGE JAVA: Specifies the programming language of the stored procedure (currently, JAVA is the only option). DYNAMIC RESULT SETS 1: Specifies the maximum number of result sets retrieved; in this case, it is 1. EXTERNAL NAME 'com.oracle.tutorial.jdbc.StoredProcedureJavaDBSample.showSupp liers' specifies the fully qualified Java method that this stored procedure calls. Note: Java DB must be able to find the method specified here in your class path or in a JAR file directly added to the database. See the following step, Package Java Class in JAR File. The following statement (which is found in StoredProcedureJavaDBSample.createProcedures) creates a stored procedure named GET_SUPPLIERS_OF_COFFEE (line breaks have been added for clarity):
CREATE PROCEDURE GET_SUPPLIER_OF_COFFEE( IN coffeeName varchar(32), OUT supplierName varchar(40)) PARAMETER STYLE JAVA LANGUAGE JAVA DYNAMIC RESULT SETS 0 EXTERNAL NAME 'com.oracle.tutorial.jdbc. StoredProcedureJavaDBSample. getSupplierOfCoffee'
This stored procedure has two formal parameters, coffeeName and supplierName. The parameter specifiers IN and OUT are called parameter modes. They define the action of formal parameters. See Parameter Modes for more information. This stored procedure does not retrieve a result set, so the procedure element DYNAMIC RESULT SETS is 0. The following statement creates a stored procedure named RAISE_PRICE (line breaks have been added for clarity):
CREATE PROCEDURE RAISE_PRICE( IN coffeeName varchar(32), IN maximumPercentage float, INOUT newPrice float) PARAMETER STYLE JAVA LANGUAGE JAVA DYNAMIC RESULT SETS 0 EXTERNAL NAME 'com.oracle.tutorial.jdbc. StoredProcedureJavaDBSample.raisePrice'
You can use SQL scripts to create stored procedures in Java DB. See the script javadb/createprocedures.sql and the Ant target javadb-create-procedure in the build.xml Ant
114
build script.
Note: The method StoredProcedureJavaDBSample.registerJarFile demonstrates how to call these system procedures. If you call this method, ensure that you have modified javadb-sample-properties.xml so that the value of the property jar_file is set to the full path name of JDBCTutorial.jar. The install_jar procedure in the SQL schema adds a JAR file to the database. The first argument of this procedure is the full path name of the JAR file on the computer from which this procedure is run. The second argument is an identifier that Java DB uses to refer to the JAR file. (The identifier APP is the Java DB default schema.) The replace_jar procedure replaces a JAR file already in the database. The system procedure SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY sets or deletes the value of a property of the database on the current connection. This method sets the property derby.database.classpath to the identifier specified in the install_jar file. Java DB first looks in your Java class path for a class, then it looks in derby.database.classpath.
115
drop procedure if exists SHOW_SUPPLIERS| # ... SELECT 'Creating procedure SHOW_SUPPLIERS' AS ' '| create procedure SHOW_SUPPLIERS() begin select SUPPLIERS.SUP_NAME, COFFEES.COF_NAME from SUPPLIERS, COFFEES where SUPPLIERS.SUP_ID = COFFEES.SUP_ID order by SUP_NAME; end|
The DROP PROCEDURE statement deletes that procedure SHOW_SUPPLIERS if it exists. In MySQL, statements in a stored procedure are separated by semicolons. However, a different delimiter is required to end the create procedure statement. This example uses the pipe ( |) character; you can use another character (or more than one character). This character that separates statements is defined in the delimiter attribute in the Ant target that calls this script. This excerpt is from the Ant build file build.xml (line breaks have been inserted for clarity):
<target name="mysql-create-procedure"> <sql driver="${DB.DRIVER}" url="${DB.URL}" userid="${DB.USER}" password="${DB.PASSWORD}" classpathref="CLASSPATH" print="true" delimiter="|" autocommit="false" onerror="abort"> <transaction src="./sql/${DB.VENDOR}/ create-procedures.sql"> </transaction> </sql> </target>
Alternatively, you can use the DELIMITER SQL statement to specify a different delimiter character. The CREATE PROCEDURE statement consists of the name of the procedure, a comma-separated list of parameters in parentheses, and SQL statements within the BEGIN and END keywords. You can use the JDBC API to create a stored procedure. The following method, StoredProcedureMySQLSample.createProcedureShowSuppliers, performs the same tasks as the previous script:
public void createProcedureShowSuppliers() throws SQLException { String createProcedure = null;
String queryDrop = "DROP PROCEDURE IF EXISTS SHOW_SUPPLIERS"; createProcedure = "create procedure SHOW_SUPPLIERS() " + "begin " + "select SUPPLIERS.SUP_NAME, " + "COFFEES.COF_NAME " +
116
"from SUPPLIERS, COFFEES " + "where SUPPLIERS.SUP_ID = " + "COFFEES.SUP_ID " + "order by SUP_NAME; " + "end"; Statement stmt = null; Statement stmtDrop = null; try { System.out.println("Calling DROP PROCEDURE"); stmtDrop = con.createStatement(); stmtDrop.execute(queryDrop); } catch (SQLException e) { JDBCTutorialUtilities.printSQLException(e); } finally { if (stmtDrop != null) { stmtDrop.close(); } } try { stmt = con.createStatement(); stmt.executeUpdate(createProcedure); } catch (SQLException e) { JDBCTutorialUtilities.printSQLException(e); } finally { if (stmt != null) { stmt.close(); } } }
Note that the delimiter has not been changed in this method. The stored procedure SHOW_SUPPLIERS generates a result set, even though the return type of the method createProcedureShowSuppliers is void and the method does not contain any parameters. A result set is returned when the stored procedure SHOW_SUPPLIERS is called with the method CallableStatement.executeQuery:
CallableStatement cs = null; cs = this.con.prepareCall("{call SHOW_SUPPLIERS}"); ResultSet rs = cs.executeQuery();
The following excerpt from the method StoredProcedureMySQLSample.createProcedureGetSupplierOfCoffee contains the SQL query that creates a stored procedure named GET_SUPPLIER_OF_COFFEE:
public void createProcedureGetSupplierOfCoffee() throws SQLException { String createProcedure = null; // ... createProcedure = "create procedure GET_SUPPLIER_OF_COFFEE(" + "IN coffeeName varchar(32), " + "OUT supplierName varchar(40)) " + "begin " + "select SUPPLIERS.SUP_NAME into " + "supplierName " + "from SUPPLIERS, COFFEES " + "where SUPPLIERS.SUP_ID = " + "COFFEES.SUP_ID " +
117
"and coffeeName = COFFEES.COF_NAME; " + "select supplierName; " + "end"; // ...
This stored procedure has two formal parameters, coffeeName and supplierName. The parameter specifiers IN and OUT are called parameter modes. They define the action of formal parameters. See Parameter Modes for more information. The formal parameters are defined in the SQL query, not in the method createProcedureGetSupplierOfCoffee. To assign a value to the OUT parameter supplierName, this stored procedure uses a SELECT statement. The following excerpt from the method StoredProcedureMySQLSample.createProcedureRaisePrice contains the SQL query that creates a stored procedure named RAISE_PRICE:
public void createProcedureRaisePrice() throws SQLException { String createProcedure = null; // ... createProcedure = "create procedure RAISE_PRICE(" + "IN coffeeName varchar(32), " + "IN maximumPercentage float, " + "INOUT newPrice numeric(10,2)) " + "begin " + "main: BEGIN " + "declare maximumNewPrice " + "numeric(10,2); " + "declare oldPrice numeric(10,2); " + "select COFFEES.PRICE into oldPrice " + "from COFFEES " + "where COFFEES.COF_NAME " + "= coffeeName; " + "set maximumNewPrice = " + "oldPrice * (1 + " + "maximumPercentage); " + "if (newPrice > maximumNewPrice) " + "then set newPrice = " + "maximumNewPrice; " + "end if; " + "if (newPrice <= oldPrice) " + "then set newPrice = oldPrice; " + "leave main; " + "end if; " + "update COFFEES " + "set COFFEES.PRICE = newPrice " + "where COFFEES.COF_NAME " + "= coffeeName; " + "select newPrice; " + "END main; " + "end"; } // ...
The stored procedure assigns a value to the INOUT parameter newPrice with the SET and SELECT statements. To exit the stored procedure, the stored procedure first encloses the statements
118
in a BEGIN ... END block labeled main. To exit the procedure, the method uses the statement leave main.
Note: As with Statement objects, to call the stored procedure, you can call execute, executeQuery, or executeUpdate depending on how many ResultSet objects the procedure returns. However, if you are not sure how many ResultSet objects the procedure returns, call execute. Calling the stored procedure SHOW_SUPPLIERS is demonstrated in the section Creating Stored Procedure with JDBC API in MySQL. The following excerpt from method runStoredProcedures, calls the stored procedure GET_SUPPLIER_OF_COFFEE:
cs = this.con.prepareCall("{call GET_SUPPLIER_OF_COFFEE(?, ?)}"); cs.setString(1, coffeeNameArg); cs.registerOutParameter(2, Types.VARCHAR); cs.executeQuery(); String supplierName = cs.getString(2);
The interface CallableStatement extends PreparedStatement. It is used to call stored procedures. Specify values for IN parameters (such as coffeeName in this example) just like you would with a PreparedStatement object by calling the appropriate setter method. However, if a stored procedure contains an OUT parameter, you must register it with the registerOutParameter method. The following excerpt from the method runStoredProcedures, calls the stored procedure RAISE_PRICE:
cs = this.con.prepareCall("{call RAISE_PRICE(?,?,?)}"); cs.setString(1, coffeeNameArg); cs.setFloat(2, maximumPercentageArg); cs.registerOutParameter(3, Types.NUMERIC); cs.setFloat(3, newPriceArg); cs.execute();
Because the parameter newPrice (the third parameter in the procedure RAISE_PRICE) has the parameter mode INOUT, you must both specify its value by calling the appropriate setter method and register it with the registerOutParameter method.
119
The sample contains five text fields that correspond to each of the columns in the COFFEES table. It also contains three buttons: Add row to table: Adds a row to the sample's table based on the data entered in the text fields. Update database: Updates the table COFFEES based on the data in the sample's table. Discard changes: Retrieves the contents of the COFFEES table, replacing the existing data in the sample's table. This sample (which requires CoffeesTableModel) demonstrates the following general steps to integrate JDBC with the Swing API: 1. Implementing the TableModel interface 2. Implementing the RowSetListener interface 3. Laying out the Swing components 4. Adding listeners for the buttons in the sample
Implementing javax.swing.event.TableModel
The TableModel interface enables a Java Swing application to manage data in a JTable object. The sample, CoffeesTableModel.java, implements this interface. It specifies how a JTable object should retrieve data from a RowSet object and display it in a table. Note: Although this sample displays the contents of the COFFEES table in a Swing application, the class CoffeesTableModel should work for any SQL table provided that its data can be represented with String objects. (However, the fields that enable users to add rows to COFFEES, which are specified in the class CoffeesFrame, would have to be modified for other SQL tables.) Before implementing the methods of the interface TableModel, the constructor of the class CoffeeTableModel initializes various member variables required for these implemented methods as follows:
public CoffeesTableModel(CachedRowSet rowSetArg) throws SQLException { this.coffeesRowSet = rowSetArg; this.metadata = this.coffeesRowSet.getMetaData(); numcols = metadata.getColumnCount(); // Retrieve the number of rows.
120
this.coffeesRowSet.beforeFirst(); this.numrows = 0; while (this.coffeesRowSet.next()) { this.numrows++; } this.coffeesRowSet.beforeFirst();
The following describes the member variables initialized in this constructor: CachedRowSet coffeesRowSet: Stores the contents of the table COFFEES. This sample uses a RowSet object, in particular, a CachedRowSet object, rather than a ResultSet object for two reasons. A CachedRowSet object enables the user of the application to make changes to the data contained in it without being connected to the database. In addition, because a CachedRowSet object is a JavaBeans component, it can notify other components when certain things happen to it. In this sample, when a new row is added to the CachedRowSet object, it notifies the Swing component that is rendering its data in a table to refresh itself and display the new row. ResultSetMetaData metadata: Retrieves the number of columns in the table COFFEES as well as the names of each of them. int numcols, numrows: Stores the number of columns and rows, respectively, in the table COFFEES. The CoffeesTableModel.java sample implements the following methods from TableModel interface: Class<?> getColumnClass(int columnIndex): Returns the most specific superclass for all the cell values in the column. int getColumnCount(): Returns the number of columns in the model. String getColumnName(int columnIndex): Returns the name of the column specified by the parameter columnIndex. int getRowCount(): Returns the number of rows in the model. Object getValueAt(int rowIndex, int columnIndex): Returns the value for the cell at intersection of the column columnIndex and the row rowIndex. boolean isCellEditable(int rowIndex, int columnIndex): Returns true if the cell at the intersection of the column rowIndex and the row columnIndex can be edited. The following methods have not been implemented because this sample does not allow users to directly edit the contents of the table: void addTableModelListener(TableModelListener l): Adds a listener to the list that is notified each time a change to the data model occurs. void removeTableModelListener(TableModelListener l): Removes a listener from the list that is notified each time a change to the data model occurs. void setValueAt(Object aValue, int rowIndex, int columnIndex): Sets the value in the cell at the intersection of the column columnIndex and the row rowIndex to the object aValue.
121
public int getRowCount() { return numrows; }
Implementing getColumnClass
The getColumnClass method returns the data type of the specified column. To keep things simple, this method returns the String class, thereby converting all data in the table into String objects. The JTable class uses this method to determine how to render data in the GUI application.
public Class getColumnClass(int column) { return String.class; }
Implementing getColumnName
The getColumnName method returns the name of the specified column. The JTable class uses this method to label each of its columns.
public String getColumnName(int column) { try { return this.metadata.getColumnLabel(column + 1); } catch (SQLException e) { return e.toString(); } }
Implementing getColumnAt
The getColumnAt method retrieves the value at the specified row and column in the row set coffeesRowSet. The JTable class uses this method to populate its table. Note that SQL starts numbering its rows and columns at 1, but the TableModel interface starts at 0; this is the reason why the rowIndex and columnIndex values are incremented by 1.
public Object getValueAt(int rowIndex, int columnIndex) { try { this.coffeesRowSet.absolute(rowIndex + 1); Object o = this.coffeesRowSet.getObject(columnIndex + 1); if (o == null) return null; else return o.toString(); } catch (SQLException e) { return e.toString(); } }
Implementing isCellEditable
Because this sample does not allow users to directly edit the contents of the table (rows are added by another window control), this method returns false regardless of the values of rowIndex and columnIndex:
public boolean isCellEditable(int rowIndex, int columnIndex) { return false; }
Implementing javax.sql.RowSetListener
The class CoffeesFrame implements only one method from the interface RowSetListener, rowChanged. This method is called when a user adds a row to the table.
public void rowChanged(RowSetEvent event) {
122
CachedRowSet currentRowSet = this.myCoffeesTableModel.coffeesRowSet; try { currentRowSet.moveToCurrentRow(); myCoffeesTableModel = new CoffeesTableModel( myCoffeesTableModel.getCoffeesRowSet()); table.setModel(myCoffeesTableModel); } catch (SQLException ex) { JDBCTutorialUtilities.printSQLException(ex); // Display the error in a dialog box. JOptionPane.showMessageDialog( CoffeesFrame.this, new String[] { // Display a 2-line message ex.getClass().getName() + ": ", ex.getMessage() } ); } }
As mentioned previously, instead of a ResultSet object to represent the contents of the COFFEES table, this sample uses a RowSet object, notably a CachedRowSet object. The method CoffeesFrame.getContentsOfCoffeesTable retrieves the contents of the table COFFEES. The method CoffeesTableModel.addEventHandlersToRowSet adds the event handler defined in the CoffeesFrame class, which is the method rowChanged, to the row set member variable CoffeesTableModel.coffeesRowSet. This enables the class CoffeesFrame to notify the row set coffeesRowSet of any events, in particular, when a user clicks the button Add row to table, Update database, or Discard changes. When the row set coffeesRowSet is notified of one of these changes, the method CoffeesFrame.rowChanged is called. The statement table.setModel(myCoffeesTableModel) specifies that it use the CoffeesTableModel object myCoffeesTableModel to populate the JTable Swing component table. The following statements specify that the CoffeesFrame class use the layout GridBagLayout to lay out its Swing components:
Container contentPane = getContentPane(); contentPane.setComponentOrientation(
123
ComponentOrientation.LEFT_TO_RIGHT); contentPane.setLayout(new GridBagLayout()); GridBagConstraints c = new GridBagConstraints();
See How to Use GridBagLayout in the Creating a GUI With JFC/Swing for more information about using the layout GridBagLayout. See the source code for CoffeesFrame.java to see how the Swing components of this sample are added to the layout GridBagLayout.
} });
When a user clicks this button, it performs the following: Creates a message dialog box that displays the row to be added to the table. Calls the method CoffeesTableModel.insertRow, which adds the row to the member variable CoffeesTableModel.coffeesRowSet. If an SQLException is thrown, then the method CoffeesFrame.displaySQLExceptionDialog creates a message dialog box that displays the content of the SQLException. The following statement adds a listener to the button Update database:
button_UPDATE_DATABASE.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) {
124
try { myCoffeesTableModel.coffeesRowSet.acceptChanges(); msgline.setText("Updated database"); } catch (SQLException sqle) { displaySQLExceptionDialog(sqle); // Now revert back changes try { createNewTableModel(); msgline.setText("Discarded changes"); } catch (SQLException sqle2) { displaySQLExceptionDialog(sqle2); } } } ); }
When a user clicks this button, the table COFFEES is updated with the contents of the row set myCoffeesTableModel.coffeesRowSet. The following statement adds a listener to the button Discard changes:
button_DISCARD_CHANGES.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { try { createNewTableModel(); } catch (SQLException sqle) { displaySQLExceptionDialog(sqle); } } });
When a user clicks this button, the method CoffeesFrame.createNewTableModel is called, which repopulates the JTable component with the contents of the COFFEES table.