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 SortedSetusedHandles = 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.
No comments:
Post a Comment