Programming WebLogic JDBC
This section includes the following information about using JDBC rowSets with WebLogic Server:
RowSets are a JDBC 2.0 extension to the java.sql.ResultSet
interface. The WebLogic Server implementation of RowSets provides a disconnected RowSet. In this model, a RowSet object is populated from the database and then the database cursor and connection are immediately released. The RowSet is disconnected from the database and provides a ResultSet interface for the cached data. The user may read, modify, delete, or even insert new rows into the RowSet in memory. When acceptChanges
is called, the RowSet takes all of the in-memory updates and writes them back to the database.
In most cases, populating a RowSet data and updating the database occur in separate transactions. The RowSet implementation uses optimistic concurrency control to ensure data consistency.
WebLogic Server RowSets implementation implements and extends java.io.Serializable
, so the RowSet can be sent as an RMI parameter or return value. For example, an EJB method could populate a RowSet from a database query and then return the RowSet to a client.
RowSets can also read and write their state and metadata to an XML format. The RowSet metadata is written as an XML schema document and the RowSet data is written as an XML document that conforms to the schema. You can also populate the metadata and cached data for a RowSet from XML documents.
Note: When using a RowSet in a client-side application, the exact same JDBC driver classes must be in the CLASSPATH
on both the server and the client. If the driver classes do not match, you may see java.rmi.UnmarshalException
exceptions.
RowSets are created from a factory interface.
import weblogic.jdbc.rowset.RowSetFactory;
import weblogic.jdbc.rowset.WLCachedRowSet;
RowSetFactory factory = RowSetFactory.newInstance();
WLCachedRowSet rowSet = factory.newCachedRowSet();
The following sections describe how to populate a RowSet, manipulate the data in the RowSet, and then flush the changes to the database.
Note: Delimiter identifiers may not be used for column or table names in RowSets. Delimiter identifiers are identifiers that need to be enclosed in double quotation marks when appearing in a SQL statement. They include identifiers that are SQL reserved words (e.g., USER
, DATE
, etc.) and names that are not identifiers. A valid identifier must start with a letter and contain only letters, numbers, and underscores.
After the RowSet object is created, its cache can be filled with data. Once a RowSet has been populated, it is disconnected from the database and acts as a memory cache. There are three methods sources for populating the RowSet's cache with data:
A RowSet can be populated from an existing JDBC ResultSet. This is a common case when data is read from a stored procedure or JDBC code already exists to load the data. The RowSet can be loaded by calling its populate method.
rowSet.populate(myResultSet);
A RowSet can be populated by providing database connection information and a SQL query. First, you provide the CachedRowSet with information needed to get a JDBC connection. This can be done by providing a javax.sql.DataSource
object, a DataSource JNDI name, or a JDBC Driver URL. The DataSource API is recommended since it is the standard JDBC 2.0 method for retrieving JDBC connections. Also, only connections retrieved via the DataSource API can participate in XA/2PC transactions.
rowSet.setDataSourceName("myDataSource");
If necessary, the setUsername
and setPassword
methods can be used to set the credentials necessary to access your DataSource in WebLogic Server.
rowSet.setUsername("weblogic");
rowSet.setPassword("weblogic");
Next, specify a SQL query to use to load the database. For instance, the following query populates the RowSet with all employees with a salary greater than 50000:
rowSet.setCommand("select e_name, e_id from employees WHERE e_salary > ?");
rowSet.setInt(1, 50000);
Finally, run the execute method that runs the specified query and loads the RowSet with data. The execute method closes the JDBC connection. The RowSet does not maintain open cursors or connections to the database.
rowSet.execute();
Because the RowSet is an extension to the ResultSet interfaces, it inherits all of the ResultSet methods for retrieving data. As with a ResultSet, you can iterate through a RowSet using the next()
method. The getXXX
methods can be used to read data from the RowSet.
while(rowSet.next()) {
String name = rowSet.getString("e_name");
int id = rowSet.getInt("e_id");
System.out.println("Read name: "+name+ " id: "+id);
}
while(rowSet.next()) {
String name = rowSet.getString("e_name");
int id = rowSet.getInt("e_id");
System.out.println("Read name: "+name+ " id: "+id);
}
RowSets use the ResultSet updateXXX
methods for updating data.
It is important to understand that RowSet updates are kept in memory only. Updates are written back to the database only when you call the acceptChanges
method.
// move back to the beginning of the rowSet
rowSet.beforeFirst();
while(rowSet.next()) {
String name = rowSet.getString("e_name");
// convert to upper case
name = name.toUpper();
rowSet.updateString("e_name", name);
rowSet.updateRow();
}
// Call acceptChanges to write all of the in-memory updates to the database
rowSet.acceptChanges();
Note: You must call RowSet.updateRow
or RowSet.cancelRowUpdates
before moving the RowSet's cursor with the next
method.
Deleting rows is very similar to updating rows. The deleteRow()
method marks a row for deletion. When you call the acceptChanges
method, the RowSet issues the appropriate SQL to delete the selected rows.
// move back to the beginning of the rowSet
rowSet.beforeFirst();
while(rowSet.next()) {
String name = rowSet.getString("e_name");
if ("Rob".equals(name)) {
rowSet.deleteRow();
}
}
// When acceptChanges all of the in-memory deletions are written to
// the database
rowSet.acceptChanges();
Like ResultSets, RowSets have the concept of a special insert row. To insert data, call moveToInsertRow
and then update the values in the row. The insertRow
method is called to indicate that the updates are done. You can either insert another row, or call moveToCurrentRow
to return to the read data. When you call acceptChanges
, all inserted rows are sent to the database.
rowSet.moveToInsertRow();
rowSet.updateString("e_name", "Seth");
rowSet.updateInt("e_id", 2);
rowSet.insertRow();
rowSet.updateString("e_name", "Matt");
rowSet.updateInt("e_id", 3);
rowSet.insertRow();
rowSet.moveToCurrentRow();
// issues SQL INSERTs to database
rowSet.acceptChanges();
A RowSet acts like a database cache, and all updates to it occur in memory. To flush these changes back to the database, call the acceptChanges
method.
The RowSet's acceptChanges
method uses the DataSource or connection information to acquire a database connection. It then issues all of the INSERT, UPDATE, or DELETE statements that have been made in memory to the database.
Since the RowSet was disconnected from the database and not holding any locks or database resources, it is possible that the underlying data in the database has been changed since the RowSet was populated. The RowSet implementation uses optimistic concurrency control on its UPDATE and DELETE statements to check for stale data. See Optimistic Concurrency Policies for details.
The RowSet API provides a getMetaData
method for access to the associated javax.sql.RowSetMetaData
object. The WLCachedRowSet implementation provides a WLRowSetMetaData interface that extends the standard RowSetMetaData with additional functionality.
The metadata can be accessed with:
WLRowSetMetaData metaData = (WLRowSetMetaData) rowSet.getMetaData();
In most cases, populating a RowSet with data and updating the database occur in separate transactions. The underlying data in the database can change in the time between the two transactions. The WebLogic Server RowSet implementation uses optimistic concurrency control to ensure data consistency.
With optimistic concurrency, RowSets work on the assumption that multiple users are unlikely to change the same data at the same time. Therefore, as part of the disconnected RowSet model, the RowSet does not lock database resources. However, before writing changes to the database, the RowSet must check to make sure that the data to be changed in the database has not already changed since the data was read into the RowSet.
The UPDATE and DELETE statements issued by the RowSet include WHERE clauses that are used to verify the data in the database against what was read when the RowSet was populated. If the RowSet detects that the underlying data in the database has changed, it issues an OptimisticConflictException
. The application can catch this exception and determine how to proceed. Typically, applications will refresh the updated data and present it to the user again.
The WLCachedRowSet implementation offers several optimistic concurrency policies that determine what SQL the RowSet issues to verify the underlying database data:
To illustrate the differences between these policies, we will use an example that uses the following:
CREATE TABLE employees (
e_id integer primary key,
e_salary integer,
e_name varchar(25)
);
e_id = 1, e_salary = 10000, and e_name = 'John Smith'
In the example for each of the optimistic concurrency policies listed below, the RowSet will read this row from the employees table and set John Smith's salary to 20000. The example will then show how the optimistic concurrency policy affects the SQL code issued by the RowSet.
The default RowSet optimistic concurrency control policy is VERIFY_READ_COLUMNS. When the RowSet issues an UPDATE or DELETE, it includes all columns that were read from the database in the WHERE clause. This verifies that the value in all columns that were initially read into the RowSet have not changed.
In our example update, the RowSet issues:
UPDATE employees SET e_salary = 20000
WHERE e_id = 1 AND e_salary=10000 AND e_name = 'John Smith';
The VERIFY_MODIFIED_COLUMNS policy only includes the primary key columns and the updated columns in the WHERE clause. It is useful if your application only cares if its updated columns are consistent. It does allow your update to commit if columns that have not been updated have changed since the data has been read.
In our example update, the RowSet issues:
UPDATE employees SET e_salary = 20000
WHERE e_id = 1 AND e_salary=10000
The e_id
column is included since it is a primary key column. The e_salary
column is a modified column so it is included as well. The e_name
column was only read so it is not verified.
The VERIFY_SELECTED_COLUMNS includes the primary key columns and columns you specify in the WHERE clause.
WLRowSetMetaData metaData = (WLRowSetMetaData) rowSet.getMetaData();
metaData.setOptimisticPolicy(WLRowSetMetaData.VERIFY_SELECTED_COLUMNS);
// Only verify the e_salary column
metaData.setVerifySelectedColumn("e_salary", true);
metaData.acceptChanges();
In our example update, the RowSet issues:
UPDATE employees SET e_salary = 20000
WHERE e_id = 1 AND e_salary=10000
The e_id
column is included since it is a primary key column. The e_salary
column is a selected column so it is included as well.
The VERIFY_NONE policy only includes the primary key columns in the WHERE clause. It does not provide any additional verification on the database data.
In our example update, the RowSet issues:
UPDATE employees SET e_salary = 20000 WHERE e_id = 1
The VERIFY_AUTO_VERSION_COLUMNS includes the primary key columns as well as a separate version column that you specify in the WHERE clause. The RowSet will also automatically increment the version column as part of the update. This version column must be an integer type. The database schema must be updated to include a separate version column (e_version
). Assume for our example this column currently has a value of 1.
metaData.setOptimisticPolicy(WLRowSetMetaData.
VERIFY_AUTO_VERSION_COLUMNS);
metaData.setAutoVersionColumn("e_version", true);
metaData.acceptChanges();
In our example update, the RowSet issues:
UPDATE employees SET e_salary = 20000, e_version = 2
WHERE e_id = 1 AND e_version = 1
The e_version
column is automatically incremented in the SET clause. The WHERE clause verified the primary key column and the version column.
The VERIFY_VERSION_COLUMNS has the RowSet check the primary key columns as well as a separate version column. The RowSet does not increment the version column as part of the update. The database schema must be updated to include a separate version column (e_version
). Assume for our example this column currently has a value of 1.
metaData.setOptimisticPolicy(WLRowSetMetaData.VERIFY_VERSION_COLUMNS);
metaData.setVersionColumn("e_version", true);
metaData.acceptChanges();
In our example update, the RowSet issues:
UPDATE employees SET e_salary = 20000
WHERE e_id = 1 AND e_version = 1
The WHERE clause verifies the primary key column and the version column. The RowSet does not increment the version column so this must be handled by the database. Some databases provide automatic version columns that increment when the row is updated. It is also possible to use a database trigger to handle this type of update.
The Optimistic policies only verify UPDATE and DELETE statements against the row they are changing. Read-only rows are not verified against the database.
Most databases do not allow BLOB or CLOB columns in the WHERE clause so the RowSet never verifies BLOB or CLOB columns.
When multiple tables are included in the RowSet, the RowSet only verifies tables that have been updated.
The default VERIFY_READ_COLUMNS provides a strong-level of consistency at the expense of some performance. Since all columns that were initially read must be sent to the database and compared in the database, there is some additional overhead to this policy. VERIFY_READ_COLUMNS is appropriate when strong levels of consistency are needed, and the database tables cannot be modified to include a version column.
The VERIFY_SELECTED_COLUMNS is useful when the developer needs complete control over the verification and wants to use application-specific knowledge to fine-tune the SQL.
The VERIFY_AUTO_VERSION_COLUMNS provides the same level of consistency as VERIFY_READ_COLUMNS but only has to compare a single integer column. This policy also handles incrementing the version column so it requires a minimal amount of database setup.
The VERIFY_VERSION_COLUMNS is recommended for production systems that want the highest level of performance and consistency. Like VERIFY_AUTO_VERSION_COLUMNS, it provides a high level of consistency while only incurring a single column comparison in the database. VERIFY_VERSION_COLUMNS requires that the database handle incrementing the version column. Some databases provide a column type that automatically increments itself on updates, but this behavior can also be implemented with a database trigger.
The VERIFY_MODIFIED_COLUMNS and VERIFY_NONE decrease the consistency guarantees, but they also decrease the likelihood of an optimistic conflict. You should consider these policies when performance and avoiding conflicts outweigh the need for higher level of data consistency.
When data is read into a RowSet, the RowSet implementation uses the ResultSetMetaData interface to automatically learn the table and column names of the read data. In many cases, this is enough information for the RowSet to generate the required SQL for writing changes back to the database. However, many JDBC drivers just return an empty string when asked for the table name of a given column. Without the table name, the RowSet can be used for read-only operations only. The RowSet cannot issue updates unless the table name is specified programmatically.
The RowSet implementation provides an extended MetaData interface that allows you to specify schema information that cannot be automatically determined via the JDBC Driver. The WLRowSetMetaData interface can be used to set the schema information.
When a RowSet is populated via a SQL query, the execute()
method is generally used to run the query and read the data. The WLCachedRowSet implementation provides the executeAndGuessTableName
and executeAndGuessTableNameAndPrimaryKeys
methods that extend the execute method to also determine the associated table metadata.
The executeAndGuessTableName
method parses the associated SQL and sets the table name for all columns as the first word following the SQL keyword FROM
.
The executeAndGuessTableNameAndPrimaryKeys
method parses the SQL command to read the table name. It then uses the java.sql.DatabaseMetaData
to determine the table's primary keys.
You can also choose to set the table and primary key information using the RowSetMetaData interface.
WLRowSetMetaData metaData = (WLRowSetMetaData) rowSet.getMetaData();
// convenience method to set one table name for all columns
metaData.setTableName("employees");
metaData.setTableName("e_id", "employees");
metaData.setTableName("e_name", "employees");
You can also use the WLRowSetMetaData to identify primary key columns.
metaData.setPrimaryKeyColumn("e_id", true);
The WLRowSetMetaData interface includes the setWriteTableName
method to indicate the only table that should be updated or deleted. This is typically used when a RowSet is populated via a join from multiple tables, but the RowSet should only update one table. Any column that is not from the write table is marked as read-only.
For instance, a RowSet might include a join of orders and customers. The write table could be set to orders. If deleteRow were called, it would delete the order row, but not delete the customer row.
Most database or JDBC applications use transactions, and RowSets support transactions, including JTA transactions. The common use case is to populate the RowSet in Transaction 1. Transaction 1 commits, and there are no database or application server locks on the underlying data. The RowSet holds the data in-memory, and it can be modified or shipped over the network to a client. When the application wishes to commit the changes to the database, it starts Transaction 2 and calls the RowSet's acceptChanges
method. It then commits Transaction 2.
The EJB container and the UserTransaction interface start transactions with the JTA transaction manager. The RowSet operations can participate in this transaction. To participate in the JTA transaction, the RowSet must use a transaction-aware DataSource (TxDataSource). The DataSource can be configured in the WebLogic Server console.
If an Optimistic conflict or other exception occurs during acceptChanges
, the RowSet aborts the global JTA transaction. The application will typically re-read the data and process the update again in a new transaction.
In the case of a failure or rollback, the data is rolled back from the database, but is not rolled back from the rowset. Before proceeding you should do one of the following:
rowset.refresh
to update the rowset with data from the database. If a JTA global transaction is not being used, the RowSet uses a local transaction. It first calls setAutoCommit(false)
on the connection, then it issues all of the SQL statements, and finally it calls connection.commit()
. This attempts to commit the local transaction. This method should not be used when trying to integrate with a JTA transaction that was started by the EJB or JMS containers.
If an Optimistic conflict or other exception occurs during acceptChanges
, the RowSet rolls back the local transaction. In this case, none of the SQL issued in acceptChanges
will commit to the database.
This section provides information on the behavior of rowsets in failed local transactions. The behavior depends on the type of connection object:
In this situation, the connection object is not created by the rowset and initiates a local transaction by calling connection.commit
. If the transaction fails or if the connection calls connection.rollback
, the data is rolled back from the database, but is not rolled back in the rowset. Before proceeding, you must do one of the following:
rowset.refresh
to update the rowset with data from the database. In this situation, the rowset creates its own connection object and uses it to update the data in rowset by calling acceptChanges
. In the case of failure or if the rowset calls connection.rollback
, the data is be rolled back from the rowset and also from the database.
Consider the following performance options when using RowSets.
The RowSet implementation includes support for JDBC 2.0 batch operations. Instead of sending each SQL statement individually to the JDBC driver, a batch sends a collection of statements in one bulk operation to the JDBC driver. Batching is disabled by default, but it generally improves performance when large numbers of updates occur in a single transaction. It is worthwhile to benchmark with this option enabled and disabled for your application and database.
The WLCachedRowSet interface contains the methods setBatchInserts(boolean)
, setBatchDeletes(boolean)
, and setBatchUpdates(boolean)
to control batching of INSERT, DELETE, and UPDATE statements.
Note: The setBatchInserts
, setBatchDeletes
, or setBatchUpdates
methods must be called before the acceptChanges
method is called.
Since the WLCachedRowSet relies on optimistic concurrency control, it needs to determine whether an update or delete command has succeeded or an optimistic conflict occurred. The WLCachedRowSet implementation relies on the JDBC driver to report the number of rows updated by a statement to determine whether a conflict occurred or not. In the case where 0 rows were updated, the WLCachedRowSet knows that a conflict did occur.
Oracle JDBC drivers return java.sql.Statement.SUCCESS_NO_INFO
when batch updates are executed, so the RowSet implementation cannot use the return value to determine whether a conflict occurred.
When the RowSet detects that batching is used with an Oracle database, it automatically changes its batching behavior:
Batched inserts perform as usual since they are not verified.
Batched updates run as normal, but the RowSet issues an extra SELECT query to check whether the batched update encountered an optimistic conflict.
Batched deletes use group deletes since this is more efficient than executing a batched delete followed by a SELECT verification query.
When multiple rows are deleted, the RowSet would normally issue a DELETE statement for each deleted row. When group deletes are enabled, the RowSet issues a single DELETE statement with a WHERE clause that includes the deleted rows.
For instance, if we were deleting 3 employees from our table, the RowSet would normally issue:
DELETE FROM employees WHERE e_id = 3 AND e_version = 1;
DELETE FROM employees WHERE e_id = 4 AND e_version = 3;
DELETE FROM employees WHERE e_id = 5 AND e_version = 10;
When group deletes are enabled, the RowSet issues:
DELETE FROM employees
WHERE e_id = 3 AND e_version = 1 OR
e_id = 4 AND e_version = 3 OR
e_id = 5 AND e_version = 10;
The programmer can use the WLRowSetMetaData.setGroupDeleteSize
to determine the number of rows included in a single DELETE statement. The default value is 50.
The WLCachedRowSet implementation provides support for writing its metadata as an XML schema document and its data as an XML document that conforms to the schema. The WLCachedRowSet can also populate itself and its metadata from an existing XML schema and XML document.
For instance, a RowSet can be converted to XML and sent as an XML message to another process. The other process could rebuild the RowSet instance in memory, read and update data, and send the response back as another XML message. Finally the original server could convert the XML message back to a RowSet and update the database.
The WLRowSetMetaData interface contains the method writeXMLSchema
to write the RowSetMetaData as an XML schema document. The WLRowSetMetaData interface has a writeXML method for converting the RowSet's data into an XML instance document.
XMLOutputStreamFactory xoFactory =
XMLOutputStreamFactory.newInstance();
WLRowSetMetaData metaData = (WLRowSetMetaData) rowSet.getMetaData();
XMLOutputStream xos = null;
// Write XSD Schema
try {
xos = xoFactory.newDebugOutputStream(new
FileOutputStream("rowset.xsd");
metaData.writeXMLSchema(xos);
} finally {
if (xos != null) xos.close();
}
// Write XML Instance data
try {
xos = xoFactory.newDebugOutputStream(new
FileOutputStream("rowset.xml");
rowSet.writeXML(xos);
} finally {
if (xos != null) xos.close();
}
The WLRowSetMetaData interface contains the method loadXMLSchema
to load the RowSetMetaData from an XML schema document. The WLRowSetMetaData interface has a loadXML
method for populating from an XML instance document.
XMLInputStreamFactory xiFactory =
XMLInputStreamFactory.newInstance();
XMLInputStream xis = null;
WLCachedRowSet rowSet = factory.newCachedRowSet();
WLRowSetMetaData metaData = (WLRowSetMetaData) rowSet.getMetaData();
// Read XSD
try {
xis = xiFactory.newInputStream(new FileInputStream("rowset.xsd"));
metaData.loadXMLSchema(xis);
} finally {
if (xis != null) xis.close();
}
// Read XML
try {
xis = xiFactory.newInputStream(new FileInputStream("rowset.xml"));
rs.loadXML(xis);
} finally {
if (xis != null) xis.close();
}
RowSets can be populated from SQL queries that return columns from multiple tables. It is important to understand the RowSet semantics when dealing with multiple tables and the SQL issued by RowSets in multi-table scenarios.
The RowSet optimistic concurrency control policies only verify tables that have been updated. If a RowSet is populated with columns from tables t1
and t2
, and column C
from table t1
is updated, there will be no SQL UPDATE or SELECT that verifies the values read from table t2
.
RowSets do not recognize foreign key or other constraints between tables, so when updating multiple tables, it is possible that RowSet updates will fail because of integrity constraints between tables.
Multi-table RowSets work well when a RowSet is a join from N tables with a single write table. For instance, a query might join in several tables but only update the employees table. In this case, the programmer should call setWriteTableName
to ensure that updates and deletes only apply to the write table.
Another common multi-table scenario is multiple tables that share the same primary key space. This is one logical table that has been split over multiple physical tables in the database. In this scenario, the RowSet will be able to update multiple tables.
Since multi-table RowSets can have complicated update semantics, it is recommended that users set the write table name and only update a single table.
Consider a simple order entry system that has customer and order tables.
CREATE TABLE customer (
id integer primary key,
name varchar(200),
email varchar(200)
);
CREATE TABLE order (
id integer primary key,
sku integer,
quantity integer,
customer_id integer,
foreign key customer_id references customer(id)
);
This example shows a 1 to many relationship where each customer may have many orders.
A customer portal application might issue a query that loads a customer's current orders and some information about the customer with SQL like this:
SELECT o.id, o.sku, o.quantity, c.name, c.email
FROM order o, customer c
WHERE c.id = o.customer_id
This data will be read into the RowSet with one row containing the matching order and customer columns from the SQL Join.
In cases like this, it is recommended that the many side (order) be set as the write table. This ensures that the one side (customer) is read-only. This allows the user to update details in their order, but will prevent changes to their customer record. This is especially useful for deletes since calling deleteRow
will delete the order record but will not delete the customer.