Cookbook - database
Datasource independence
We talked about using databases with RIFE in . We added a simple database layer to the Friends database application, but we really just skimmed the surface there so let's revisit databases again.
Using multiple database drivers
In the Friends database, the elements all used a hard coded query manager, FriendManager, by explicitly creating it:
public void processElement()
{
Template template = getHtmlTemplate("display");
FriendManager manager = new FriendManager();
...
}
This works fine for simple cases, such as when you only use a small subset of database features that work exactly the same on a wide range of servers. It also would be fine when only one database server needs to be supported. Unfortunately, both those cases are quite rare.
The only way to solve this is by writing a separate database driver for each server. A good pattern that helps doing this cleanly and efficiently is to split the code in two parts: one that is common to all databases and one that isn't. The recommended way to do this with RIFE is to specify an interface for the manager, with methods for all database operations, such as install, remove, add, display like in our example.
Then we write an abstract subclass of DBQueryManager, with methods that matches the interface, but prefixed with an underscore, like _install, _remove, _add, _display and so on. In those methods, we put the parts that are independent of which database is used, for example checking that parameters are valid, executing statements and processing the results.
After doing this split, writing a driver for a specific database becomes a rather simple task. A driver is just a subclass of the abstract manager class mentioned above, and handles database dependent code like building queries and handling exceptions. This is done in the implementation of the interface, that is the methods install, remove and add, etc. A simple example might look like the following:
Datasource dependent implementation
public void add(Friend friend)
throws TextManagerException
{
// here we can perform database specific tasks
...
// then we let the manager superclass perform the database
// independent task
try
{
_add(sAdd, friend);
}
catch (FriendManagerException e)
{
// ... handle the exception, which often is database dependent
}
}
Finally, we'll take a look at the corresponding method in the shared part. The actual implementation is not important here, but notice the general structure, with no database dependent code:
Datasource independent implementation
protected void _add(Select addQuery, Friend friend)
throws TextManagerException
{
try
{
DbPreparedStatement ps_add = getPreparedStatement(addQuery);
try
{
ps_add.setBean(friend);
ps_add.executeQuery();
}
finally
{
ps_add.close();
}
}
catch (DatabaseException e)
{
throw new AddErrorException(friend, e);
}
}
Handling database exceptions
The database related methods in RIFE throw exceptions that extend DatabaseException on failure. This group of exceptions exists merely for informational purposes, to indicate that an error occurred. The exceptions are handled by the RIFE framework, that cleans up any used database resources, so you don't need to worry about that.
However, some methods let you go behind RIFE and access JDBC objects such as ResultSet directly. If you get an exception when invoking methods on those object, you are responsible for cleaning up all resources yourself.
Row processors revisited
In , we used a simple row processor, that takes rows from the database and appends the information as blocks in a template.
Row processors can be used in a more powerful way than that. A nice pattern that can be useful if you need to reuse a processor in various places in different ways, is to do as following: Create an abstract row processor class that implements the processRow method and gets the data from the result set as usual. Then it calls an abstract method, that you can be overridden in a subclass, to perform the actual work.
An abstract row processor
public boolean processRow(ResultSet resultSet)
throws SQLException
{
String name = resultSet.getString("name");
if (name != null)
{
gotName(name);
}
}
protected abstract gotName(String name);
This allows you to reuse the row processor and change its behavior instead of writing a new one every time. Since the actual logic of accessing the resultset and column names is isolated in one worker method, any class that extends this abstract row processor doesn't have to know the slightest single bit about the actual structure of the database itself.
You could even make the abstract row processor implement an interface that only declares the abstract methods and make your application completely independent of the fact that the data is provided through a database.