A quick intro into SQLC
SQLC (SQL Compiler) generates Java classes from SQL statements using statement and table metadata obtained from the database.
It is implemented as an
Ant task.
The primary goal of SQLC is to decouple Java code and SQL (or any other QL).
Sample application
SQLC sample application can be downloaded from
Download page.
Unzip it and run build - it will generate database access classes, compile code and execute it.
Additional reading
You can read more about SQLC and the sample application in this
article.
Supported databases
SQLC uses statement metadata, in particular parameter metadata. Thus database driver must implement getParameterMetadata() method. The table below contains results of my evaluations of some popular RDBMS.
Database
|
Version
|
Compatible with SQLC
|
Hypersonic
|
1.7.2 on JDK 1.4
|
Yes
|
Cloudscape
|
10
|
Yes
|
Firebird
|
1.5.1, JDBC driver 1.5
|
Yes
|
Oracle |
ORANXO driver
|
Yes
|
Drivers from Oracle 8.1.7, 9.2, 10g (classes12.zip, ojdbc14.jar) |
No |
MySQL |
4.0.2, JDBC driver v. 3.1.4 |
No |
Sun ODBC-JDBC bridge
|
Java 1.4
|
No
|
I will appreciate if you send me results of your evaluations to put to the table above.
Define sqlc task
SQLC resides in
Common library and uses
BCEL and
ANTLR for code generation. If you have hgcommons.jar, bcel-5.1.jar and antlr.jar in the system classpath or in Ant lib directory then SQLC task can be defined as
Otherwise jars which are not in the classpath shall be specified in task definition classpath. SQLC connects to the target database during generation, therefore database driver shall also be present in the classpath
Generation from table metadata
SQLC can generate standard operations for tables using table metadata:
- Select all rows
- Select a row by primary key (if table has primary key)
- Delete all rows
- Delete row by primary key (if table has primary key)
- Insert row
- Update row by primary key
This behaviour can be turned on by adding
table element to
sqlc task.
Generation from index metadata
SQLC generates methods from table indices with names confirming with the following naming convention:
_SQLC
_
Modes
Code | Method postfix | Generates |
O | Ordered | Select ordered by the index. |
E | EQ | Select and delete of rows with equal index columns |
N | NE | Select and delete of rows with non-equal index columns |
L | LT | Select and delete of rows with positions in the index less than specified in parameters |
M | LE | Select and delete of rows with positions in the index less or equal than specified in parameters |
G | GT | Select and delete of rows with positions in the index greater than specified in parameters |
H | GE | Select and delete of rows with positions in the index greated or equal than specified in parameters |
Generated methods follow the following pattern:
- Select: get
- Delete: delete
Example: Index on COMPILATION_UNIT table with name IX_COMPILATION_UNIT_SQLCE_PACKAGE will result in the following generated methods, assuming that the index is built on PACKAGE column of type CHAR or VARCHAR:
- getCompilationUnitPackage(String Package)
- deleteCompilationUnitPackage(String Package)
If contains _ then it is removed and next letter capitalized. E.g. COMPILATION_UNIT -> CompilationUnit.
Generation from foreign key metadata
SQLC generates methods from table foreign (imported) keys with names confirming with the following naming convention:
_SQLC
Generated methods follow the following pattern:
By
- Delete: delete
By
Example: Foreign key on ACCOUNT table with name FK_ACCOUNT_SQLCOWNER will result in the following generated method, assuming that the index is built on not nullable ACCOUNT_OWNER column of type INTEGER:
- getAccountByOwner(int AccountOwner)
Generate classes
This is a fragment of Jsel build file
script="src/biz/hammurapi/jsel/impl/Hypersonic.sql"
dir="sqlc_generated"
docDir="sqlcDoc"
package="biz.hammurapi.jsel.impl.sql"
masterEngine="Engine"
>
SELECT * FROM COMPILATION_UNIT WHERE ID=?
SELECT * FROM COMPILATION_UNIT C
WHERE REPOSITORY=? AND C.STORE_LEVEL=? AND
EXISTS(SELECT * FROM COMPILATION_UNIT_SCAN S
WHERE S.COMPILATION_UNIT_ID=C.ID AND
S.REPOSITORY=C.REPOSITORY AND S.SCAN_ID=?)
DELETE FROM COMPILATION_UNIT WHERE ID=?
This task generates classes
Engine class and
CompilationUnit interface in
biz.hammurapi.jsel.impl.sql package. It also generates HTML documentation. The sample above uses Hypersonic in-memory database created using script file specified in script attribute. If you use another database, e.g. Oracle, then you should specify nested
connection element in SQLC task.
It is also possible to keep statements in the database itself. In this case you need to set
statementsQuery attribute. See documentation for details. This approach yields more clear separation of concerns between Java and DB and also provides more flexibility.
Use generated classes
Add generated classes and hgcommons.jar to classpath.
DataSource ds=...; // Obtain data source
SQLProcessor processor=new SQLProcessor(ds, null);
Engine engine=new Engine(processor);
CompilationUnit cu = engine.getCompilationUnit(33);
System.out.println(cu.getName());
engine.deleteCompilationUnit(88);
If you need transactional context
Connection con=...; // Obtain connection enrolled in transaction
SQLProcessor processor=new SQLProcessor(con, null);
Engine engine=new Engine(processor);
CompilationUnit cu = engine.getCompilationUnit(33);
System.out.println(cu.getName());
engine.deleteCompilationUnit(88);
... // do something else and commit transaction
Benefits
As you have seen from the samples above, usage of SQLC is quite straightforward. This is a summary of benefits it gives:
- Classes are generated from metadata obtained from the target database. This approach helps to avoid situations when somebody invokes setString(1, "Hello") for numeric field.
- Generation at build time ensures that the database and the code are in sync. E.g. you have DEV and PROD database servers and application servers. You created a new table in DEV, but by some reason this change wasn't moved to PROD database. With SQLC this problem will be discovered during build time - the build will fail. Otherwise it would be discovered only in runtime. If the forgotten table participates in a rarely used use-case the problem may be discovered months after the code move when develpment team is already dismissed. If it happens, say, during year close and it will be A REALLY BIG PROBLEM.
- Probability of connection leakage in SQLC is much less comparing to traditional JDBC-based database programming. The only scenario of connection leakage in SQLC is obtaining database-backed collection, and not iterating over it till the end of connection. In this scenario iterator, which holds database resources, shall be explicitly closed or it will be closed in finalize().
- Decoupling of SQL from Java gives the following benefits:
- It forces usage of parameterized statements, which are more performant than queries assembled in runtime.
- Java and SQL can be owned by different teams. SQL tuning can be performed without touching Java code.
- The same Java code may be used with different databases as long as compiled statements take the same number of parameters and return compatible results.
- It makes easier to perform SQL code reviews - both manual and automated.
Interface hierarchy
SQLC generates inerfaces to represent resultsets. E.g. if you have a query named Person, which returns resultset with 3 columns: ID INT NOT NULL, LAST_NAME VARCHAR(50), FIRST_NAME VARHCAR(50), then SQLC will generate interface
Person {
int getId();
String getLastName();
String getFirstName();
}
There can be multiple queries returning same set of columns. SQLC reuses already defined interfaces. You can explicitly suggest SQLC interfaces to use using nested
interface element. SQLC also generates interface hierarchy. E.g. if you have a query named UsaPerson, which returns all the columns of Person plus SSN VARCHAR(9), then SQLC will generate interface UsaPerson extends
Person {
String getSsn();
}
All generated interfaces extend biz.hammurapi.xml.dom.DomSerializable interface.
SQLC also discovers "Common denominator" interfaces. E.g. there are two queries, PersonUsa and PersonRussia:
public interface
PersonUsa {
int getId();
String getLastName();
String getFirstName();
String getSsn();
}
public interface PersonRussia {
int getId();
String getLastName();
String getFirstName();
String getPassportNo();
}
SQLC will find that
- Both queries names start with word 'Person'. Word is defined as a sequence of charactes starting with capital letter.
- Queries have common columns
And it will generate inerface Person. PersonUsa and PersonRussia will extend interface Person:
public interface
Person {
int getId();
String getLastName();
String getFirstName();
}
public interface PersonUsa extends Person {
String getSsn();
}
public interface PersonRussia extends Person{
String getPassportNo();
}
Generated interface implementations
SQLC generates implementations of interfaces. There are two kinds of generated implementations - Plain and Smart. Plain implementations are generated for queries and smart implementations are generated for tables. Plain implementation is a simple JavaBean with getters and setters. Smart implementation extends
biz.hammurapi.sql.DatabaseObject and keeps track of modified fields.
update<table name>(<row interface>) and
insert<table name>(<row interface>) engine methods detect whether parameter passed to them is instance of DatabaseObject and delegates update and insert to the parameter if so. DatabaseObject inserts/updates only modified fields, which allows, for example, to insert records into tables with automatically generated primary key without having to create a separate
for it.
Projection to subclasses of generated implementations
Per each select SQLC generates several methods. For single-row select there are two methods - one returns automatically generated implementation and the other takes additional parameter of type
java.lang.Class. If passed parameter is subtype of automatically generated implementation then it is instantiated. Otherwise an attempt to convert source object to target class is taken. Target class is searched for constructors taking source class and if one is found target class is instantiated. As the last resort constructor from String is sought and used after converting source object to String.
The same thing happens with elements of collections returned by the generated engine class.
Projection to arbitrary classes
For multirow selects SQLC generates 6 methods per select:
- Projection to collection
- Projection to collection of targetClass instances as described in the previous section.
- Projection with conversion - biz.hammurapi.util.Converter instance shall be passed as a parameter to convert objects behind the scenes.
- Projection to database backed collection
- Projection to database backed collection of targetClass instances as described in the previous section.
- Projection to database backed collection with conversion - biz.hammurapi.util.Converter instance shall be passed as a parameter to convert objects behind the scenes.
Database backed collections and JDBC resources leaks
Database backed collection is an implementation of
java.util.Collection to iterate over big result sets without placing them to memory. The collection doesn't hold any JDBC resources or data items. For each invocation it goes to the database.
Iterators created by database backed collections hold open result sets and release them when there are no more elements in the result set. If client code doesn't intend to iterate till the end of the collection it must cast iterator to
com.pavelvasov.sql.DataIterator and call its
close() method.
It is also a good idea to close data iterators in finally blocks.
Failure to ensure proper iterator lifecycle leads to connection leaks. To troubleshoot such situations iterators have finalize() method, which outputs a message to console saying which SQL statement was leaked.
Nullability and primitive types
For columns of primitive type, such as integer, return type of generated getter depends on column nullability. For example, for integer non-nullable column ID
int getId() will be generated. For nullable column return type would be
java.lang.Integer.
The same applies to parameters, but by default all parameters are treated as non-nullable, unless
hasNullableParameters attribute is set to
true
Processing instead of projection
Sometimes you want to execute a query and take some action on each row without projecting this row to Java class. SQLC generates processXXX(..., RowProcessor) method along with getXXX methods for each query.
Dynamic queries
It is often needed to execute families of queries where list of columns doesn't change and only WHERE clause changes. One of examples is search for, say, account transaction, based on criteria set by user.
SQLC addresses this scenario by generating 2 static project() methods in interface implementations. These methods take SQL statement and parameterizer. Query result set is projected to a collection of instances of the declaring class.
Thus in SQLC you should define one of the dynamic queries to generate interface and implementation and then use project() to execute others.
Documentation
SQLC generates documentation in XML, which can be styled to HTML and also be used to generate higher-level classes, which use SQLC-generated classes) (e.g. JSP's which display table content).
Adding qualities to smart implementations
Smart implementations by default extend
biz.hammurapi.sql.DatabaseObject
. This class provides a number of services like serialization to and from XML, but it is impossible to anticipate and put all possible services in one class. There is a way to tell SQLC what base class to use for smart interface implementations. Just set
smartBase
attribute to base class name. hgcommons library provides one subclass of
DatabaseObject
-
MeasuringDatabaseObject
. This class provides an additional service - it measures intensity (number of invocations) and time of load, insert, update and delete operations.
Conclusion
There are many different approaches coping with alleviation of communication between Java and relational databases. Most of them concentrate on flexibility and have word "mapping" somewhere in description.
SQLC concentrates on simplicity and robustness. Give it a try and you'll be surprised how easy it will become to work with databases.