I’ve stalled on a project, because I decided to have a go with JDBI 3. I’ve only used JDBI 2 in previous projects – this is the general pattern I was using:
An Interface:
public interface AccountService { void addAccount(Account account, User user); }
Implementation:
public abstract class AccountServiceJdbi implements AccountService { @Override @Transaction public final void addAccount(Account account, User user) { long accountId = accountDao().insertAccount(account); accountDao().linkAccountToOwner(accountId, user.getId()); } @CreateSqlObject abstract AccountDao accountDao(); }
You can imagine the Dao – just a simple interface with @SQLQuery and @SQLUpdate annotations.
The Service is instantiated using
dbi.onDemand(AccountServiceJdbi.class);
I like this approach, because it is easy to write a Mock implementation of AccountService for use when testing other components, and to create a concrete extension of AccountServiceJdbi with a mock AccountDao for testing the service class logic.
The abstract class allows me to have transactional methods that combine Data Access methods from one or more DAOs. The implemented methods are final, because my class is not intended to be sub classed. This prohibits unintended overriding of methods, for example when creating a concrete implementation for testing.
HOWEVER…
If I try to follow this pattern in JDBI I get the following error:
java.lang.IllegalArgumentException: On-demand extensions are only supported for interfaces
As far as I can tell, these are my options in JDBI 3:
1. Use default methods in interfaces:
public interface AccountServiceJdbi extends AccountService { @Override @Transaction default void addAccount(Account account, User user) { long accountId = accountDao().insertAccount(account); accountDao().linkAccountToOwner(accountId, user.getId()); } @CreateSqlObject AccountDao accountDao(); }
This may look similar to what I was doing in JDBI 2, but it feels WRONG. I’ve lost all control over concrete implementations of this interface. You can’t make default methods final, so if I create an implementation for testing, I can’t guarantee that some of the default methods won’t be overridden.
Furthermore, in the abstract class, the `accountDao` method has default access, so can’t be accessed from outside the package. In JDBI 3 I lose this restriction; all methods in interfaces are public, so my AccountDao can be accessed from outside the package.
It just feels less concise; I am less able to prescribe how the classes should be used. More commenting and self enforced discipline will be required.
2. Handle transactions programmatically:
public final class AccountServiceJdbi implements AccountService { @Override public void addAccount(Account account, User user) { jdbi.useTransaction(handle -> { AccountDao accountDao = handle.attach(AccountDao.class); long accountId = accountDao().insertAccount(account); accountDao().linkAccountToOwner(accountId, user.getId()); }); } }
Because this is a concrete implementation, I can make it final and prohibit unintended sub classing. There is no unintended access to DAO classes.
However, the transaction handling code is entwined with the business logic, making it difficult to unit test this file without connecting to a database; I can’t easily create mock implementations of the Dao classes in order to test the business logic in isolation, because the DAOs are instantiated within the business logic. I guess I could do this with a proper mocking framework, but personally I’d much rather implement simple mocks myself for testing.
SO…
I’m still stalled. I don’t like either of these approaches, and I’m not very good at writing code I don’t like. I could go back to the JDBI 2 but that doesn’t seem right either. Suggestions welcome…