Integrates JDBI3 with guice. Based on dropwizard-jdbi3 integration.
Features:
- JDBI instance available for injection
- Introduce unit of work concept, which is managed by annotations and guice aop (very like spring's @Transactional)
- Repositories (JDBI proxies for interfaces):
- installed automatically (when classpath scan enabled)
- are normal guice beans, supporting aop and participating in global (thread bound) transaction.
- no need to compose repositories anymore (e.g. with @CreateSqlObject) to gain single transaction.
- can reference guice beans (with annotated getters)
- Automatic installation for custom
RowMapper
Added installers:
- RepositoryInstaller - sql proxies
- MapperInstaller - row mappers
Maven:
<dependency>
<groupId>ru.vyarus.guicey</groupId>
<artifactId>guicey-jdbi3</artifactId>
<version>{guicey.version}</version>
</dependency>
Gradle:
implementation 'ru.vyarus.guicey:guicey-jdbi3:{guicey.version}'
Omit version if guicey BOM used.
Register bundle:
GuiceBundle.builder()
.bundles(JdbiBundle.<ConfType>forDatabase((conf, env) -> conf.getDatabase()))
...
Here default JDBI instance will be created from database configuration (much like it's described in dropwizard documentation).
Or build JDBI instance yourself:
JdbiBundle.forDbi((conf, env) -> locateDbi())
Jdbi3 introduce plugins concept. Dropwizard will automatically register SqlObjectPlugin
, GuavaPlugin
, JodaTimePlugin
.
If you need to install custom plugin:
JdbiBundle.forDbi((conf, env) -> locateDbi())
.withPlugins(new H2DatabasePlugin())
Also, If custom registration must be performed on jdbi instance:
JdbiBundle.forDbi((conf, env) -> locateDbi())
.withConfig((jdbi) -> { jdbi.callSomething() })
Such configuration block will be called just after jdbi instance creation (but before injector creation).
Unit of work concept states for: every database related operation must be performed inside unit of work.
In JDBI such approach was implicit: you were always tied to initial handle. This lead to cumbersome usage of sql object proxies: if you create it on-demand it would always create new handle; if you want to combine multiple objects in one transaction, you have to always create them manually for each transaction.
Integration removes these restrictions: dao (repository) objects are normal guice beans and transaction
scope is controlled by @InTransaction
annotation (note that such name was intentional to avoid confusion with
JDBI own's Transaction annotation and more common Transactional annotations).
At the beginning of unit of work, JDBI handle is created and bound to thread (thread local). All repositories are simply using this bound handle and so share transaction inside unit of work.
Annotation on method or class declares transactional scope. For example:
@Inject MyDAO dao
@InTransaction
public Result doSomething() {
dao.select();
...
}
Transaction opened before doSomething() method and closed after it. Dao call is also performed inside transaction. If exception appears during execution, it's propagated and transaction rolled back.
Nested annotations are allowed (they simply ignored).
Note that unit of work is not the same as transaction scope (transaction scope could be less or equal to unit of work).
But, for simplicity, you may think of it as the same things, if you always use @InTransaction
annotation.
Transaction isolation level and readonly flag could be defined with annotation:
@InTransaction(TransactionIsolationLevel.READ_UNCOMMITTED)
@InTransaction(readOnly = true)
In case of nested transactions error will be thrown if:
- Current transaction level is different then nested one
- Current transaction is read only and nexted one is not (note that some drivers, like h2, ignore readOnly flag completely)
For example:
@InTransaction
public void action() {
nestedAction();
}
@InTransaction(TransactionIsolationLevel.READ_UNCOMMITTED)
public void nestedAction() {
...
}
When action()
method called new transaction is created with default level
(usually READ_COMMITTED). When 'nestedAction()' is called exception will be thrown
because it's transaction level requirement (READ_UNCOMMITTED) contradict with current transaction.
If required, you may use your own annotation for transaction definition:
JdbiBundle.forDatabase((conf, env) -> conf.getDatabase())
.withTxAnnotations(MyCustomTransactional.class);
Note that this will override default annotation support. If you want to support multiple annotations then specify all of them:
JdbiBundle.forDatabase((conf, env) -> conf.getDatabase())
.withTxAnnotations(InTransaction.class, MyCustomTransactional.class);
If you need to support transaction configuration with your annotation then:
- Add required properties into annotation itself (see
@InTransaction
as example). - Create implementation of
TxConfigFactory
(seeInTransactionTxConfigFactory
as example) - Register factory inside your annotation with
@TxConfigSupport(MyCustomAnnotationTxConfigFactory.class)
Your factory will be instantiated as guice bean so annotate it as Singleton, if possible to avoid redundant instances creation.
Configuration is resolved just once for each method, so yur factory will be called just once for each annotated (with your custom annotation) method.
Inside unit of work you may reference current handle by using:
@Inject Provider<Handle>
You may define transaction (with unit of work) without annotation using:
@Inject TransactionTempate template;
...
template.inTrasansaction((handle) -> doSomething())
Note that inside such manual scope you may also call any repository bean, as it's absolutely the same definition as with annotation.
You can also specify transaction config (if required):
@Inject TransactionTempate template;
...
template.inTrasansaction(
new TxConfig().level(TransactionIsolationLevel.READ_UNCOMMITTED),
(handle) -> doSomething())
Declare repository (interface or abstract class) as usual, using DBI annotations.
It only must be annotated with @JdbiRepository
so installer
could recognize it and register in guice context.
NOTE: singleton scope will be forced for repositories.
@JdbiRepository
@InTransaction
public interface MyRepository {
@SqlQuery("select name from something where id = :id")
String findNameById(@Bind("id") int id);
}
Note the use of @InTransaction
: it was used to be able to call repository methods without extra annotations
(the lowest transaction scope is repository itself). It will make beans "feel the same" as usual DBI on demand
sql object proxies.
@InTransaction
annotation is handled using guice aop. You can use any other guice aop related features.
Don't use DBI @Transaction and @CreateSqlObject annotations anymore: probably they will even work, but they are not needed now and may confuse.
All installed repositories are reported into console:
INFO [2016-12-05 19:42:27,374] ru.vyarus.guicey.jdbi3.installer.repository.RepositoryInstaller: repositories =
(ru.vyarus.guicey.jdbi3.support.repository.SampleRepository)
Repository can't be recognized from guice binding because repository type is abstract and guice would complain about it. But repository can be recognized from the chain.
For example, suppose there is a base interface Storage
and JDBI implementation is only one possible implementation: JdbiStorage extends Storage
.
In this case you can bind: bind(Storage.class).to(JdbiStorage.class)
and use
everywhere in code @Inject Storage storage;
(installer would bind interface to implementation and
guice would be able to correctly track binding to the generated instance).
Only in this case repository class could be recognized from guice binding (even if it's not declared as extension and classpath scan not used).
In all other cases, repository declaration would cause an error (to identify incorrect declaration).
By default, JDBI proxies for declared repositories created only on first repository method call. Lazy behaviour is important to take into account all registered JDBI extensions. Laziness also slightly speeds up application startup.
If required, you can enable eager initialization during bundle construction:
JdbiBundle.forDatabase((conf, env) -> conf.getDatabase())
.withEagerInitialization()
In the eager mode all proxies would be constructed after application initialization (before web part initialization).
You can access guice beans by annotating getter with @Inject
(javax or guice):
@JdbiRepository
@InTransaction
public interface MyRepository {
@Inject
MyOtherRepository getOtherRepo();
@SqlQuery("select name from something where id = :id")
String findNameById(@Bind("id") int id);
default String doSomething(int id) {
String name = findNameById(id);
return getOtherRepo().doSOmethingWithName(name);
}
}
Here call to getOtherRepo()
will return MyOtherRepository
guice bean, which is actually
another proxy.
If you have custom implementations of RowMapper
, it may be registered automatically.
You will be able to use injections there because mappers become ususal guice beans (singletons).
When classpath scan is enabled, such classes will be searched and installed automatically.
public class CustomMapper implements RowMapper<Custom> {
@Override
Custom map(ResultSet rs, StatementContext ctx) throws SQLException {
// mapping here
return custom;
}
}
And now Custom type could be used for queries:
@JdbiRepository
@InTransaction
public interface CustomRepository {
@SqlQuery("select * from custom where id = :id")
Custom findNameById(@Bind("id") int id);
}
All installed mappers are reported to console:
INFO [2016-12-05 20:02:25,399] ru.vyarus.guicey.jdbi3.installer.MapperInstaller: jdbi mappers =
Sample (ru.vyarus.guicey.jdbi3.support.mapper.SampleMapper)
If, for some reason, you don't need transaction at some place, you can declare raw unit of work and use assigned handle directly:
@Inject UnitManager manager;
manager.beginUnit();
try {
Handle handle = manager.get();
// logic executed in unit of work but without transaction
} finally {
manager.endUnit();
}
Repositories could also be called inside such manual unit (as unit of work is correctly started).