Javadatabase
Javadatabase
Chapter
Writing Database Drivers
C HAPTER
10
Writing Database
Drivers
149
150 Java Database Programming with JDBC
select-list::= * | column-identifier
[, column-identifier
]...
table-name::= user-defined-name
::= letter[digit | letter]
user-defined-name
What all this grammar means is that the SimpleText driver supports a CRE-
ATE TABLE statement, a DROP TABLE statement, an INSERT statement
(with parameters), and a very simple SELECT statement (with a WHERE
clause). It may not seem like much, but this grammar is the foundation that
will allow us to create a table, insert some data, and select it back.
.SDFCOL1,#COL2,@COL3
Note that none of the SQL grammar is case-sensitive. The .SDF is the file
signature (this is how the SimpleText driver validates whether the text file
can be used), followed by a comma-separated list of column names. The
first character of the column name can specify the data type of the col-
umn. A column name starting with a # indicates a numeric column, while
a column name starting with an @ indicates a binary column. What’s that?
Binary data in a text file? Well, not quite. A binary column actually con-
tains an offset pointer into a sister file. This file, with an extension of .SBF
(Simple Binary File), contains any binary data for columns in the text file,
as well as the length of the data (maximum length of 1048576 bytes). Any
152 Java Database Programming with JDBC
.SDFCOL1,#COL2,@COL3
FOO,123,0
COL3 contains an offset of zero since this is the first row in the file. This is
the offset from within the TEST.SBF table in which the binary data resides.
Starting at the given offset, the first four bytes will be the length indicator,
followed by the actual binary data that was inserted. Note that any charac-
ter or binary data must be enclosed in single quotation marks.
We’ll be looking at plenty of code from the SimpleText driver throughout
this chapter. But first, let’s start by exploring what is provided by the JDBC
developer’s kit.
The DriverManager
The JDBC DriverManager is a static class that provides services to connect
to JDBC drivers. The DriverManager is provided by JavaSoft and does not
require the driver developer to perform any implementation. Its main
purpose is to assist in loading and initializing a requested JDBC driver.
Other than using the DriverManager to register a JDBC driver
(registerDriver) to make itself known and to provide the logging facility
(which is covered in detail later), a driver does not interface with the
DriverManager. In fact, once a JDBC driver is loaded, the DriverManager
drops out of the picture all together, and the application or applet inter-
faces with the driver directly.
//— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
// fooBar
// Demonstrates how to throw an SQLException
//— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
public void fooBar()
throws SQLException
{
throw new SQLException("I just threw a SQLException");
}
try {
fooBar();
}
catch (SQLException ex) {
ment, or ResultSet, which we’ll cover later). Because they are put on a list,
it is up to the application to poll for warnings after the completion of an
operation. Listing 10.1 shows a method that accepts an SQLWarning and
places it on a list.
// Find the end of the chain. When the current warning does
// not have a next pointer, it must be the end of the chain.
while (chain.getNextWarning() != null) {
chain = chain.getNextWarning();
}
Listing 10.2 uses this method to create two SQLWarnings and chain them
together.
Now we’ll call the method that puts two SQLWarnings on our warning
stack, then poll for the warning using the JDBC method getWarnings, as
shown in Listing 10.3.
// Now, poll for the warning chain. We'll simply dump any warning
// messages to standard output.
SQLWarning chain = getWarnings();
if (chain != null) {
System.out.println("Warning(s):");
// Now, poll for the warning chain. We'll simply dump any warning
// messages to standard output.
SQLWarning chain = getWarnings();
if (chain != null) {
System.out.println("Warning(s):");
Chapter 10: Writing Database Drivers 157
At a minimum, a JDBC driver must support one (if not all) of the charac-
ter data types (CHAR, VARCHAR, and LONGVARCHAR ). A driver may
also support driver-specific data types (OTHER) which can only be ac-
cessed in a JDBC application as an Object. In other words, you can get
data as some type of object and put it back into a database as that same
type of object, but the application has no idea what type of data is actually
contained within. Let’s take a look at each of the data types more closely.
Numeric
As mentioned before, the Numeric class was introduced with the JDBC API
to represent signed, exact numeric values with a fixed number of decimal
places. This class is ideal for representing monetary values, allowing accu-
rate arithmetic operations and comparisons. Another aspect is the ability to
change the rounding value. Rounding is performed if the value of the scale
(the number of fixed decimal places) plus one digit to the right of the deci-
mal point is greater than the rounding value. By default, the rounding value
is 4. For example, if the result of an arithmetic operation is 2.495, and the
scale is 2, the number is rounded to 2.50. Listing 10.6 provides an example
of changing the rounding value. Imagine that you are a devious retailer
investigating ways to maximize your profit by adjusting the rounding value.
class NumericRoundingValueTest {
System.out.println("discounted price="+newPrice.toString());
discounted price=004.17
discounted price with high rounding=004.18
Date
The Date class is used to represent dates in the ANSI SQL format YYYY-
MM-DD, where YYYY is a four-digit year, MM is a two-digit month, and DD
is a two-digit day. The JDBC Date class extends the existing java.util.Date
class (setting the hour, minutes, and seconds to zero) and, most impor-
tantly, adds two methods to convert Strings into dates, and vice-versa:
The Date class also serves very well in validating date values. If an invalid
date string is passed to the valueOf method, a java.lang.IllegalArgument-
Exception is thrown:
String s;
It is worth mentioning again that the Java date epoch is January 1, 1970;
therefore, you cannot represent any date values prior to January 1, 1970,
with a Date object.
Time
The Time class is used to represent times in the ANSI SQL format
HH:MM:SS, where HH is a two-digit hour, MM is a two-digit minute, and
SS is a two-digit second. The JDBC Time class extends the existing
java.util.Date class (setting the year, month, and day to zero) and, most
importantly, adds two methods to convert Strings into times, and vice-versa:
The Time class also serves very well in validating time values. If an invalid
time string is passed to the valueOf method, a java.lang.IllegalArgument-
Exception is thrown:
String s;
Timestamp
The Timestamp class is used to represent a combination of date and time
values in the ANSI SQL format YYYY-MM-DD HH:MM:SS.F..., where YYYY
is a four-digit year, MM is a two-digit month, DD is a two-digit day, HH is a
two-digit hour, MM is a two-digit minute, SS is a two-digit second, and F is
an optional fractional second up to nine digits in length. The JDBC
Timestamp class extends the existing java.util.Date class (adding the frac-
tion seconds) and, most importantly, adds two methods to convert Strings
into timestamps, and vice-versa:
System.out.println("Timestamp=" + t.toString());
The Timestamp class also serves very well in validating timestamp values.
If an invalid time string is passed to the valueOf method, a java.lang.Illegal-
ArgumentException is thrown:
String s;
As is the case with the Date class, the Java date epoch is January 1, 1970;
therefore, you cannot represent any date values prior to January 1, 1970,
with a Timestamp object.
obvious drawback is that the JDBC driver is not portable and cannot be
automatically downloaded by today’s browsers.
If a native bridge is required for your JDBC driver, you should keep a few
things in mind. First, do as little as possible in the C bridge code; you will want
to keep the bridge as small as possible, ideally creating just a Java wrapper
around the C API. Most importantly, avoid the temptation of performing
memory management in C (i.e. malloc). This is best left in Java code, since
the Java Virtual Machine so nicely takes care of garbage collection. Secondly,
keep all of the native method declarations in one Java class. By doing so, all of
the bridge routines will be localized and much easier to maintain. Finally,
don’t make any assumptions about data representation. An integer value may
be 2 bytes on one system, and 4 bytes on another. If you are planning to port
the native bridge code to a different system (which is highly likely), you should
provide native methods that provide the size and interpretation of data.
Listing 10.7 illustrates these suggestions. This module contains all of the
native method declarations, as well as the code to load our library. The
library will be loaded when the class is instantiated.
import java.sql.*;
//— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
// Native method declarations
//— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
Once this module has been compiled (javac), a Java generated header file
and C file must be created:
javah jdbc.test.MyBridge
javah -stubs jdbc.test.MyBridge
These files provide the mechanism for the Java and C worlds to communi-
cate with each other. Listing 10.8 shows the generated header file
(jdbc_test_MyBridge.h, in this case), which will be included in our C bridge
code.
#ifndef _Included_jdbc_test_MyBridge
Chapter 10: Writing Database Drivers 167
#define _Included_jdbc_test_MyBridge
#ifdef __cplusplus
extern "C" {
#endif
__declspec(dllexport) long jdbc_test_MyBridge_getINTSize(struct
Hjdbc_test_MyBridge *);
__declspec(dllexport) long jdbc_test_MyBridge_getINTValue(struct
Hjdbc_test_MyBridge *,HArrayOfByte *);
struct Hjava_lang_String;
__declspec(dllexport) void jdbc_test_MyBridge_callSomeFunction(struct
Hjdbc_test_MyBridge *,struct Hjava_lang_String *,HArrayOfByte *);
#ifdef __cplusplus
}
#endif
#endif
The generated C file (shown in Listing 10.9) must be compiled and linked
with the bridge.
_P_[0].i = jdbc_test_MyBridge_getINTValue(_P_[0].p,((_P_[1].p)));
return _P_ + 1;
}
/* SYMBOL: "jdbc/test/MyBridge/callSomeFunction(Ljava/lang/String;[B)V",
Java_jdbc_test_MyBridge_callSomeFunction_stub */
__declspec(dllexport) stack_item
*Java_jdbc_test_MyBridge_callSomeFunction_stub(stack_item *_P_,struct
execenv *_EE_) {
extern void jdbc_test_MyBridge_callSomeFunction(void *,void *,void
*);
(void) jdbc_test_MyBridge_callSomeFunction(_P_[0].p,((_P_[1].p)),
((_P_[2].p)));return _P_;
}
The bridge code is shown in Listing 10.10. The function prototypes were
taken from the generated header file.
//— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
// getINTSize
// Return the size of an int
//— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
long jdbc_test_MyBridge_getINTSize(
struct Hjdbc_test_MyBridge *caller)
{
return sizeof(int);
}
Chapter 10: Writing Database Drivers 169
//— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
// getINTValue
// Given a buffer, return the value as an int
//— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
long jdbc_test_MyBridge_getINTValue(
struct Hjdbc_test_MyBridge *caller,
HArrayOfByte *buf)
{
// Cast our array of bytes to an integer pointer
int* pInt = (int*) unhand (buf)->body;
//— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
// callSomeFunction
// Call some function that takes a String and an int pointer as arguments
//— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
void jdbc_test_MyBridge_callSomeFunction(
struct Hjdbc_test_MyBridge *caller,
struct Hjava_lang_String *stringValue,
HArrayOfByte *buf)
{
// Cast the string into a char pointer
char* pString = (char*) makeCString (stringValue);
// This fictitious function will print the string, then return the
// length of the string in the int pointer.
printf("String value=%s\n", pString);
*pInt = strlen(pString);
}
class Test {
try {
Implementing Interfaces
The JDBC API specification provides a series of interfacesthat must be imple-
mented by the JDBC driver developer. An interface declaration creates a
new reference type consisting of constants and abstract methods. An inter-
face cannot contain any implementations (that is, executable code). What
does all of this mean? The JDBC API specification dictates the methods
and method interfaces for the API, and a driver must fully implement these
interfaces. A JDBC application makes method calls to the JDBC interface,
not a specific driver. Because all JDBC drivers must implement the same
interface, they are interchangeable.
There are a few rules that you must follow when implementing interfaces.
First, you must implement the interface exactly as specified. This includes
the name, return value, parameters, and throws clause. Secondly, you must
be sure to implement all interfaces as public methods. Remember, this is
the interface that other classes will see; if it isn’t public, it can’t be seen.
Finally, all methods in the interface must be implemented. If you forget,
the Java compiler will kindly remind you.
Take a look at Listing 10.12 for an example of how interfaces are used.
The code defines an interface, implements the interface, and then uses
the interface.
//— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
// Define 3 methods in this interface
//—— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
void method1();
int method2(int x);
String method3(String y);
}
//— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
// MyImplementation.java
//
// Sample code to demonstrate the use of interfaces
//— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
package jdbc.test;
//— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
// Note that you are free to add methods and attributes to this
// new class that were not in the interface, but they cannot be
// seen from the interface.
//— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
protected int addOne(int x)
{
return x + 1;
}
Chapter 10: Writing Database Drivers 173
}
//— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
// TestInterface.java
//
// Sample code to demonstrate the use of interfaces
//— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
import jdbc.test.*;
class TestInterface {
}
}
As you can see, implementing interfaces is easy. We’ll go into more detail
with the major JDBC interfaces later in this chapter. But first, we need to
cover some basic foundations that should be a part of every good JDBC
driver.
Tracing
One detail that is often overlooked by software developers is providing a facil-
ity to enable debugging. The JDBC API does provide methods to enable and
disable tracing, but it is ultimately up to the driver developer to provide trac-
ing information in the driver. It becomes even more critical to provide a de-
tailed level of tracing when you consider the possible wide-spread distribution
of your driver. People from all over the world may be using your software,
and they will expect a certain level of support if problems arise. For this
reason, I consider it a must to trace all of the JDBC API method calls (so that
a problem can be re-created using the output from a trace).
174 Java Database Programming with JDBC
Turning On Tracing
The DriverManager provides a method to set the tracing PrintStream to
be used for all of the drivers; not only those that are currently active, but
any drivers that are subsequently loaded. Note that if two applications are
using JDBC, and both have turned tracing on, the PrintStream that is set
last will be shared by both applications. The following code snippet shows
how to turn tracing on, sending any trace messages to a local file:
try {
// Create a new OuputStream using a file. This may fail if the
// calling application/applet does not have the proper security
// to write to a local disk.
java.io.OutputStream outFile = new
java.io.FileOutputStream("jdbc.out");
Using this code, a new file named jdbc.out will be created (if an existing file
already exists, it will be overwritten), and any tracing information will be
saved in the file.
DriverManager.println("Trace=" + a + b + c);
//— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
// traceOn
// Returns true if tracing (logging) is currently enabled
//— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
176 Java Database Programming with JDBC
From an application, you can use this method to check if tracing has been
previously enabled before blindly setting it:
// Before setting tracing on, check to make sure that tracing is not
// already turned on. If it is, notify the application.
if (traceOn()) {
// Issue a warning that tracing is already enabled
.
.
.
}
From the driver, I use this method to check for tracing before attempting to
send information to the PrintStream. In the example where we traced the
message text of “Trace=The quick brown fox jumped over the lazy dog,” a lot
had to happen before the message was sent to the DriverManager.println
method. All of the given String objects had to be concatenated, and a new
String had to be constructed. That’s a lot of overhead to go through before
even making the println call, especially if tracing is not enabled (which will
probably be the majority of the time). So, for performance reasons, I prefer to
ensure that tracing has been enabled before assembling my trace message:
Data Coercion
At the heart of every JDBC driver is data. That is the whole purpose of the
driver: providing data. Not only providing it, but providing it in a requested
Chapter 10: Writing Database Drivers 177
format. This is what data coercion is all about—converting data from one
format to another. As Figure 10.1 shows, JDBC specifies the necessary con-
versions.
In order to provide reliable data coercion, a data wrapper class should be
used. This class contains a data value in some known format and provides
methods to convert it to a specific type. As an example, I have included the
CommonValue class from the SimpleText driver in Listing 10.13. This class
has several overloaded constructors that accept different types of data val-
ues. The data value is stored within the class, along with the type of data
(String, Integer, etc.). A series of methods are then provided to get the data
in different formats. This class greatly reduces the burden of the JDBC driver
developer, and can serve as a fundamental class for any number of drivers.
import java.sql.*;
public CommonValue(String s)
{
data = (Object) s;
internalType = Types.VARCHAR;
}
public CommonValue(int i)
{
data = (Object) new Integer(i);
internalType = Types.INTEGER;
}
public CommonValue(Integer i)
{
data = (Object) i;
internalType = Types.INTEGER;
}
//— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
// isNull
// returns true if the value is null
//— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
public boolean isNull()
{
return (data == null);
}
//— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
// getMethods
//— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
switch(internalType) {
case Types.VARCHAR:
s = (String) data;
break;
case Types.INTEGER:
s = ((Integer) data).toString();
break;
case Types.VARBINARY:
{
// Convert a byte array into a String of hex digits
byte b[] = (byte[]) data;
int len = b.length;
String digits = "0123456789ABCDEF";
char c[] = new char[len * 2];
default:
throw new SQLException("Unable to convert data type to
String: " +
internalType);
}
return s;
}
switch(internalType) {
case Types.VARCHAR:
i = (Integer.valueOf((String) data)).intValue();
break;
case Types.INTEGER:
i = ((Integer) data).intValue();
break;
default:
throw new SQLException("Unable to convert data type to
String: " +
internalType);
}
return i;
}
Chapter 10: Writing Database Drivers 181
switch(internalType) {
case Types.VARCHAR:
{
// Convert the String into a byte array. The String must
// contain an even number of hex digits.
String s = ((String) data).toUpperCase();
String digits = "0123456789ABCDEF";
int len = s.length();
int index;
if ((len % 2) != 0) {
throw new SQLException(
"Data must have an even number of hex
digits");
}
if (index < 0) {
throw new SQLException("Invalid hex digit");
}
if (index < 0) {
throw new SQLException("Invalid hex digit");
}
b[i] += (byte) index;
}
182 Java Database Programming with JDBC
}
break;
case Types.VARBINARY:
b = (byte[]) data;
break;
default:
throw new SQLException("Unable to convert data type to
byte[]: " +
internalType);
}
return b;
}
Note that the SimpleText driver supports only character, integer, and bi-
nary data; thus, CommonValue only accepts these data types, and only at-
tempts to convert data to these same types. A more robust driver would
need to further implement this class to include more (if not all) data types.
Escape Clauses
Another consideration when implementing a JDBC driver is process-
ing escape clauses. Escape clauses are used as extensions to SQL and
provide a method to perform DBMS-specific extensions, which are
interoperable among DBMSes. The JDBC driver must accept escape
clauses and expand them into the native DBMS format before process-
ing the SQL statement. While this sounds simple enough on the sur-
face, this process may turn out to be an enormous task. If you are
developing a driver that uses an existing DBMS, and the JDBC driver
simply passes SQL statements to the DBMS, you may have to develop a
parser to scan for escape clauses.
The following types of SQL extensions are defined:
• Date, time, and timestamp data
• Scalar functions such as numeric, string, and data type conversion
Chapter 10: Writing Database Drivers 183
{escape}
We’ll cover the specific syntax for each type of escape clause in the follow-
ing sections.
{d 'value'}
{t 'value'}
{ts 'value'}
Scalar Functions
The five types of scalar functions—string, numeric, time and date, system,
and data type conversion—all use the syntax:
{fn scalar-function}
184 Java Database Programming with JDBC
{escape 'escape-character'}
The following SQL statement uses the LIKE predicate escape clause to
search for any columns that start with the “%” character:
Outer Joins
JDBC supports the ANSI SQL-92 LEFT OUTER JOIN syntax. The escape
clause syntax is
{oj outer-join}
Procedures
A JDBC application can call a procedure in place of an SQL statement.
The escape clause used for calling a procedure is
where procedure-name
specifies the name of a procedure stored on the data
source, and paramspecifies procedure parameters. A procedure can have
zero or more parameters, and may return a value.
186 Java Database Programming with JDBC
Driver
The Driver class is the entry point for all JDBC drivers. From here, a con-
nection to the database can be made in order to perform work. This class
is intentionally very small; the intent is that JDBC drivers can be pre-regis-
tered with the system, enabling the DriverManager to select an appropri-
ate driver given only a URL (Universal Resource Locator). The only way to
determine which driver can service the given URL is to load the Driver
class and let each driver respond via the acceptsURL method. To keep the
amount of time required to find an appropriate driver to a minimum,
each Driver class should be as small as possible so it can be loaded quickly.
REGISTER THYSELF
The very first thing that a driver should do is register itself with the
DriverManager. The reason is simple: You need to tell the DriverManager
that you exist; otherwise you may not be loaded. The following code illus-
trates one way of loading a JDBC driver:
java.sql.Driver d = (java.sql.Driver)
Class.forName ("jdbc.SimpleText.SimpleTextDriver").newInstance();
The class loader will create a new instance of the SimpleText JDBC driver.
The application then asks the DriverManager to create a connection using
the given URL. If the SimpleText driver does not register itself, the
DriverManager will not attempt to load it, which will result in a nasty “No
capable driver” error.
The best place to register a driver is in the Driver constructor:
public SimpleTextDriver()
throws SQLException
{
// Attempt to register this driver with the JDBC DriverManager.
// If it fails, an exception will be thrown.
DriverManager.registerDriver(this);
}
URL PROCESSING
As I mentioned a moment ago, the acceptsURL method informs the
DriverManager whether a given URL is supported by the driver. The gen-
eral format for a JDBC URL is
jdbc:subprotocol:subname
and the subnameis defined by the JDBC driver. For example, the format
for the JDBC-ODBC Bridge URL is:
jdbc:odbc:foobar
the only driver that will respond that the URL is supported is the JDBC-
ODBC Bridge; all others will ignore the request.
Listing 10.14 shows the acceptsURL method for the SimpleText driver.
The SimpleText driver will accept the following URL syntax:
jdbc:SimpleText
boolean rc = false;
Chapter 10: Writing Database Drivers 189
// Get the subname from the url. If the url is not valid for
// this driver, a null will be returned.
if (getSubname(url) != null) {
rc = true;
}
if (traceOn()) {
trace(" " + rc);
}
return rc;
}
//— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
// getSubname
// Given a URL, return the subname. Returns null if the protocol is
// not "jdbc" or the subprotocol is not "simpletext."
//—— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
public String getSubname(
String url)
{
String subname = null;
String protocol = "JDBC";
String subProtocol = "SIMPLETEXT";
DRIVER PROPERTIES
Connecting to a JDBC driver with only a URL specification is great, but
the vast majority of the time, a driver will require additional information
in order to properly connect to a database. The JDBC specification has
addressed this issue with the getPropertyInfo method. Once a Driver has
been instantiated, an application can use this method to find out what
required and optional properties can be used to connect to the database.
You may be tempted to require the application to embed properties within
the URL subname, but by returning them from the getPropertyInfo
method, you can identify the properties at runtime, giving a much more
robust solution. Listing 10.15 shows an application that loads the SimpleText
driver and gets the property information.
class PropertyTest {
}
catch (SQLException ex) {
System.out.println ("\nSQLException(s) caught\n");
Number of properties: 1
Property 1
Name: Directory
Description: Initial text file directory
Required: false
Value: null
Choices: null
}
else {
// Create an empty list
prop = new DriverPropertyInfo[0];
}
return prop;
}
return con;
}
Chapter 10: Writing Database Drivers 195
As you can see, there isn’t a lot going on here for the SimpleText driver;
remember that we need to keep the size of the Driver class implementation
as small as possible. To aid in this, all of the code required to perform the
database connection resides in the Connection class, which we’ll discuss next.
Connection
The Connection class represents a session with the data source. From here,
you can create Statement objects to execute SQL statements and gather
database statistics. Depending upon the database that you are using, mul-
tiple connections may be allowed for each driver.
For the SimpleText driver, we don’t need to do anything more than actu-
ally connect to the database. In fact, there really isn’t a database at all—
just a bunch of text files. For typical database drivers, some type of
connection context will be established, and default information will be set
and gathered. During the SimpleText connection initialization, all that we
need to do is check for a read-only condition (which can only occur within
untrusted applets) and any properties that are supplied by the applica-
tion, as shown in Listing 10.18.
if (securityManager != null) {
try {
// Use some arbitrary file to check for file write privileges
securityManager.checkWrite ("SimpleText_Foo");
196 Java Database Programming with JDBC
if (s == null) {
s = System.getProperty("user.dir");
}
setCatalog(s);
}
CREATING STATEMENTS
From the Connection object, an application can create three types of State-
ment objects. The base Statement object is used for executing SQL state-
ments directly. The PreparedStatement object (which extends Statement)
is used for pre-compiling SQL statements that may contain input param-
eters. The CallableStatement object (which extends PreparedStatement)
is used to execute stored procedures that may contain both input and out-
put parameters.
For the SimpleText driver, the createStatement method does nothing more
than create a new Statement object. For most database systems, some type
of statement context, or handle, will be created. One thing to note when-
ever an object is created in a JDBC driver: Save a reference to the owning
object because you will need to obtain information (such as the connec-
tion context from within a Statement object) from the owning object.
Chapter 10: Writing Database Drivers 197
return stmt;
}
Which module will you compile first? You can’t compile the Connection
class until the Statement class has been compiled, and you can’t compile
the Statement class until the Connection class has been compiled. This is a
circular dependency. Of course, the Java compiler does allow multiple files
to be compiled at once, but some build environments do not support cir-
cular dependency. I have solved this problem in the SimpleText driver by
defining some simple interface classes. In this way, the Statement class knows
only about the general interface of the Connection class; the implementa-
tion of the interface does not need to be present. Our modified initialize
method looks like this:
{
// Save the owning connection object
ownerConnection = con;
}
Note that our interface class extends the JDBC class, and our Connection
class implements this new interface. This allows us to compile the inter-
face first, then the Statement, followed by the Connection. Say good-bye to
your circular dependency woes.
Now, back to the Statement objects. The prepareStatement and
prepareCall methods of the Connection object both require an SQL state-
ment to be provided. This SQL statement should be pre-compiled and
stored with the Statement object. If any errors are present in the SQL
statement, an exception should be raised, and the Statement object should
not be created.
DatabaseMetaData
At over 130 methods, the DatabaseMetaData class is by far the largest. It
supplies information about what is supported and how things are supported.
It also supplies catalog information such as listing tables, columns, indexes,
procedures, and so on. Because the JDBC API specification does an ad-
equate job of explaining the methods contained in this class, and most of
them are quite straightforward, we’ll just take a look at how the SimpleText
driver implements the getTables catalog method. But first, let’s review the
basic steps needed to implement each of the catalog methods (that is,
those methods that return a ResultSet):
1. Create the result columns, which includes the column name, type, and
other information about each of the columns. You should perform
this step regardless of whether the database supports a given catalog
function (such as stored procedures). I believe that it is much better to
return an empty result set with only the column information than to
raise an exception indicating that the database does not support the
function. The JDBC specification does not currently address this issue,
so it is open for interpretation.
2. Retrieve the catalog information from the database.
3. Perform any filtering necessary. The application may have specified
the return of only a subset of the catalog information. You may need to
filter the information in the JDBC driver if the database system doesn’t.
4. Sort the result data per the JDBC API specification. If you are lucky,
the database you are using will sort the data in the proper sequence.
Most likely, it will not. In this case, you will need to ensure that the data
is returned in the proper order.
5. Return a ResultSet containing the requested information.
The SimpleText getTables method will return a list of all of the text files
in the catalog (directory) given. If no catalog is supplied, the default
directory is used. Note that the SimpleText driver does not perform all
of the steps shown previously; it does not provide any filtering, nor does
it sort the data in the proper sequence. You are more than welcome to
200 Java Database Programming with JDBC
add this functionality. In fact, I encourage it. One note about column in-
formation: I prefer to use a Hashtable containing the column number as
the key, and a class containing all of the information about the column as
the data value. So, for all ResultSets that are generated, I create a Hashtable
of column information that is then used by the ResultSet object and the
ResultSetMetaData object to describe each column. Listing 10.19 shows
the SimpleTextColumn class that is used to hold this information for each
column.
public SimpleTextColumn(
String name,
int type)
{
this.name = name;
this.type = type;
this.precision = 0;
}
public SimpleTextColumn(
String name)
{
this.name = name;
this.type = 0;
Chapter 10: Writing Database Drivers 201
this.precision = 0;
}
Note that I have used several constructors to set up various default infor-
mation, and that all of the attributes are public. To follow object-oriented
design, I should have provided a get and set method to encapsulate each
attribute, but I chose to let each consumer of this object access them di-
rectly. Listing 10.20 shows the code for the getTables method.
if (types != null) {
willBeEmpty = true;
for (int ii = 0; ii < types.length; ii++) {
if (types[ii].equalsIgnoreCase("TABLE")) {
willBeEmpty = false;
Chapter 10: Writing Database Drivers 203
break;
}
}
}
if (!willBeEmpty) {
Hashtable singleRow;
SimpleTextTable table;
return rs;
}
Let’s take a closer look at what’s going on here. The first thing we do is
create a Statement object to “fake out” the ResultSet object that we will be
creating to return back to the application. The ResultSet object is depen-
dent upon a Statement object, so we’ll give it one. The next thing we do is
204 Java Database Programming with JDBC
create all of the column information. Note that all of the required col-
umns are given in the JDBC API specification. The add method simply
adds a SimpleTextColumn object to the Hashtable of columns:
Next, we create another Hashtable to hold all of the data for all of the
catalog rows. The Hashtable contains an entry for each row of data. The
entry contains the key, which is the row number, and the data value, which
is yet another Hashtable whose key is the column number and whose data
value is a CommonValue object containing the actual data. Remember that
the CommonValue class provides us with the mechanism to store data and
coerce it as requested by the application. If a column is null, we simply
cannot store any information in the Hashtable for that column number.
After some sanity checking to ensure that we really need to look for the cata-
log information, we get a list of all of the tables. The getTables method in the
Connection class provides us with a list of all of the SimpleText data files:
if (file.isDirectory()) {
Chapter 10: Writing Database Drivers 205
// List all of the files in the directory with the .SDF extension
String entries[] = file.list(filter);
SimpleTextTable tableEntry;
return list;
}
Again, I use a Hashtable for each table (or file in our case) that is found.
By now, you will have realized that I really like using Hashtables; they can
grow in size dynamically and provide quick access to data. And because a
Hashtable stores data as an abstract Object, I can store whatever is neces-
sar y. In this case, each Hashtable entr y for a table contains a
SimpleTextTable object:
name = file;
}
}
Notice that the constructor strips the file extension from the given file
name, creating the table name.
Now, back to the getTables method for DatabaseMetaData. Once a list of
all of the tables has been retrieved, the Hashtable used for storing all of
the rows is generated. If you were to add additional filtering, this is the
place that it should be done. Finally, a new ResultSet object is created and
initialized. One of the constructors for the ResultSet class accepts two
Hashtables: one for the column information (SimpleTextColumn objects),
and the other for row data (CommonValue objects). We’ll see later how
these are handled by the ResultSet class. For now, just note that it can
handle both in-memory results (in the form of a Hashtable) and results
read directly from the data file.
Statement
The Statement class contains methods to execute SQL statements directly
against the database and to obtain the results. A Statement object is cre-
ated using the createStatement method from the Connection object. Of
note in Listing 10.21 are the three methods used to execute SQL state-
ments: executeUpdate, executeQuery, and execute. In actuality, you only
need to worry about implementing the execute method; the other meth-
ods use it to perform their work. In fact, the code provided in the
SimpleText driver should be identical for all JDBC drivers.
//
// Returns the table of data produced by the SQL statement.
//— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
public ResultSet executeQuery(
String sql)
throws SQLException
{
if (traceOn()) {
trace("@executeQuery(" + sql + ")");
}
java.sql.ResultSet rs = null;
//— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
// executeUpdate - JDBC API
// Execute an SQL INSERT, UPDATE, or DELETE statement. In addition,
// SQL statements that return nothing, such as SQL DDL statements,
// can be executed.
//
// sql an SQL INSERT, UPDATE, or DELETE statement, or an SQL
// statement that returns nothing.
//
// Returns either the row count for INSERT, UPDATE, or DELETE; or 0
// for SQL statements that return nothing.
//— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
public int executeUpdate(
String sql)
throws SQLException
{
if (traceOn()) {
trace("@executeUpdate(" + sql + ")");
}
208 Java Database Programming with JDBC
return count;
}
As you can see, executeQuery and executeUpdate are simply helper meth-
ods for an application; they are built completely upon other methods con-
tained within the class. The execute method accepts an SQL statement as
its only parameter, and will be implemented differently, depending upon
the underlying database system. For the SimpleText driver, the SQL state-
ment will be parsed, prepared, and executed. Note that parameter mark-
ers are not allowed when executing an SQL statement directly. If the SQL
statement created results containing columnar data, execute will return
true; if the statement created a count of rows affected, execute will return
false. If execute returns true, the application then uses getResultSet to
return the current result information; otherwise, getUpdateCount will
return the number of rows affected.
WARNINGS
As opposed to SQLException, which indicates a critical error, an
SQLWarning can be issued to provide additional information to the ap-
plication. Even though SQLWarning is derived from SQLException, warn-
ings are not thrown. Instead, if a warning is issued, it is placed on a warning
stack with the Statement object (the same holds true for the Connection
and ResultSet objects). The application must then check for warnings
after every operation using the getWarnings method. At first, this may
seem a bit cumbersome, but when you consider the alternative of wrap-
ping try...catch statements around each operation, this seems like a
Chapter 10: Writing Database Drivers 209
better solution. Note also that warnings can be chained together, just like
SQLExceptions (for more information on chaining, see the JDBC Excep-
tion Typessection earlier in this chapter).
PreparedStatement
The PreparedStatement is used for pre-compiling an SQL statement, typi-
cally in conjunction with parameters, and can be efficiently executed mul-
tiple times with just a change in a parameter value; the SQL statement does
not have to be parsed and compiled each time. Because the
PreparedStatement class extends the Statement class, you will have already
implemented a majority of the methods. The executeQuery, executeUpdate,
and execute methods are very similar to the Statement methods of the same
name, but they do not take an SQL statement as a parameter. The SQL
statement for the PreparedStatement was provided when the object was cre-
ated with the prepareStatement method from the Connection object. One
danger to note here: Because PreparedStatement is derived from the State-
210 Java Database Programming with JDBC
The verify method validates that the given parameter index is valid for the
current prepared statement, and also clears any previously bound value
for that parameter index:
Because the CommonValue class does not yet support all of the JDBC data
types, not all of the set methods have been implemented in the SimpleText
212 Java Database Programming with JDBC
driver. You can see, however, how easy it would be to fully implement these
methods once CommonValue supported all of the necessary data coercion.
WHAT IS IT?
Another way to set parameter values is by using the setObject method.
This method can easily be built upon the other set methods. Of interest
here is the ability to set an Object without giving the JDBC driver the type
of driver being set. The SimpleText driver implements a simple method to
determine the type of object, given only the object itself:
try {
if ((Integer) x != null) {
return Types.INTEGER;
}
}
catch (Exception ex) {
}
try {
if ((byte[]) x != null) {
return Types.VARBINARY;
}
}
catch (Exception ex) {
}
SETTING INPUTSTREAMS
As we’ll see with ResultSet later, using InputStreams is the recommended
way to work with long data (blobs). There are two ways to treat InputStreams
when using them as input parameters: Read the entire InputStream when
the parameter is set and treat it as a large data object, or defer the read
until the statement is executed and read it in chunks at a time. The latter
approach is the preferred method because the contents of an InputStream
may be too large to fit into memory. Here’s what the SimpleText driver
does with InputStreams:
try {
x.read(b);
}
catch (Exception ex) {
throw new SQLException("Unable to read InputStream: " +
ex.getMessage());
}
But wait, this isn’t the preferred way! You are correct, it isn’t. The
SimpleText driver simply reads in the entire InputStream and then sets
the parameter as a byte array. I’ll leave it up to you to modify the driver to
defer the read until execute time.
214 Java Database Programming with JDBC
ResultSet
The ResultSet class provides methods to access data generated by a table
query. This includes a series of get methods which retrieve data in any one
of the JDBC SQL type formats, either by column number or by column
name. When the issue of providing get methods was first introduced by
JavaSoft, some disgruntled programmers argued that they were not neces-
sary; if an application wanted to get data in this manner, then the applica-
tion could provide a routine to cross reference the column name to a column
number. Unfortunately (in my opinion), JavaSoft chose to keep these meth-
ods in the API and provide the implementation of the cross reference method
in an appendix. Because it is part of the API, all drivers must implement the
methods. Implementing the methods is not all that difficult, but it is tedious
and adds overhead to the driver. The driver simply takes the column name
that is given, gets the corresponding column number for the column name,
and invokes the same get method using the column number:
if (x != null) {
return (x.intValue());
}
Chapter 10: Writing Database Drivers 215
This method uses a Hashtable to cache the column number and column names.
String s = null;
if (inMemoryRows != null) {
s = (getColumn(rowNum, columnIndex)).getString();
}
else {
CommonValue value = getValue(colNo);
if (value != null) {
s = value.getString();
}
}
216 Java Database Programming with JDBC
if (s == null) {
lastNull = true;
}
return s;
}
The method starts out by verifying that the given column number is valid.
If it is not, an exception is thrown. Some other types of initialization are
also performed. Remember that all ResultSet objects are provided with a
Hashtable of SimpleTextColumn objects describing each column:
if (col == null) {
throw new SQLException("Invalid column number: " + column);
}
return col.colNo;
}
Next, if the row data is stored in an in-memory Hashtable (as with the
DatabaseMetaData catalog methods), the data is retrieved from the
Hashtable. Otherwise, the driver gets the data from the data file. In both
instances, the data is retrieved as a CommonValue object, and the getString
method is used to format the data into the requested data type. Null values
are handled specially; the JDBC API has a wasNull method that will return
true if the last column that was retrieved was null:
ResultSetMetaData
The ResultSetMetaData class provides methods that describe each one of
the columns in a result set. This includes the column count, column at-
tributes, and the column name. ResultSetMetaData will typically be the
smallest class in a JDBC driver, and is usually very straightforward to imple-
ment. For the SimpleText driver, all of the necessary information is re-
trieved from the Hashtable of column information that is required for all
result sets. Thus, to retrieve the column name:
if (column == null) {
throw new SQLException("Invalid column number: " + col);
}
return column;
}
218 Java Database Programming with JDBC
Summary
We have covered a lot of material in this chapter, including the JDBC
DriverManager and the services that it provides, implementing Java inter-
faces, creating native JDBC drivers, tracing, data coercion, escape sequence
processing, and each one of the major JDBC interfaces. This information,
in conjunction with the SimpleText driver, should help you to create your
own JDBC driver without too much difficulty.