Course Outline
JDBCTM is a JavaTM API (Application Programming Interface) that
documents a standard framework for dealing with tabular and, generally,
relational data. While JDBC 2.0 begins a move to make SQL semi-transparent
to the programmer, SQL is still the lingua franca of the standard
database engines and represents a major industry victory in the effort to
separate data from code. Before getting into the course proper, it's worth
taking a few moments to provide some background on the movement from
straight-ahead SQL to JDBC.
SQL is a standardized language used to create, manipulate, examine, and
manage relational databases. This course will not extensively explain SQL,
although a very basic SQL
Primer and SQL
Resources are provided.
However, you should understand the following:
- A database is essentially a smart container for tables.
- A table is a container comprised of rows.
- A row is (conceptually) a container comprised of columns.
- A column is a single data item having a name, type, and
value.
While you should review the definitions and understand the important
differences, initially you can use the following analogs: A database
approximates a file system; a table approximates a file; a row
approximates a record or structure; and a column approximates a field or
variable. If these terms are unfamiliar, you should review some
programming resources, particularly in the area of Input/Output (I/O)
operations, before proceeding with the course.
Because SQL is an application-specific language, a single statement can
be very expressive and can initiate high-level actions, such as sorting
and merging, on an entire set of data. SQL was standardized in 1992 so
that a program could communicate with most database systems without having
to change the SQL commands. However, you must connect to a database before
sending SQL commands, and each database vendor has a different interface
to do so, as well as different extensions of SQL. Enter ODBC.
ODBC (Open Database Connectivity), a C-based interface to SQL-based
database engines, provides a consistent interface for communicating with a
database and for accessing database metadata (information about
the database system vendor, how the data is stored, and so on). Individual
vendors provide specific drivers or "bridges" to their particular database
management system. Consequently, thanks to ODBC and SQL, you can connect
to a database and manipulate it in a standard way. It is no surprise that,
although ODBC began as a PC standard, it has become nearly an industry
standard.
Although SQL is well-suited for manipulating databases, it was not
designed to be a general application language; rather, it was intended to
be used only as a means of communicating with databases. Another more
general and complete programming language is needed to host and feed SQL
statements to a database and process results for data manipulation, visual
display, or report generation. Unfortunately, you cannot easily write a
program that will run on multiple platforms, even though the database
connectivity standardization issue has been largely resolved. For example,
if you wrote a database client in C++, you might have to totally rewrite
the client for another platform; that is to say, your PC version would not
run on a Macintosh. There are two reasons for this. First, C++ as a
language is not portable because C++ is not completely specified (for
example, how many bits does an int hold?). Second, and more
importantly, support libraries such as network access and GUI (Graphical
User Interface) frameworks are different on each platform. Enter the Java
programming language and JDBC.
A Java program, written properly and according to specification, can
run on any Java technology-enabled platform without recompilation. The
Java programming language is completely specified and, by definition, a
Java technology-enabled platform must support a known core of libraries.
One such library is the java.sql package or JDBC, which you
can think of as a portable version of ODBC, and is itself a major
standard. Using the Java programming language in conjunction with JDBC
provides a truly portable solution to writing database applications.
Note: While portable applications and a standard database interface
are major achievements, keep in mind that, for historical, competitive,
and sometimes nonsensical reasons, the various databases are not
completely standardized. This may mean that you have to aim for a lowest
common denominator in terms of capabilities or build-in adjustments for
specific databases, even on the same platform. This problem remains
whether you use standard SQL, ODBC, JDBC, or other solutions.
A JDBC driver is a class that implements the JDBC
Driver interface and understands how to convert program (and
typically SQL) requests for a particular database. Clearly, the driver is
what makes it all work. There are four different driver types, which are
discussed in the JDK (Java Development Kit) documentation at JDBC
Driver Types. This course uses type 4 drivers because of their nearly
zero installation requirements and dynamic nature. Another driver type may
make more sense for your particular project. Most database vendors now
provide drivers to implement the JDBC API for their particular systems.
These are generally provided free of charge. Third party drivers are also
available, ranging in cost from free to very expensive. For links to JDBC
driver resources, see Specific
Information and the other Resources.
The JDBC 1.0 API provided the basic framework for data access,
consisting primarily of the following interfaces and classes:
Driver
DriverManager
Connection
Statement
PreparedStatement
CallableStatement
ResultSet
DatabaseMetaData
ResultSetMetaData
Types
As you will see in this course, you pass a Driver to the
DriverManager and then obtain a Connection . A
Statement , PreparedStatement , or
CallableStatement is then created and used to update the
database or execute a query. A query returns a ResultSet
containing the requested data, which is retrieved by Type .
DatabaseMetaData and ResultSetMetaData classes
are available to provide information about a database or a
ResultSet .
The JDBC 2.0 API is broken into two parts: the core API, which
this course discusses, and the JDBC
2.0 Optional Package. In general, the JDBC 2.0 core API adds a few
more classes, but is primarily concerned with performance, class
enhancements and functionality, and the new SQL3 (also known as SQL-99)
datatypes.
The new functionality in the core API includes scrollable result sets,
batch updates, programmatic inserts, deletes, and updates, performance
hints, character streams for streams of internationalized Unicode
characters, full precision for java.math.BigDecimal values
and support for time zones in Date , Time , and
Timestamp values.
At the time this course was prepared, the JDBC 3.0 draft was under
review and planned to be included in the 1.4 release of the JDK.
The first hands-on experience with JDBC in this course involves a basic
but complete example to illustrate the overall concepts related to
creating and accessing information in a database. The fundamental issues
encountered when writing any database application are:
The initial task for this example requires setting up the structures
and inserting data to track java (that is, coffee) intake at the jGuru
Jive Java Jumphouse, better known to the initiated as the 4J
Cafe. Then a report must be generated for 4J Cafe management that
includes total coffee sales and the maximum coffee consumed by a customer
in one day. Here's the data:
Coffee Consumption at the jGuru Jive
Java Jumphouse "At the 4J Cafe, caffeine is our
most important product"
Entry |
Customer |
DOW |
Cups |
Type |
1 |
John |
Mon |
1 |
JustJoe |
2 |
JS |
Mon |
1 |
Cappuccino |
3 |
Marie |
Mon |
2 |
CaffeMocha |
4 |
Anne |
Tue |
8 |
Cappuccino |
5 |
Holley |
Tue |
2 |
MoJava |
6 |
jDuke |
Tue |
3 |
Cappuccino |
7 |
Marie |
Wed |
4 |
Espresso |
8 |
JS |
Wed |
4 |
Latte |
9 |
Alex |
Thu |
3 |
Cappuccino |
10 |
James |
Thu |
1 |
Cappuccino |
11 |
jDuke |
Thu |
4 |
JustJoe |
12 |
JS |
Fri |
9 |
Espresso |
13 |
John |
Fri |
3 |
Cappuccino |
14 |
Beth |
Fri |
2 |
Cappuccino |
15 |
jDuke |
Fri |
1 |
Latte |
As noted aboolean autoCommitve, database creation is DBMS-specific. To
aid in understanding the example, an exception is made here to the basic
course rule of adherence to JDBC standards. A database is created in
Cloudscape by setting a database connection URL attribute that is passed
to the driver. As you will see shortly, this attribute is:
create=true . The named database, which is jGuru
here, is created in the DBMS default directory. For the J2EE download as
explained in Cloudscape
Installation and Setup, this will be
J2EE_HOME/Cloudscape . If the database already exists,
Cloudscape creates a Connection , but then issues an
SQLWarning .
Note: Keep in mind that this is Cloudscape's method and does not
necessarily apply to any other DBMS. For example, to create the database
on UDB2/NT, CREATE DATABASE jGuru was used; On DB2/400, first
the command STRSQL was issued, then CREATE COLLECTION
jGuru was used.
There are always two steps to making a database connection using the
DriverManager :
- Load the JDBC driver.
You must load a driver that enables the JDBC classes to communicate
with a data source. In the initial examples, the driver class used with
Cloudscape, RmiJdbcDriver , is hard-coded. Here's the
standard method for dynamically loading a driver: Class.forName( DriverClassName);
A standard JDBC CompliantTM
driver should also create a new instance of the driver class
with this code. Unfortunately, in practice this does not work for all
cases. For that reason, the exercises use the following code: Class.forName(DriverClassName).newInstance();
While this code will create an additional object in many cases, the
code required to determine whether an instance was created, and
to create a new instance if not, generally outweighs that extra cost.
Fortunately, the garbage collector eventually cleans up the unreferenced
object and the DriverManager does not register the driver
twice.
Drivers can also be specified from the command line via the
jdbc.drivers system property, but this method requires the
driver(s) to be in the classpath at compile time: java -Djdbc.drivers=DriverClassName AJavaApp
The specific DriverClassName used in this course for connecting to
Cloudscape in the recommended set up is: COM.cloudscape.core.RmiJdbcDriver
- Connect to a data source.
The driver supplies methods to make a Connection , but
requires a specific type of URL, which uses the jdbc protocol.
The generalized form is
jdbc:<subprotocol>:<subname>. See URLs
in General Use and JDBC
URLs in Getting Started with the JDBC API for more
information.
One obvious point that is often taken for granted: the use of URLs
means that JDBC applications are more or less automatically network and
internet enabled. Given that this sample uses Cloudscape's driver, the
URL required takes the following form: jdbc:cloudscape:rmi:jGuru;create=true
Using the DriverManager class, you request a
Connection using the passed URL and the
DriverManager selects the appropriate driver; here, only
the Cloudscape driver is loaded. Here's the standard form of the
Connection request: Connection con = DriverManager.getConnection(
URL,
Username,
Password );
This form is best for portability even in cases where
Username and Password are empty strings (
"" ) due to a database default or, say, text files acting
as ODBC data sources, which cannot make use of such attributes.
For Cloudscape's driver, this is the actual point at which the database
is created due to the 'create=true' URL attribute, which will be dropped
for later connections.
While the Connection class has a number of capabilities,
in order to use DDL or Data Manipulation Language ( DML ) SQL statements,
a Statement object is required. So, the next step is to ask
the Connection for a Statement object: Statement stmt = con.createStatement(); At this point, the
program can begin to do some actual work. To store the data, the example
creates a table named JJJJData in the jGuru database.
Following is the SQL statement to do that, which includes the columns
needed for each data item. SQL keywords are capitalized in the sample for
better visibility, but this is a programmer preference and not necessary
in your code. CREATE TABLE JJJJData (
Entry INTEGER NOT NULL,
Customer VARCHAR (20) NOT NULL,
DOW VARCHAR (3) NOT NULL,
Cups INTEGER NOT NULL,
Type VARCHAR (10) NOT NULL,
PRIMARY KEY( Entry )
)
The program code to do this is: stmt.executeUpdate( "CREATE TABLE JJJJData (" +
"Entry INTEGER NOT NULL, " +
"Customer VARCHAR (20) NOT NULL, " +
"DOW VARCHAR (3) NOT NULL, " +
"Cups INTEGER NOT NULL, " +
"Type VARCHAR (10) NOT NULL," +
"PRIMARY KEY( Entry )" +
")" );
Notice that no terminator is supplied for the actual SQL statement.
The various databases use different terminators, and portability is
promoted by using none in the listed code. Instead, the task of
inserting the proper terminator is delegated to the driver.
The code also indicates to the database that none of the columns may be
NULL, mostly to avoid a sometimes troublesome area for SQL newcomers, and
defines a primary key to identify each row.
Now that the table has been created, the data can be entered using the
SQL INSERT statement: INSERT INTO JJJJData VALUES ( 1, 'John', 'Mon', 1, 'JustJoe' )
INSERT INTO JJJJData VALUES ( 2, 'JS', 'Mon', 1, 'Cappuccino' )
INSERT INTO JJJJData VALUES ( 3, 'Marie', 'Mon', 2, 'CaffeMocha' )
...
In the example program, an array named SQLData contains
the actual values, with each element in a form like this: "(1, 'John', 'Mon', 1, 'JustJoe')" The program code
corresponding to the INSERT statements above is: stmt.executeUpdate(
"INSERT INTO JJJJData VALUES " + SQLData[i] );
To briefly review the discussion so far: First, any JDBC program loads
a JDBC driver and creates a URL using the jdbc protocol (
including an attribute to create the database here ). At that point, the
program can connect to the database. Next, the returned
Connection object is asked for a Statement . The
specific example for this section then uses SQL statements passed to the
driver to create and populate the JJJJData table.
The exercise for this section includes the source code for a complete
application to create the table JJJJData and insert the
required rows.
Exercise
- Creating
and Populating a Table
To retrieve information from a database, you send SQL
SELECT statements to the database via the
Statement.executeQuery method, which returns the requested
information as rows of data in a ResultSet object. A default
ResultSet is examined row by row using
ResultSet.next() ( to position to the next row ) and
ResultSet.getXXX() to obtain individual column
data.
Consider, for example, how to obtain the maximum number of cups of
coffee consumed by a 4J Cafe customer in one day. In terms of SQL, one way
to get the maximum value is to sort the table by the Cups
column in descending order using the ORDER BY clause. The
first row in the returned ResultSet contains the largest
value for Cups . All columns are selected so that the program
can report and verify that the data was entered into the table as
expected. Use the SQL statement: SELECT Entry, Customer, DOW, Cups, Type
FROM JJJJData
ORDER BY Cups DESC
In a program, execute the SQL statement with: ResultSet result = stmt.executeQuery(
"SELECT Entry, Customer, DOW, Cups, Type " +
"FROM JJJJData " +
"ORDER BY Cups DESC");
ResultSet.next() returns a boolean: true if there is a
next row and false if not (meaning the end of the data/set has been
reached). Conceptually, a pointer or cursor is positioned just before the
first row when the ResultSet is obtained. Invoking
next() moves to the first row, then the second and so on. To
get the first row, the one with the most Cups, takes some special
handling:
if( result.next() ) The if-statement collects the data. After
that, a loop while(result.next()) is used, to allow the program to continue
to the end of the data.
Once positioned at a row, the application can get the data on a
column-by-column basis using the appropriate
ResultSet.getXXX method. Here are the methods used
in the example to collect the data, as well as code to sum the Cup column
for each row: iEntry = result.getInt("Entry");
Customer = result.getString("Customer");
DOW = result.getString("DOW");
Cups = result.getInt("Cups");
TotalCups += Cups; // increment total
Type = result.getString("Type");
The program uses standard out for reporting with
System.out.println() .
If all goes well, the output shows that:
JS consumed the most coffee, 9 Espressos on Friday!
The total cups of coffee consumed was 48.
The row by row output is:
12 |
JS |
Fri |
9 |
Espresso |
4 |
Anne |
Tue |
8 |
Cappuccino |
11 |
jDuke |
Thu |
4 |
JustJoe |
8 |
JS |
Wed |
4 |
Latte |
7 |
Marie |
Wed |
4 |
Espresso |
13 |
John |
Fri |
3 |
Cappuccino |
9 |
Alex |
Thu |
3 |
Cappuccino |
6 |
jDuke |
Tue |
3 |
Cappuccino |
14 |
Beth |
Fri |
2 |
Cappuccino |
5 |
Holley |
Tue |
2 |
MoJava |
3 |
Marie |
Mon |
2 |
CaffeMocha |
15 |
jDuke |
Fri |
1 |
Latte |
10 |
James |
Thu |
1 |
Cappuccino |
2 |
JS |
Mon |
1 |
Cappuccino |
1 |
John |
Mon |
1 |
JustJoe |
Note that the ResultSet is ordered by
Cups only. Therefore, there is no guarantee of the order for
entries with the same number of cups. For example, the entries with 3 cups
for John, Alex, and jDuke may appear in any order. All three entries will
come after entries with 4 or more cups and before entries with 2 or fewer
cups (remember that descending order was requested), but that's all that
really can be said.
The exercise for this section includes the source code for a complete
application to examine the JJJJData table and generate
the report.
Exercise
- Data
Retrieval
In concluding this section, remember that:
- JDBC is portable.
The driver name and URL, user, and password data have been hard-coded
here to keep things simple. By substituting variables for this
information, these programs will run with any JDBC Compliant driver.
- All of the code and material presented in this section
applies to and runs under JDK 1.1 and JDBC 1.2 with the proper
driver.
From this point on, however, the course assumes that JDK 1.3 and JDBC
2.0 are available (but most of the material runs happily under JDK 1.2
as well).
A Connection
object represents and controls a connection to a database.
Connection basics have already been discussed in Connecting
to the Database; this section clarifies a few points, mentions the
various areas that a Connection controls, and presents two
exercises that demonstrate a general method to provide the information
required to connect successfully.
While everything in JDBC depends on the capabilities of the database
and the JDBC driver, in general, you can have multiple connections to the
same database and/or connections to multiple databases. The DriverManager
class handles driver registration and provides methods for obtaining a
Connection . Note that all DriverManager methods are static;
there's no need to create an instance.
One of the first steps in obtaining a Connection is often
the most frustrating: how to set up that @#$!!!@# database URL? As
mentioned earlier, the basics look very clean
jdbc:<subprotocol>:<subname>, with the
<subprotocol>: identifying the machine or server and <subname>
essentially identifying the database. In practice, the content depends on
the specific driver and can be bewildering, ranking along with classpath
problems in producing "no suitable driver" errors. Consider the Cloudscape
URL used in the previous examples: jdbc:cloudscape:rmi:jGuru
which translates into jdbc: <subprotocol>: <subname>
jdbc: cloudscape:rmi: jGuru
This is fairly straightforward, primarily because the client and the
server run on the same machine. Similar URLs are often seen with drivers
below a type 4, because there is some other setup involved and the
information required to locate a server is obtained from the setup
information.
Even here, things are not always as they seem. Most DBMS engines that
support remote (and even local) connections do so using a TCP/IP
(Transmission Control Protocol/Internet Protocol) port. Actually, even
Cloudscape does with the cloudscape:rmi: subprotocol; run
netstat after starting Cloudscape and you will see it
listening on port 1099. Like any other socket program, the DBMS engine is
free to decide what port it wants to use. While TCP/IP is generally the
norm, other communication protocols may be used. DB2, for example, can
also use APPC (Advanced Program to Program Communication) on several
platforms.
When applications attempt to connect to a network or internet server,
identification/location information must be provided. The general JDBC way
is to use //host:port/subsubname , where host is an IP address
or DNS (Domain Name Service) or other locatable name. Check your
driver/database documentation for the default port, and remember that a
system administrator can decide to use a different one. Here the database
becomes the subsubname and the driver writer is free to allow additional
attributes in their own syntax. Using Cloudscape as an example again, this
code is used to create the database: jdbc:cloudscape:rmi:jGuru;create=true
The ;create=true portion is an attribute using Cloudscape
syntax. The moral is: review the documentation for your driver and
database.
A Connection is automatically closed when it is garbage
collected, but cautious programmers always close the
Connection explicitly to directly determine that and when
this occurs and to conserve resources. Note that while the API
specifically says that closing a Connection "releases...
database and JDBC resources immediately," the JDBC recommendation is to
explicitly close Connection s and Statement s.
Connection , like other important areas of the JDBC
API, is an Interface . Many programmers wonder where the
objects come from since an Interface can't be instantiated. Short answer:
the JDBC driver implements the interface and returns real
objects when requested. This also explains why an application compiles
perfectly and then may have numerous problems at runtime: code is compiled
against the standard interface, and only gets the real thing once the
program and driver are loaded and running.
Most of the preceding section relates to setup for
DriverManager 's getConnection() methods. The
Connection itself is responsible for several areas including:
- Creating
Statement , PreparedStatement , and
CallableStatement (used with stored procedures) instances.
- Obtaining
DatabaseMetadata objects.
- Controlling transactions via the
commit() and
rollback() methods.
- Setting the isolation level involved in transactions.
There's even a method to obtain any SQL statement in a given
database's native dialect, appropriately named nativeSQL() .
Several of these areas are discussed in later sections of the course.
Before moving on, the new DataSource
class introduced in the JDBC 2.0
Optional Package should be mentioned. The specification recommends
DataSource as the means for obtaining a
Connection and actually talks about deprecating the current
DriverManager / Connection method. While the
JDBC programmer should be aware of this movement, and may even use
it--most commonly in a J2EE environment,--it would be very surprising to
see the DriverManager approach abandoned anytime soon.
It should be evident from the above discussion of information needed to
obtain a Connection object that hardcoding the information is
not a rewarding decision. The following exercises provide two methods of
obtaining this information--using a ResourceBundle and/or
getting it directly from the end user--in two common programming
scenarios.
You may wonder if the "sa" and "admin" that the exercises set for
userID and password are Cloudscape defaults or just magic. The answers are
that, out of the box, authentication/security is not enabled for
Cloudscape; you have to set it up yourself. Otherwise it just ignores
invalid arguments and attributes. These effective dummies have been
included to give the feel of the JDBC standard Connection
arguments from the beginning. This should again underscore the importance
of reviewing your driver and database documentation. The second answer is
that, in programming, as in many other areas, there may be mirrors, but
there ain't no magic.
Exercises
- Generalizing
Connection Information - Batch
- Generalizing
Connection Information - Interactive
A Statement
object is a container or transport mechanism to send/execute (normally)
SQL statements and retrieve any results via its associated
Connection . As mentioned in Areas
Controlled by the Connection Interface, there are three types of
Statement s, including Prepared
Statements and Callable
Statements, both of which are subinterfaces of Statement .
As noted earlier, you do not create a new instance of
Statement , but instead, request the associated
Connection to create one: Statement stmt = con.createStatement();
The execute series are the most often used of
Statement 's methods:
- executeQuery() is used to execute SQL statements that
return a single ResultSet.
- executeUpdate() is used to execute SQL statements that
modify a table or values of columns in a table and return the number of
rows affected (which is zero in the case of DDL statements).
- execute() can be used to execute any type of SQL statement,
but is intended for those that can return multiple results or values.
execute() is not discussed further in the course.
To allow the most flexibility to work with various databases and data
sources, JDBC places no restriction on the kinds of SQL statements that a
Statement can send. In fact, if the data source can
understand it (and this is a programmer responsibility ), the statements
don't even have to be SQL, which raises some interesting possibilities.
However, a driver that claims to be JDBC Compliant must support
at least ANSI SQL-92 Entry Level capabilities.
A Statement should automatically be closed when the
Connection is garbage collected, but you should close it
yourself as soon as it is no longer needed. The JDBC recommendation is to
always close the Statement explicitly.
Update has a specific meaning to programmers and, indeed, to
SQL, so executeUpdate() is probably an unfortunate name for a
method that is used to execute DML ( INSERT ,
UPDATE , and DELETE ) statements as well as DDL
statements such as CREATE TABLE , DROP TABLE , and
ALTER TABLE . Regardless, it is used for all of these; in
fact, as a rule of thumb, use it for anything that does not return a
ResultSet .
JDBC defines types to match SQL data types. These must be appropriate
to the data to avoid technical problems, unanticipated results, and to
promote job retention. See Java-SQL
Type Equivalence for further information on the available and
appropriate types.
executeUpdate() returns an int containing the
affected row count for INSERT, UPDATE, or DELETE statements, or zero for
SQL statements that do not return anything, like DDL statements.
Exercise
- Using
executeUpdate()
executeQuery() is used for Statement s that
return a ResultSet,
basically a SELECT statement.
The default ResultSet object returned by
executeQuery() has a cursor that moves forward only, by use
of the next() method. It should be noted that
executeQuery() always returns a non-null
ResultSet . Newcomers often try to determine if rows were
returned by comparing the ResultSet to null .
Short of driver error, this never happens. next() returns a
boolean value, which is true if another row is available and
false if the ResultSet is exhausted. You may use
an if statement if you anticipate that only one row will be
returned. Otherwise a while loop is the norm: int iCount = 0;
while( myResultSet.next() )
{
// retrieve column data
// do something with it
iCount++;
}
if( iCount == 0 )
{
System.out.println(
"myResultSet returned no data.");
}
else
if( bNoErrorsOrExceptionsOrEarlyTerminations )
{
System.out.println(
"All rows from myResultSet were processed.");
}
Columns should be read from left to right (the same order as in the
SELECT ) statement and can be obtained by column name or
index. Using an index is preferred for efficiency ( and goes 1,2,3...
not 0,1,2,3...) whereas column names may lead to more
understandable code. Databases and drivers may vary, but for portability
you should expect that in a default ResultSet you may only
get a row, and even a column from that row, exactly once.
ResultSet 's getXXX() methods are used to retrieve
column data. JDBC defines types to match the SQL data types and there is a
getXXX() method for each. See Java-SQL
Type Equivalence for further information on the available and
appropriate types.
A Statement only keeps one ResultSet open at
a time and often reuses the same ResultSet for new data. You
should be sure to get all the data required from the
ResultSet before executing another query via its associated
Statement . A Statement should automatically
close() the ResultSet on re-execution and on
Statement.close() , but you may want to close the
ResultSet yourself as soon as its data is no longer needed.
Cautious programmers may always close the ResultSet
explicitly.
A ResultSet can also return metadata, which is
information about the ResultSet itself and the data it
contains. This is discussed further in ResultSet
Metadata.
Exercise
- Selecting
Data and Presenting Information
A PreparedStatement
is a subinterface of Statement that offers several benefits:
- The contained SQL is sent to the database and compiled or prepared
beforehand. From this point on, the prepared SQL is sent and this step
is bypassed. The more dynamic
Statement requires this step
on every execution. Depending on the DB engine, the SQL may be cached
and reused even for a different PreparedStatement and most
of the work is done by the DB engine rather than the driver.
- A
PreparedStatement can take IN
parameters, which act much like arguments to a method, for column
values.
PreparedStatement s deal with data conversions that can
be error prone in straight ahead, built on the fly SQL; handling quotes
and dates in a manner transparent to the developer, for example.
Note: The SQL3 types, in general, assume usage of prepared
statements for DML.
Here are two examples of setting up and obtaining prepared statements: pstmtU = con.prepareStatement(
"UPDATE myTable SET myStringColumn = ? " +
"WHERE myIntColumn = ?" );
pstmtQ = con.prepareStatement(
"SELECT myStringColumn FROM myTable " +
"WHERE myIntColumn = ? ");
The question marks are stand-ins for values to be set before statement
execution and are called parameter markers. These are referred to
by number, starting from 1, in left to right order.
PreparedStatement 's setXXX() methods are used to set
the IN parameters, which remain set until changed. Again, see
Java-SQL
Type Equivalence for information on the available types. Here's an
example for setting the parameters in the previous statements: pstmtU.setString( 1, "myString" );
pstmtU.setInt( 2, 1024 );
pstmtU.executeUpdate();
pstmtQ.setInt( 1, 1024 );
pstmtQ.executeQuery();
You can also prepare a statement that has no parameters. Note that
PreparedStatement has its own version of the
execute method series, which have no arguments, due to
setting the parameters. Remember that PreparedStatement
inherits from Statement and includes all of
Statement 's functionality. In general, consider prepared
statements when a query is run multiple times and only the values of the
same columns change or the same query is run repeatedly.
Exercise
- Using
Prepared Statements
JDBC defines Types
to provide generic SQL types for conversion to standard Java types. In
general, it's straightforward to determine the types and methods needed.
The following two tables show the normal ResultSet methods
used to get each data type. Typically the setXXX() methods
follow the same patterns.
Common SQL Types--Standard Retrieval Methods
SQL Type |
Java Method |
BIGINT |
getLong() |
BINARY |
getBytes() |
BIT |
getBoolean() |
CHAR |
getString() |
DATE |
getDate() |
DECIMAL |
getBigDecimal() |
DOUBLE |
getDouble() |
FLOAT |
getDouble() |
INTEGER |
getInt() |
LONGVARBINARY |
getBytes() |
LONGVARCHAR |
getString() |
NUMERIC |
getBigDecimal() |
OTHER |
getObject() |
REAL |
getFloat() |
SMALLINT |
getShort() |
TIME |
getTime() |
TIMESTAMP |
getTimestamp() |
TINYINT |
getByte() |
VARBINARY |
getBytes() |
VARCHAR |
getString() |
For display purposes, ResultSet.getString() can be also be
used on the above types, with the possible exception of
OTHER .
SQL3 Types--Retrieval Methods
SQL Type |
Java Method |
ARRAY |
getArray() |
BLOB |
getBlob() |
CLOB |
getClob() |
DISTINCT |
getUnderlyingType() |
REF |
getRef() |
STRUCT |
(castToStruct)getObject() |
JAVA_OBJECT |
(castToObjectType)getObject()
|
ResultSet.getObject() can be also be used on any of the
listed types in both tables.
This may seem very clear and basic, but the professional programmer
should spend some time reading both Mapping
SQL data types into Java and Mapping
SQL and Java Types. In particular, review the table Conversions
by ResultSet.getXXX() Methods to see the variety of options
available.
"How do I get the type into the database in the first place?" is a
question frequently asked regarding the SQL3 types that use Locators,
due to unfortunate gaps in the documentation. The best general answer to
this is to look at the corresponding class (for example, Blob
for BLOB ), and look to the getXXX()
methods for materializing the data for clues to which
setXXX() methods to use, generally with
PreparedStatement . For Blob , these are
getBinaryStream() and getBytes() , hence
setBinaryStream() and setBytes() . See LOBs
and the section's associated exercises for more information and example
code.
"I don't want to think about it." That's probably an honest developer's
response to queries regarding exception/error handling in general, which
is difficult to do properly and usually unrewarding. It is also critical
to production quality applications.
The exercises in this course highlight specific JDBC areas and make no
pretensions about being production quality. At the same time, a level of
exception handling has been present, starting with the first exercise.
However, that level has not been complete and it's time to remedy that
with an introduction to the three types of SQLExceptions .
Note that a fourth type, BatchUpdateException was
added in JDBC 2.0, which is discussed in Batch
Update Facility.
Many of the methods in the java.sql package throw an
SQLException
, which requires a try/catch block like any other
Exception . Its purpose is to describe database or driver
errors (SQL syntax, for example). In addition to the standard
getMessage() inherited from Throwable ,
SQLException has two methods which provide further
information, a method to get (or chain) additional exceptions and
a method to set an additional exception.
getSQLState() returns an SQLState identifier based on
the X/Open SQL specification. Your DBMS manuals should list some of
these or see Resources
for information to find SQLStates.
getErrorCode() is provided to retrieve the
vendor-specific error code.
getNextException() retrieves the next
SQLException or null if there are no more. Many things can
go wrong between your program and the database. This method allows
tracking all problems that occur.
setNextException() allows the programmer to add an
SQLException to the chain.
These methods should be fairly straightforward. Typical
catch code would look similar to the following: try
{
// some DB work
} // end try
catch ( SQLException SQLe)
{
while( SQLe != null)
{
// do handling
SQLe = SQLe.getNextException();
}
} // end catch
Tip: Programmers are often perplexed by syntax errors, which
seem to refer to some invisible operation, like "ungrok found at line 1,
position 14." Consistently reporting the output of
Connection.nativeSQL(yourQueryString) in exception handlers
will clarify matters.
An SQLWarning
is a subclass of SQLException , but is not thrown like other
exceptions. The programmer must specifically ask for warnings.
Connection s, Statement s, and
ResultSet s all have a getWarnings() method that
allows retrieval. There is also a clearWarnings() method to
avoid duplicate retrievals. The SQLWarning class itself only
adds the methods getNextWarning() and
setNextWarning() .
An SQLWarning is very similar to traditional compiler
warnings: something not exactly right occurred, but its effect was not
severe enough to end processing. Whether it is important enough to
investigate depends on the operation and context. An example of an
SQLWarning is mentioned in the Scrollable
Result Sets section.
Statement s clear warnings automatically on the next
execution. ResultSet s clear warnings every time a new row is
accessed. The API documentation is silent regarding
Connection ; to be cautious, issue
clearWarnings() after warnings are obtained.
Typical code for obtaining SQLWarning s looks similar to
this: try
{
...
stmt = con.createStatement();
sqlw = con.getWarnings();
while( sqlw != null)
{
// handleSQLWarnings
sqlw = sqlw.getNextWarning();
}
con.clearWarnings();
stmt.executeUpdate( sUpdate );
sqlw = stmt.getWarnings();
while( sqlw != null)
{
// handleSQLWarnings
sqlw = sqlw.getNextWarning();
}
} // end try
catch ( SQLException SQLe)
{
...
} // end catch
DataTruncation
is sort of an oddball subclass of SQLWarning . If it
occurs on a read, an SQLWarning is issued, if it occurs on a
write/update, an SQLException is thrown. In practice, it is
only a concern on write/update operations, and therefore handled as an
SQLException , which always has an SQLState of
01004 .
Data truncation basically means that less information was read or
written than requested. Some databases/drivers will accept data
that is larger than a column can contain, truncate the data, write the
truncated data, and then happily report, via a DataTruncation
SQLException "You gave me too much data, but I handled it."
The DataTruncation class includes the following methods
for information about the truncated data: getDataSize() ,
getIndex() , getParameter() ,
getRead() , and getTransferSize() .
The following is a set of actual error information, as returned from
Cloudscape, UDB2/NT, and DB2/400, resulting from data specifically
prepared to exhibit problems. See this section's exercise
for details.
- DELETE FROM JJJJTee
WHERE Entry = 97
CS Result: 0 rows processed.
UDB2/NT Result: 0 rows processed.
DB2/400 Result: DELETE FROM JJJJTee WHERE Entry = 97 Statement
Warnings: [SQL0100] Row not found for DELETE. SQL State:
02000 Vendor Error Code: 100 0 rows processed.
- INSERT INTO JJJJTee
VALUES (25, 'Rosa', 'Petite',
'Blue')
CS Result: INSERT INTO JJJJTee VALUES (25, 'Rosa', 'Petite',
'Blue') problems with executeUpdate: The statement was aborted
because it would have caused a duplicate key value in a unique or
primary key constraint. SQL State: 23500 Vendor Error Code:
20000
UDB2/NT Result: INSERT INTO JJJJTee VALUES (25, 'Rosa', 'Petite',
'Blue') problems with executeUpdate: [IBM][CLI Driver][DB2/NT]
SQL0803N One or more values in the INSERT statement, UPDATE statement,
or foreign key update caused by a DELETE statement are not valid because
they would produce duplicate rows for a table with a primary key, unique
constraint, or unique index. SQLSTATE=23505 SQL State:
23505 Vendor Error Code: -803
DB2/400 Result: INSERT INTO JJJJTee VALUES (25, 'Rosa', 'Petite',
'Blue') problems with executeUpdate: [SQL0803] Duplicate key value
specified. SQL State: 23505 Vendor Error Code: -803
- UPDATE JJJJTee
SET TColor =
'Black' WHERE TColor = 'Appetite'
CS Result: 0 rows processed.
UDB2/NT Result: 0 rows processed.
DB2/400 Result: UPDATE JJJJTee SET TColor = 'Black' WHERE TColor =
'Appetite' Statement Warnings: [SQL0100] Row not found for
UPDATE. SQL State: 02000 Vendor Error Code: 100 0 rows
processed.
- DROP TABLE IDontExist
CS Result: DROP TABLE IDontExist problems with
executeUpdate: Table 'IDONTEXIST' does not exist. SQL State:
42X05 Vendor Error Code: 20000
UDB2/NT Result: DROP TABLE IDontExist problems with
executeUpdate: [IBM][CLI Driver][DB2/NT] SQL0204N "userID.IDONTEXIST"
is an undefined name. SQLSTATE=42704 SQL State: 42S02 Vendor Error
Code: -204
DB2/400 Result: DROP TABLE IDontExist problems with
executeUpdate: [SQL0204] IDONTEXIST in JGURU type *FILE not
found. SQL State: 42704 Vendor Error Code: -204
- UPDATE JJJJTee
SET TSize =
'Small Doppelganger' WHERE
TSize = 'Small'
CS Result: UPDATE JJJJTee SET TSize =
'Small Doppelganger' WHERE TSize =
'Small' problems with executeUpdate: Non-blank characters were
found while truncating string
'Small Doppelganger' from length 22 to
length 10. SQL State: 22001 Vendor Error Code: 20000
UDB2/NT Result: UPDATE JJJJTee SET TSize =
'Small Doppelganger' WHERE TSize =
'Small' problems with executeUpdate: [IBM][CLI Driver][DB2/NT]
SQL0433N Value "Small Doppelganger" is too
long. SQLSTATE=22001 SQL State: 22001 Vendor Error Code: -433
DB2/400 Result: UPDATE JJJJTee SET TSize =
'Small Doppelganger' WHERE TSize =
'Small' problems with executeUpdate: [SQL0404] Value for column or
variable TSIZE too long. SQL State: 22001 Vendor Error Code:
-404
- UPDATE JJJJTee
SET TSize =
'Small '
WHERE TSize = 'Small'
CS Result: 3 rows processed.
UDB2/NT Result: 3 rows processed.
DB2/400 Result: 3 rows processed.
- DROP TSBLE BadSQL
CS Result: DROP TSBLE BadSQL problems with
executeUpdate: Syntax error: Encountered "TSBLE" at line 1, column
6. SQL State: 42X01 Vendor Error Code: 20000
UDB2/NT Result: DROP TSBLE BadSQL problems with
executeUpdate: [IBM][CLI Driver][DB2/NT] SQL0104N An unexpected token
"TSBLE" was found following "DROP ". Expected tokens may include: "JOIN
<joined_table>". SQLSTATE=42601 SQL State: 42601 Vendor
Error Code: -104
DB2/400 Result: DROP TSBLE BadSQL problems with
executeUpdate: [SQL0104] Token TSBLE was not valid. Valid tokens:
DISTINCT DATA. SQL State: 42601 Vendor Error Code:
-104
Exercise
- Handling
SQLExceptions and SQLWarnings
Metadata is data (or information) about data. JDBC allows the
programmer to discover a large amount of information about a database and
any given ResultSet via metadata classes.
In order to discover information about a database, a DatabaseMetaData
object must be obtained. Once a program has obtained a valid
Connection , this code gets a metadata object: DatabaseMetaData dbmd = con.getMetaData();
The good news is that, at that point, you just call methods for the
desired information. Most of the bad news is:
- There are approximately 150 methods in the
DatabaseMetaData class. Clearly, mastering (or even being
aware of) the available information is a major task. However, a scan of
the API can help.
- Many of the methods return
ResultSet s, which the
programmer has to step through to get the specific information.
- Several of the methods, including those that return information
about database and table components, use confusing name patterns.
Depending on the database, the information may be in upper, lower, or
mixed case, and the patterns are case-sensitive. As a result, there are
methods that need to be called to discover how the information is stored
before attempting to get the information.
While this can be discouraging, the most common
DatabaseMetaData information, like database name, driver
name, version, maximum connections available, SQL conformance, and so on,
is easily obtained. Many programs won't need such information at all. Note
that a given DBMS may not provide information for all of the methods, so
check returned objects for nulls or empty strings.
Links are provided to programs in the course that use
DatabaseMetaData in the "Exercises" section below. Go to the
program "Solution" portion of each exercise and scan on
DatabaseMetaData for example usage.
Exercises
- Generalizing
Connection Information - Batch
- Determining
Available Scalar Functions
- Using
Batch Updates
- Paging
with Scrollable ResultSets
In order to discover information about a given ResultSet ,
a ResultSetMetaData
object must be obtained. Once a program has obtained a valid
ResultSet , this code gets a metadata object: ResultSetMetaData rsmd = rs.getMetaData();
The ResultSetMetaData class is more manageable than
DatabaseMetaData , with around 25 methods. Using
ResultSetMetaData , an application can discover the number of
columns returned, an individual column's suggested display size, column
names, column types, and so on. Note that a given DBMS may not provide
information for all of the methods, so check returned objects for nulls or
empty strings.
Links are provided to programs in the course that use
ResultSetMetaData in the "Exercises" section below. Go to the
program "Solution" portion of each exercise and scan on
ResultSetMetaData for example usage.
Exercises
- Generalizing
Connection Information - Batch
- Generalizing
Connection Information - Interactive
- Selecting
Data and Presenting Information
- Paging
with Scrollable ResultSets
Most databases provide scalar functions (sometimes referred to as
built in functions) that can be used to perform an operation on
the specific value of a column, or even to provide the value of a
built-on-the-fly column. The JDBC specification supports the various math,
string, system, time and date, and type conversion functions specified by
the X/Open Call Level Interface (CLI), and JDBC Compliant drivers must as
well, if the underlying DBMS supports the functionality. The
names of these functions should match the X/Open names, although
this is not always the case. Scalar functions can be valuable for their
functionality or to shift work to the database from your application.
JDBC provides these methods to determine the scalar functions:
getNumericFunctions() , getStringFunctions() ,
getSystemFunctions() , getTimeDateFunctions() ,
and two versions of supportsConvert() . The
getXXXFunctions() methods return the function names in a
comma delimited String .
Because different databases use differing syntax for scalar function
invocation, JDBC defines a specific escape syntax. The JDBC driver should
understand this syntax and map it to the proper syntax for the underlying
database. Escapes are also used for LIKE characters, date and
time literals, stored procedure calls and outer joins. The escape for
scalar functions is fn . The actual function name, along with
any arguments, are enclosed in curly braces, as { fn <scalar
function()> } .
Scalar functions are normally used with columns in an SQL statement.
For example, the PI() numeric function can be used as:
UPDATE myTable SET circularVal = squared * { fn PI()
} ...
or
SELECT { fn concat( string, "bean" ) } ...
Consult your DBMS manuals for supported functionality.
Exercise
- Determining
Available Scalar Functions
Stored procedures are user-generated functions or procedures that, once
registered with the database, can be called by client applications. They
can be very valuable because they shift work to the server and reduce
coding, particularly with complex operations. Unfortunately, there is no
standard for manner of, requirements for, or even language for creating
stored procedures. And not all databases support them. Given this state of
affairs, there is no way to create a generally useful exercise, so this
section is limited to a discussion of and code snippets for invoking
stored procedures using the JDBC standard method. Of course, creating a
stored procedure is a one-time operation, and you are normally told the
name and type of parameters required.
There are several DatabaseMetaData methods that return
information about the support that a particular data provides for stored
procedures.
supportsStoredProcedures() determines if the DBMS
supports JDBC standard stored procedure escape syntax.
getProcedures() returns a list of available stored
procedures, while getProcedureColumns() describes
parameters and results.
getProcedureTerm() informs the programmer of the
vendor's preferred name for stored procedures.
When invoked, as with standard methods or functions, a stored procedure
can receive zero or more arguments or parameters, referred to as IN
parameters. They can return a ResultSet , update count, result
parameter, and/or zero or more OUT parameters. In addition, a stored
procedure can have INOUT parameters, in which case a value is sent in and
a different value is returned in the same variable. IN, OUT, and INOUT
parameters are all enclosed in a parenthetical expression and
distinguished only by number, which corresponds to the order of parameter
marker (?--the question mark) appearance, starting with 1, not zero.
As mentioned in Escape
Syntax and Scalar Functions, stored procedures require JDBC escape
syntax for standard invocation. Again, the driver handles the actual
mapping. The basic format consists of call sp_name or ?
= call sp_name with optional parameters, all enclosed in curly
braces. Several example forms are shown below and are discussed in more
detail in the following paragraphs.
A - takes no parameters and returns nothing, a ResultSet
or a row count: { call sp_A }
B - single parameter and returns a result parameter. Assumes
int result parameter and a String IN parameter: { ? = call sp_B( ? ) }
C - multiple parameters and returns nothing, a
ResultSet or a row count. Assumes int IN, OUT,
and INOUT parameters: { call sp_C( ? ? ? ) }
To actually send the request for stored procedure execution to the
database, you use a CallableStatement ,
which extends PreparedStatement . When creating the
CallableStatement , the escape syntax discussed above is used
in quotes or as a String variable. You should be sure to be
especially careful to get the syntax right; you're just sending a
String .
A - CallableStatement cstmt =
con.prepareCall( "{ call sp_A }" );
B - CallableStatement cstmt =
con.prepareCall( "{ ? = call sp_B( ? ) }" );
C - CallableStatement cstmt =
con.prepareCall( "{ call sp_C( ? ? ? ) }" );
Before invoking a stored procedure, the parameter markers must be
matched up with variables and types. See Java-SQL
Type Equivalence for type information.
- IN parameters are set using the
setXXX() methods
inherited from PreparedStatement .
- OUT parameters must be registered, using one of the
CallableStatement.registerOutParameter() methods.
- INOUT parameters must be both set and registered.
The actual invocation will, as usual, use executeQuery() ,
executeUpdate() , or execute() depending on the
expected result.
A - CallableStatement cstmt =
con.prepareCall( "{ call sp_A }" );
For no return: cstmt.execute(); // could use executeUpdate()
For returned ResultSet : ResultSet rs = cstmt.executeQuery();
For returned update count: int iUC = cstmt.executeUpdate();
B - CallableStatement cstmt =
con.prepareCall( "{ ? = call sp_B( ? ) }" );
// int result parameter
cstmt.registerOutParameter( 1, Types.INTEGER );
// String IN parameter
cstmt.setString( 2, "M-O-O-N" );
cstmt.execute(); // could use executeUpdate()
int iRP = cstmt.getInt( 1 );
C - CallableStatement cstmt =
con.prepareCall( "{ call sp_C( ? ? ? ) }" );
Setup: // set int IN parameter
cstmt.setInt( 1, 333 );
// register int OUT parameter
cstmt.registerOutParameter( 2, Types.INTEGER );
// set int INOUT parameter
cstmt.setInt( 3, 666 );
// register int INOUT parameter
cstmt.registerOutParameter( 3, Types.INTEGER );
For no return ( other than OUT and INOUT: ) cstmt.execute(); // could use executeUpdate()
// get int OUT and INOUT
int iOUT = cstmt.getInt( 2 );
int iINOUT = cstmt.getInt( 3 );
For returned ResultSet : ResultSet rs = cstmt.executeQuery();
// get int OUT and INOUT
int iOUT = cstmt.getInt( 2 );
int iINOUT = cstmt.getInt( 3 );
For returned update count: int iUC = cstmt.executeUpdate();
// get int OUT and INOUT
int iOUT = cstmt.getInt( 2 );
int iINOUT = cstmt.getInt( 3 );
All of this is detailed work, but the pattern should be clear.
In SQL terms, a transaction is one or more statements that
comprise a logical unit of work (LUW). This means that, in some sense,
everything is a transaction. Normally, however, the term
transaction is used to mean an all-or-nothing series of operations; that
is, everything should complete successfully or nothing should.
The classic example of a transaction is withdrawing money from one bank
account and depositing it in another. If only the withdrawal completes,
money is lost. Another example is debits and credits in a double entry
accounting system: both the debit and credit must complete. A third, which
is seen in this section's exercise, is to ensure that a set of
INSERT s, UPDATE s, or DELETE s all
complete with no errors.
While some SQL dialects have specific begin and end transaction
statements, in general a transaction starts at the beginning of a program
and continues until the statement(s) is (are) committed. At that
point, a new transaction begins. This is the model used by JDBC. A JDBC
driver's default is to autocommit, meaning that the result of
every SQL statement is permanent as soon as it is executed. This is why
the course hasn't had to be concerned with transactions so far, and is
perfectly acceptable in many cases.
Note: In autocommit mode, the commit occurs on
Statement completion. When a Statement returns a
ResultSet , the Statement is not complete until
the last row has been retrieved or the ResultSet is
closed.
Connection 's setAutoCommit(bo
olean autoCommit) method is the key to handling transactions.
Use Connection.setAutoCommit(true) to have every statement
committed; use Connection.setAutoCommit(false) for
programmatic transaction control. This method can be invoked at will and,
if necessary, multiple times in a program. After invoking
Connection.setAutoCommit(false) ,
Connection.commit() , and Connection.rollback()
are used to control LUWs ( yes, this should be LUsW , just as
indexes should be indices, but when in Rome...).
Once autocommit is set to false, all database DML statements can be
seen as temporary until they are committed. JDBC supports commitment with
the Connection.commit()
method. That's basically all that is needed to permanently put everything
since the last commit (or rollback) to the database, although
occasionally timing can be tricky. In the past, many databases set a type
of lock, even for reads, that prevented other users from accessing the
same data. Automatic exclusive read locks are fairly rare at this point in
time, but a good slogan from then that is still applicable is "commit
early and commit often." Just not too early, of course.
Note: DDL statements in a transaction may be ignored or may
cause a commit to occur. The behavior is DBMS dependent and can be
discovered by use of DatabaseMetaData.dataDefinitionCausesTransactionCommit()
and DatabaseMetaData.dataDefinitionIgnoredInTransactions() .
One way to avoid unexpected results is to separate DML and DDL
transactions.
Connection.rollback()
is used to remove operations performed since the previous commit or
rollback. Use this method when an exception occurs or when the program
detects some error condition or error in the data.
Most DBMSes allow multiple users to operate on the data at the same
time. Some times developers do not take enough care with database
concurrency issues. (These developers often have adventurous days and at
least one exciting conversation with the boss when data starts
disappearing or other odd things happen to the database.) The level and
type of concurrency also has an impact on performance.
JDBC recognizes the following Transaction
Isolation Levels, which control concurrency:
- TRANSACTION_NONE
- TRANSACTION_READ_COMMITTED
- TRANSACTION_READ_UNCOMMITTED
- TRANSACTION_REPEATABLE_READ
- TRANSACTION_SERIALIZABLE
Use a Connection getTransactionIsolation()
and setTransactionIsolation
(int level) methods to determine and set the desired isolation
level. A JDBC driver has an isolation level default, usually that of the
underlying database. Not all databases support all of the above
settings.
Appropriate and effective concurrency handling is extremely important
in database operations and many applications simply do not get it right.
Unfortunately, a complete discussion would require almost a course in
itself, so look to your DBMS vendor's information and see Resources.
Here is an example of typical transaction handling code: con.setAutoCommit( false );
...
bError = false;
try
{
for( ... )
{
// validate data, set bError true if error
if( bError )
{
break;
}
stmt.executeUpdate( ... );
}
if( bError )
{
con.rollback();
}
else
{
con.commit();
}
} // end try
catch ( SQLException SQLe)
{
con.rollback();
...
} // end catch
catch ( Exception e)
{
con.rollback();
...
} // end catch
Exercise
- Using
Transactions
The Batch Update Facility is new in JDBC 2.0 and allows multiple
statements to be sent to the database as a unit, which can lead to
improved performance. Be aware that drivers are not required to implement
the functionality and those that do may not implement it in a way that is
more efficient than normal submissions. Even so, there is little extra
effort required to use batch updates, other than reporting, and the
potential gain may well be worthwhile. You can determine driver support by
the DatabaseMetaData.supportsBatchUpdates() method.
JDBC 2.0 Statement s are created with an automatically
associated list of commands. The methods addBatch() ,
clearBatch() , and executeBatch() are provided to
manipulate and execute the list. executeBatch() returns an
array of int s which provide completion or error information
for each SQL statement executed. The JDBC recommendation is to set
autocommit to false when using batch updates "for proper error handling."
Doing so also allows all the benefits of transaction processing.
The int values that can be returned in the update counts
array are:
- -3--Operation error. A driver has the option to
stop at the first error and throw a
BatchUpdateException or
to report the error and continue. This value is only seen in the latter
case.
- -2--The operation was successful, but the number of
rows affected is unknown.
- Zero--DDL statement or no rows affected by the
operation.
- Greater than zero--Operation was successful, number
of rows affected by the operation.
Here is an example of typical batch update: try
{
con.setAutoCommit( false );
...
bError = false;
stmt.clearBatch();
// add SQL statements
stmt.addBatch( sUpdate1 );
stmt.addBatch( sUpdate2 );
stmt.addBatch( sUpdate3 );
// execute the statements
aiupdateCounts = stmt.executeBatch();
} // end try
// catch blocks
...
finally
{
// determine operation result
for (int i = 0; i < aiupdateCounts.length; i++)
{
iProcessed = aiupdateCounts[i];
if( iProcessed > 0 ||
iProcessed == -2
)
{
// statement was successful
...
}
else
{
// error on statement
bError = true;
break;
}
} // end for
if( bError )
{
con.rollback();
}
else
{
con.commit();
}
} // end finally
Statement.executeBatch() can throw a BatchUpdateException ,
which is a subclass of SQLException . Its only additional
method is getUpdateCounts() , which allows the programmer to
obtain the array of update counts for reporting. You would already be
aware that the batch had problems by virtue of catching the exception. One
oddity of BatchUpdateException is that it provides no
chaining method for other BatchUpdateException s, and only
inherits SQLException.getNextException() . Code using the
Batch Update Facility should also catch and handle
SQLException s.
Here's an example of handling a batch update exception: catch( BatchUpdateException bue )
{
bError = true;
aiupdateCounts = bue.getUpdateCounts();
SQLException SQLe = bue;
while( SQLe != null)
{
// do exception stuff
SQLe = SQLe.getNextException();
}
} // end BatchUpdateException catch
catch( SQLException SQLe )
{
...
} // end SQLException catch
Exercise Note: UDB2/NT returns false from
DatabaseMetaData.supportsBatchUpdates() . The following
exercise has been tested against Cloudscape and DB2/400.
Exercise
- Using
Batch Updates
To this point, all ResultSet s have been used in a
sequential manner, obtaining rows from beginning to end using
ResultSet.next() . As discussed in Statements,
ResultSets, and Interacting with a Database and seen throughout the
course, ResultSet s are obtained via Statement s,
normally with the method executeQuery . The
Statement s so far have been created with stmt = con.createStatement();
which was the only method available in JDBC 1.0. In JDBC 2.0, a new
method exists which allows the creation of scrollable and/or updatable
ResultSet s: createStatement(
int resultSetType,
int resultSetConcurrency )
resultSetType can be
- ResultSet.TYPE_FORWARD_ONLY--This is the default and the same as in
JDBC 1.0: forward movement only, columns can generally only be read
once. When
ResultSet.next() returns false , the
ResultSet data is no longer available, and generally closed
automatically.
- ResultSet.TYPE_SCROLL_INSENSITIVE allows creation of a
ResultSet in which the cursor can move backwards, forwards,
and at random. This is static data: Any changes made in the database to
the rows selected in the current ResultSet are invisible.
That is, the ResultSet is insensitive to to data
modification.
- ResultSet.TYPE_SCROLL_SENSITIVE allows creation of a
ResultSet in which the cursor can move backwards, forwards,
and at random. This provides a dynamic view of the data: Any changes
made in the database to the rows selected in the current
ResultSet are visible. That is, the ResultSet
is sensitive to to data modification.
resultSetConcurrency can be
- ResultSet.CONCUR_READ_ONLY - This is the default and the same as in
JDBC 1.0.
- ResultSet.CONCUR_UPDATABLE allows programmatic data changes via new
ResultSet methods and positioning capabilities.
Updatable ResultSet s have both advantages and
drawbacks, but are not discussed further in this course. For more
information, see the trail
in The Java Tutorial or Section 3.3 of the Advanced
Tutorial from the online version of the The
JDBC Tutorial and Reference, Second Edition book.
Note that the type of ResultSet requested, even when
supported by the driver, may not be returned. The driver should
issue an SQLWarning on the Connection if this is
the case. In addition,
DatabaseMetaData.supportsResultSetType() can be used to
determine the types of ResultSet s supported by a driver and
ResultSet.getType() provides the type of the actual
ResultSet returned. See Requesting
Features That Are Not Supported for details.
A scrollable ResultSet is obtained like any other,
normally via Statement.executeQuery() . However, with a
scrollable ResultSet the following methods are available:
absolute()
afterLast()
beforeFirst()
first()
getRow()
isAfterLast()
isBeforeFirst()
isFirst()
isLast()
last()
moveToCurrentRow() --effectively valid only with an
updatable ResultSet .
moveToInsertRow() --valid only with an updatable
ResultSet .
previous()
relative() See ResultSet
for details on these methods.
Driver capabilities and implementation levels for scrollable
ResultSets vary, sometimes dramatically. Check the
documentation. Here are a few other factors, and by no means all, to
consider when using scrollable ResultSets:
- A scrollable
ResultSet , just like a nonscrollable one,
is positioned before the first row upon retrieval.
- A
Statement is considered complete when all rows have
been retrieved. This occurs when ResultSet.next() retrieves
the last row. Some drivers take this to mean committing the
Statement at that point when autocommit is on. The outcome
is that the ResultSet is closed and an
SQLException is thrown on the next attempted access. For
portability, set autocommit to false.
ResultSet.getRow() may return zero at certain, or even
all, positions. Among other things, this means that usage of the valued
ResultSet.last() , ResultSet.getRow() sequence
to obtain the number of rows is not reliable across databases,
or even drivers for the same database.
ResultSet.absolute() throws an
SQLException if passed zero.
ResultSet.relative() should not change the cursor
position if passed zero. However, at least one vendor calls
ResultSet.absolute() from ResultSet.relative()
without checking for a zero value. Consider the potential (and
experienced by the author) outcome.
Exercise
- Paging
with Scrollable ResultSets
JDBC 2.0 includes classes for handling several SQL3 data types. This
section discusses LOBs or Large OBjects. Two types of LOBs are
defined: BLOBs --Binary Large OBjects and
CLOBs --Character Large OBjects.
From the perspective of classic relational database theory, a Clob is a
marginal type--a lot of characters--and a Blob isn't really a type at all;
all that is known is that the Blob contains some number of bytes, which
could be anything. It should be clear that this tends to defeat the notion
of data independence, particularly when there are other very acceptable
methods for using a database to track what are essentially graphics,
audio, or other types of binary files. Notice that there is no mechanism
to prevent you from, say, writing an audio file to what is supposed to be
an image Blob, or to know the name of the original source, or any number
of similar considerations.
An SQL Locator type is similar in concept to a pointer or
other information that keeps track of an entity. JDBC developers don't
have to deal with locators, but it is helpful to understand the concept,
because a locator is really what a JDBC driver expects to find in an
Array , Blob , orClob column. That
is, the actual data is not brought down in the ResultSet ,
just the locator. You specifically ask for the LOB data to be returned as
needed, which is called materializing the data. Clearly this is
more efficient than bringing down unknown quantities of bytes for each
column. The actual data is stored to and retrieved from 'somewhere else'
by the DBMS.
A Clob
is a JDBC interface mapping for an SQL CLOB. A Clob is
obtained by one of the getClob() methods of a
ResultSet or CallableStatement . A
Clob has methods to get a substring of the data, the length
of the data, and the position of another Clob or a
String in the current Clob . These methods work
without materializing the data. To materialize the data, one can use
getAsciiStream() or getCharacterStream() (for a
Unicode stream) and then construct usable objects from the returned
stream.
For Clob storage use setClob() from a
PreparedStatement or updateObject() from an
updatable ResultSet . This is where most discussions end, with
the example basically retrieving a Clob from one row and putting it to
another row in the same or a different table.
But how does a Clob get populated in the first place? There's a clue in
the Clob getAsciiStream() and
getCharacterStream() methods: use
PreparedStatement 's setAsciiStream() or
setCharacterStream() methods to populate the
Clob .
A Blob
is a JDBC interface mapping for an SQL BLOB. A Blob is
obtained by the getBlob() methods of a ResultSet
or CallableStatement . A Blob has methods to get
its number of bytes and to determine the starting position of another
Blob or an array of bytes in the current Blob .
These methods work without materializing the data. To materialize the
data, you can use getBinaryStream() or
getBytes() ( for part or all of the Blob ) and
then construct usable objects from the returned stream or byte array.
For Blob storage use setBlob() from a
PreparedStatement or updateObject from an
updatable ResultSet . Again, this is where most discussions
end, with the example retrieving a Blob from one row and putting it to
another row in the same or a different table.
How does Blob data get there in the first place? Again, look at the
Blob methods, this time getBinaryStream() and
getBytes() : use PreparedStatement 's
setBinaryStream() or setBytes() methods to
populate the Blob .
The version of Cloudscape used in this course does not support the SQL3
data types. The following exercises were tested against UDB2/NT and
DB2/400.
Exercises
- Storing
an Image in a Blob
- Retrieving
and Displaying an Image from a Blob
The base requirement for a JDBC Compliant driver is that it must
support the ANSI SQL-92 Entry Level, which is essentially Level 2 of
SQL-89. The following is a non-exhaustive list of SQL-92 Entry Level
functionality beyond the basic SELECT , INSERT ,
UPDATE , and DELETE statements:
- Multiple tables in the from clause.
- Data types: characterType, decimalType, integerType, smallintType,
floatType, realType, doublePrecisionType, and numericType.
- Simple SQL expressions: and, or, not, like, =, <>, arithmetic
functions, joins, group bys, having, order by clauses, and aggregate
functions (such as sum, count, max, min.)
- Simple table and column descriptors: tableName, columnName.
Unique and Primary Key constraints in
table descriptors.
- Check constraints in column descriptors.
- Support for correlated subqueries and EXISTS subqueries.
- Full support for
Distinct in functions.
Union is supported. For more complete
information, including detail on the Intermediate and Full SQL-92 Levels,
a good source is: FIPS PUB 127-2: The
Standard for Database Language SQL.
The DatabaseMetaData methods
supportsANSI92EntryLevelSQL() ,
supportsANSI92IntermediateSQL() , and
supportsANSI92FullSQL() are provided to allow runtime
discovery of the SQL conformance level, and therefore the capabilities, of
a particular database and driver. A JDBC Compliant driver must return true
for supportsANSI92EntryLevelSQL() .
In addition, the level of ODBC defined SQL grammar support can be
determined by the DatabaseMetaData methods
supportsMinimumSQLGrammar() ,
supportsCoreSQLGrammar() , and
supportsExtendedSQLGrammar() . A JDBC Compliant driver must
return true for supportsMinimumSQLGrammar() . In case you are
wondering, ODBC is not a purely Microsoft standard. Tables showing the SQL
grammar levels can be found at AcuODBC
SQL Conformance. More ODBC information is available at ODBC
Version 3.51.
Depending on the application, you might return a message that certain
functionality is not supported or use a different algorithm to provide the
functionality based on the SQL Level or grammar type supported by the
driver and its underlying DBMS.
In JDBC 2.0, there are actually two packages. This course has covered
the core API and functionality. The second package, known as The JDBC
2.0 Optional Package (javax.sql), includes a DataSource
interface, Connection pooling, Distributed
Transactions, and Rowsets. The article The
JDBC 2.0 Optional Package by Maydene Fisher provides an overview
of the additional functionality.
JDBC and the JDBC 2.0 Optional Package are integral parts of the Java 2
Platform, Enterprise Edition (J2EE ), which comprises a broad array of
technologies for building enterprise-class server-side applications.
This section of the course briefly discusses some aspects of using JDBC
with JSPTM as mentioned in JavaServer
Pages Fundamentals.
This course focused on the core JDBC 2.0 API. However,
connection pooling is not only desirable, but a practical necessity with
JSP, and it makes sense to discuss some of the JDBC 2.0 Optional Package
features. The JDBC 2.0 Optional Package Binary can be obtained from the JDBC Download
Page. For more information about DataSource,
ConnectionPoolDataSource, and PooledConnection, you are
again referred to the first three sections of The
JDBC 2.0 Optional Package by Maydene Fisher.
As discussed in the article, the backbone code for a program using a
DataSource and a PooledConnection is: import javax.naming.*;
import javax.sql.*;
...
Context context = new InitialContext();
DataSource ds = (DataSource)ctx.lookup(
"jdbc/DataSource" );
Connection con = ds.getConnection(
"userID", "password" );
...
finally
{
if( con != null ) { con.close(); }
}
Remember that the javax.naming package is part of the Java
Naming and Directory InterfaceTM (JNDI) and
is included in JDK 1.3. Also, remember that a connection pool class
overrides the close() method and marks the
Connection as available. That's why it is important to ensure
that close() is invoked. Otherwise, the pool considers the
Connection to be in use and creates a new
Connection the next time one is requested, losing the benefit
of pooling.
The reason that the code above can be so straightforward is that the
DataSource , pooled or not, is expected to be set up by a
database administrator, with tools provided by the DBMS vendor.
Unfortunately, this expectation makes it almost impossible to create a
generally useful exercise, because each vendor can provide different
methods to accomplish this task. For a discussion on setting up a
DataSource using JNDI, see sections 3.7 through 3.7.3 of the
Advanced
Tutorial.
The JSP version of the above application code is very similar: <%@ page import="javax.naming.*, javax.sql.*" %>
...
<%
Context context = new InitialContext();
DataSource ds = (DataSource)ctx.lookup(
"jdbc/DataSource" );
Connection con = ds.getConnection(
"userID", "password" );
...
finally
{
if( con != null ) { con.close(); }
}
%>
In an application that displays information, it makes sense to create a
bean that handles queries, and possibly returns QueryRows or
similar objects on request that contain the columns for a row and/or
report the columns in a single String , as required. The bean
could either take the DataSource or a Connection
as an argument, depending on the preferred level of control and separation
of functionality.
Assuming paged displays, the relative approach taken in the exercise
for Scrollable
Result Sets can be used, or you could obtain a row count for the
retrieval and then use absolute row and page positioning. For more ideas
and a package devoted to JSP scrolling, see the Pager Tag Library.
Refer to the JavaServer
Pages Fundamentals short course and Resources
for further ideas and information about implementing JSP.
The Cloudscape database, as included in the JavaTM 2 SDK Enterprise Edition (J2SETM), is used for the majority of the JDBCTM 2.0 Fundamentals course. As of this writing,
the version included is Cloudscape 3.0.4. The first step is to obtain the
J2EETM SDK and documentation for your
platform, which is available from the Downloads &
Specifications section of the J2EE Home page. Be sure
to read and follow the Installation
Instructions.
If you prefer to review the documentation online, see J2EE Documentation. For
the Cloudscape online documentation, see Cloudscape
3.0.1 Documentation.
If your only purpose for downloading J2EE is to use Cloudscape for the
course, you do not need to make the changes to the userconfig script as
suggested in the Installation Instructions. You do need to
perform the following steps:
- Set the environment variable
J2EE_HOME . On NT, this is
the drive letter and parent directory for the J2EE SDK, for example:
set J2EE_HOME=D:\j2ee
- Set the environment variable
JAVA_HOME . On NT, this is
the drive letter and parent directory for J2SE, for example:
set JAVA_HOME=D:\j2se
Use your own locations and names for the J2EE and J2SE directories.
From this point on, J2EE_HOME and JAVA_HOME
refers to these directories.
- To avoid classpath issues, copy and rename the following jars from
J2EE_HOME/lib/cloudscape to
JAVA_HOME/jre/lib/ext :
cloudscape.jar
client.jar rename to
cs_client.jar
RmiJdbc.jar rename to
cs_RmiJdbc.jar
tools.jar rename to
cs_tools.jar
You do not have to rename the jars (and you can use different names if
you like) but doing so will avoid any conflicts due to the common names
used. Feel free to set up things differently, but the above steps provide
the fewest changes for proper client and server operation. Note that this
setup bypasses J2EE and simply allows use of Cloudscape. If you want to
use portions of J2EE in conjunction with exercises from the course, you
need to modify the name of the database used to jGuru OR edit the
exercise programs and resource bundles to use the default
CloudscapeDB database instead.
For more information on Cloudscape, see the downloaded documentation
under Cloudscape DBMS at J2EE_HOME/doc/cloudscape/index.html. Not
all of the documentation applies to the special version included in the
J2EE download. Because this course focuses on standards, there will not be
much more discussion in the way of database specifics, as setup is
different for each one. Refer to the documentation for your own database
and drivers for this type of information and starting/stopping the
database. However, a brief discussion about starting and stopping
Cloudscape is in order.
Make sure that Cloudscape has been installed and set up as discussed in
Cloudscape
Installation and Setup.
See J2EE_HOME/doc/guides/ejb/html/Tools4.html#11919 for information
about Cloudscape output on successful startup and shutdown.
This section serves as an SQL refresher to help you along with the
exercises. It is not meant to be a tell-all resource for SQL. It
takes you through the basic commands necessary for CRUD operations.
- C--Create
- R--Read
- U--Update
- D--Delete
Use the CREATE TABLE statement when you want to create a
table. Because creating tables is such an important operation, it requires
minimum conformance. However, some datasources, such as Text ODBC sources,
only support the simplest column elements, with little or no constraint
support. CREATE TABLE <table name>
(<column element> [, <column element>]...)
A column element is of the form: <column name> <data type>
[DEFAULT <expression>]
[<column constraint> [, <column constraint>]...]
A column constraint is of the form: NOT NULL |
UNIQUE |
PRIMARY KEY
Example: CREATE TABLE java (
version_name varchar (30),
major_version int,
minor_version int,
release_date date);
Use the DROP TABLE statement when you want to drop a
table. Like CREATE TABLE , it requires minimum
conformance. DROP TABLE <table name>
Use the SELECT statement when you want to retrieve a set
of columns. The set may be from one or more tables, and you can specify
the criteria to determine which rows to retrieve. Most of the clauses are
available with minimum conformance. Additional capabilities are available
with the core grammar. SELECT [ALL | DISTINCT] <select list>
FROM <table reference list>
WHERE <search condition list>
[ORDER BY <column designator> [ASC | DESC]
[, <column designator> [ASC | DESC]]...]
The select list usually contains a comma-separated list of
columns or an '*' to select all of them. SELECT version_name, release_date from java;
If your driver supports core compliance, you can also use the
GROUP BY , HAVING , and UNION clauses
of SELECT .
To perform a join operation so that you can get results from multiple
tables, the WHERE clause needs to provide the criteria. In
the event the same column name is used in multiple tables, you can preface
the column name with the table name, followed by a period. SELECT employee_id, employee_name,
department_table.department_id, department_name
FROM employee_table, department_table
WHERE employee_table.department_id =
department_table.department_id;
You can also specify aliases to use for the tables in the from clause
to avoid cumbersome names or if you are a poor typist: SELECT employee_id, employee_name,
d.department_id, department_name
FROM employee_table e, department_table d
WHERE e.department_id =
d.department_id;
Use the INSERT statement when you want to insert rows. It
too can provide different capabilities depending upon the conformance
level supported. INSERT INTO <table name>
[(<column name> [, <column name>]...)]
VALUES (<expression> [, <expression>]...)
INSERT INTO java VALUES
('2.0Beta', 2, 0, 'Aug-1-1997');
If the core grammar is supported, you can use a SELECT
clause to load multiple rows at a time.
Use the UPDATE statement when you want to update rows. It
requires the minimum grammar. UPDATE <table name>
SET <column name = {<expression> | NULL}
[, <column name = {<expression> | NULL}]...
WHERE <search condition>
Use the DELETE statement when you want to remove rows. It
requires the minimum grammar. DELETE FROM <table name>
WHERE <search condition>
The Java Technology site at Sun
Microsystems includes Products and
APIs.
- JDBC API Tutorial and Reference, Second Edition by Maydene
Fisher, Dr. Rick Cattell, Graham Hamilton, Seth White and Mark Hapner
(Addison Wesley ISBN 0201433281)
- Database Programming with JDBC and Java, Second Edition by
George Reese (O'Reilly & Associates ISBN 1565926161) [sample
chapter]
- SQL The Complete Reference by James R. Groff and Paul N.
Weinberg (McGraw-Hill ISBN 0072118458)
- Joe Celko's SQL for Smarties : Advanced SQL Programming, Second
Edition by Joe Celko (Morgan Kaufmann ISBN 1558605762)
Copyright 1996-2000 jGuru.com. All Rights
Reserved. |