Connecting to an ODBC database from Java can be a real nightmare. Most solutions suggest using a JDBC-ODBC bridge. So what are real possibilities here? Native JDBC-ODBC bridge by Sun/Oracle sun.jdbc.odbc.JdbcOdbcDriver. Well, there are some limitations like the number of concurrent connections and poor performance. I was not able to find any free (ideally open source) JDBC-ODBC bridge. There are multiple vendors but only one seems to have a de facto standard - Easysoft. They provide trial version of their bridge so you can try the solution before you buy it. Here we realized some troubles in Java Virtual Machine consuming too much memory. I do not want to blame the bridge. It was on a separate machine but we are not able to debug it and see how it works. So not being opensource killed Easysoft's chance to get a new customer.
After all I decided to write a small Java client that would be able to access an ODBC database. We needed just some basic querying capabilities anyway. But this is a little tricky, there is no ODBC communication protocol specification. Eventually, I decided to use unixODBC library using JNI. And here is beginning of the story.
First I designed a client for accessing the database. I had some previous ODBC API knowledge so I designed the client in order to write minimum code in C.
A design note: I failed passing database handles (type void*) between C and Java, so I created an array to store real handles in the C library and pass just an index to the array. This requires tha Java part to remember which handles have been used. There definitely exist a more sophisticated solution but I wanted to query an ODBC database at first. Also note that all data are returned as Strings for simplicity. There is also a shor main() method to test the client.
package org.marvec.odbc;
public class OdbcClient {
private static SortedSet usedHandles = new TreeSet();
private int handle;
public native void connect(String connection) throws IOException;
public native void execute(String statement) throws IOException;
public native int getNumCols();
public native ColumnMetadata getColMetadata(int col);
public native boolean fetch();
public native String getColData(int col) throws IOException;
public native void freeStatement();
private native void close();
public OdbcClient() throws IOException {
int i = 0;
while (usedHandles.contains(i) && i <= 1024) {
i++;
}
if (i == 1024) {
throw new IOException("All handles are currently in use. Try to free some handles by disconnecting from ODBC.");
}
usedHandles.add(i);
handle = i;
}
public void disconnect() {
close();
usedHandles.remove(handle);
}
public static class ColumnMetadata {
public String name;
public int type;
public long length;
public int digits;
this.nullable = nullable;
public String toString() {
return name + ": " + type + "(" + length + ")" + "[" + digits + "]" + (nullable ? "*" : "");
}
}
public static void main(String... args) throws Exception {
System.loadLibrary(args[0]);
OdbcClient c = new OdbcClient();
c.connect("DRIVER={PostgreSQL64};DATABASE=myodbcdb;SERVER=localhost;PORT=35432;Uid=admin;Pwd=admin;");
c.execute("select * from persons where person_id < 1000");
System.out.println("Columns: " + c.getNumCols());
for (int i = 1, j = c.getNumCols(); i <= j; i++) {
ColumnMetadata meta = c.getColMetadata(i);
System.out.println("Column " + i + ": " + meta);
}
while (c.fetch()) {
for (int i = 1, j = c.getNumCols(); i <= j; i++) {
System.out.print(c.getColData(i) + (i == j ? "\n" : ", "));
}
}
c.freeStatement();
c.disconnect();
}
}
After compilation, I neede javah utility to obtain a C header file (.h).
javah -jni org.marvec.odbc.OdbcClient
Now the hard C part begins. I'm not a C guru and my compiler reports many warnings about my code. However, it compiles and works. First I copied org_marvec_odbc_OdbcClient.h to org_marvec_odbc_OdbcClient.c and added the shared handle storage to the header file as well as necessary includes. As you can see, we need to remember three different handles for each connection.
#include
#include
...
SQLHENV env[1024];
SQLHDBC dbc[1024];
SQLHSTMT stmt[1024];
You might find me wasting some memory but you know, in Java world, there is always enough memory... I will show you only some parts of the solution, link to a complete package is at the end of this post. The most difficult function is getting column metadata that requires creating a new Java object from C.
JNIEXPORT jobject JNICALL Java_org_marvec_odbc_OdbcClient_getColMetadata(JNIEnv *jnienv, jobject obj, jint col) {
int h = getStaticHandle(jnienv, obj);
SQLSMALLINT nameLength, dataType, decimalDigits, nullable;
SQLULEN colSize;
jstring jstr;
char name[512];
SQLDescribeCol(stmt[h], (SQLUSMALLINT) col, name, sizeof(name), &nameLength, &dataType, &colSize, &decimalDigits, &nullable);
jstr = (*jnienv)->NewStringUTF(jnienv, name);
jclass clazz;
jmethodID cid;
jobject meta;
clazz = (*jnienv)->FindClass(jnienv, "org/marvec/odbc/OdbcClient$ColumnMetadata");
cid = (*jnienv)->GetMethodID(jnienv, clazz, "", "(Ljava/lang/String;IJIZ)V");
meta = (*jnienv)->NewObject(jnienv, clazz, cid, jstr, dataType, colSize, decimalDigits, (jboolean) (nullable == SQL_NULLABLE));
return meta;
}
In some problematic methods I also check for errors using another utility library I mostly copied from Jinput project (see function throwIOException() for example).
JNIEXPORT void JNICALL Java_org_marvec_odbc_OdbcClient_connect(JNIEnv *jnienv, jobject obj, jstring conn) {
SQLRETURN ret;
jbyte *str;
int h = getStaticHandle(jnienv, obj);
char error[10240];
jclass cls = (*jnienv)->GetObjectClass(jnienv, obj);
str = (*jnienv)->GetStringUTFChars(jnienv, conn, NULL);
SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &(env[h]));
SQLSetEnvAttr(env[h], SQL_ATTR_ODBC_VERSION, (void *) SQL_OV_ODBC3, 0);
SQLAllocHandle(SQL_HANDLE_DBC, env[h], &(dbc[h]));
ret = SQLDriverConnect(dbc[h], NULL, str, SQL_NTS, NULL, 0, NULL, SQL_DRIVER_COMPLETE);
if (!SQL_SUCCEEDED(ret)) {
extractError(dbc[h], SQL_HANDLE_DBC, error, sizeof(error));
throwIOException(jnienv, "SQLDriverConnect failed with return value %d:\n%s", ret, error);
}
(*jnienv)->ReleaseStringUTFChars(jnienv, conn, str);
}
Now there are some dependencies in the C project. You must have installed the following libraries in your system:
- unixODBC
- unixODBC-dev
- some ODBC driver for your target database like odbc-postgresql
- libltdl
- and some common C libraries that are likely to be already present on your computer: pthread, dl
Now you must configure ODBC to provide you the driver (see the first part of the connection string DRIVER=). This is done in /etc/odbcinst.ini and we can configure both 32- and 64-bit versions:
[PostgreSQL]
Description = PostgreSQL driver for Linux & Win32
Driver = /usr/lib/odbc/psqlodbca.so
Setup = /usr/lib/odbc/libodbcpsqlS.so
[PostgreSQL64]
Description = PostgreSQL driver for Linux & Win32
Driver = /usr/lib64/odbc/psqlodbca.so
Setup = /usr/lib64/odbc/libodbcpsqlS.so
To compile the C library I created a small Makefile:
CC=gcc
CFLAGS=-shared -fPIC -w -I$(JAVA_HOME)/include -I$(JAVA_HOME)/include/linux
PROJECT=org_marvec_odbc_OdbcClient
all: $(PROJECT).so
$(PROJECT).so: $(PROJECT).c
$(CC) $(CFLAGS) util.c $(PROJECT).c -o $(PROJECT).so -lodbc -lpthread -lltdl -ldl
ln -sf $(PROJECT).so libjavaODBC.so
clean:
rm $(PROJECT).so libjavaODBC.so
Note the creation of a symbolic link. Java loads libraries on library path with standard name lib[name fo the library].so and only the library name is what you pass as an argument to System.loadLibrary().
Now to run the client just call:
java -Djava.library.path=. org.marvec.odbc.OdbcClient javaODBC
Feel free to download the complete source code at github. The code is provided as is, I do not take any responsibility for any damage it might cause to anybody or anything (including hardware, software, people, animals, etc). For instructions see JBoss Community document.