I'm writing a service that needs to use a different database depending on context (a simple string label). Each database has exactly the same schema. The list of databases is dynamic.
Looking through MyBatis-Guice documentation on multiple data sources, the example is where the list of datasources are known upfront, and each datasource has a different mapper. Similarly, a question found here on SO assumes the same requirements.
As stated, my requirements are much more dynamic and fluid. The idea is to have all the currently known databases (with their connection information) in a config and have that parsed at service startup. Then, dependent upon the context of any incoming requests, the code should pull the SqlSessionFactory for the correct database. All downstream code that uses that SqlSessionFactory is exactly the same - i.e. not dependent on request context. Which means the same mappers are used no matter what database is required.
My MyBatis and Guice knowledge is admittedly very new and limited. However, I've not been able to google anything that shows the MyBatis-Guice equivalent to the multiple environment approach supported by the XML configuration of MyBatis.
I managed to come up with a solution that works for me, so thought I'd share it here. The decision to use Guice had already been made, so there was no wriggle room there.
First, I wrote a MyBatis Guice module for registering a single datasource. It is a PrivateModule
so that all the MyBatis classes that get registered for one datasource do not conflict with other registrations for other datasources. It makes use of an internal MyBatisModule implementation because Java doesn't support multiple inheritance. Meaning we can't do public class MyMyBatisModule extends PrivateModule, MyBatisModule {...}
.
public class MyMyBatisModule extends PrivateModule {
private final String datasourceLabel;
private final Properies datasourceProperties;
private List< Key<?> > exposedKeys = new ArrayList< Key<?> >();
public MyMyBatisModule( String datasourceLabel, Properties datasourceProperties ) {
this.datasourceLabel = datasourceLabel;
this.datasourceProperties = datasourceProperties;
}
@Override
protected void configure() {
install( new InternalMyMyBatisModule( ) );
for( Key<?> key: keys ) {
expose( key );
}
}
private class InternalMyMyBatisModule extends MyBatisModule {
@Override
protected void initialize( ) {
environmentId( datasourceLabel );
Names.bindProperties( binder(), properties );
install( JdbcHelper.MySQL ); // See JDBC Helper commentary below
bindDataSourceProviderType( C3p0DataSourceProvider.class ); // Choose whichever one you want
bindTransactionFactoryType( JdbcTransactionFactory.class );
// Register your mapper classes here. These mapper classes will have their
// keys exposed from the PrivateModule
//
// i.e.
//
// keys.add( registerMapper( FredMapper.class );
// kets.add( registerMapper( GingerMapper.class );
}
private <T> Key<T> registerMapper( Class<T> mapperClass ) {
Key<T> key = Key.get( mapperClass, Names.named( datasourceLabel ) );
bind( key ).to( mapperClass );
addMapperClass( mapperClass );
return key;
}
}
}
JdbcHeler.MySQL: I've used JdbcHelper.MySQL
as a shortcut to map properties to the connection string, and use com.mysql.jdbc.Driver
as the JDBC driver. It's declared as:
MySQL("jdbc:mysql://${JDBC.host|localhost}:${JDBC.port|3306}/${JDBC.schema}", "com.mysql.jdbc.Driver"),
Now it's time to register all your datasources. MyBatisModules
handles this for us. It requires a map of datasourceLabel to jdbc properties.
public class MyBatisModules extends AbstractModule {
private Map< String, Properties > connectionsProperties;
public MyBatisModules( Map< String, Properties > = new HashMap< String, Properties > connectionsProperties ) {
this.connectionsProperties = connectionsProperties; // consider deep copy if appropriate
}
@Override
protected void configure( ) {
for( Entry< String, Properties > datasourceConnectionProperties : this.connectionsProperties.entrySet() ) {
install( new MyMyBatisModule( datasourceConnectionProperties.getKey(), datasourceConnectionProperties.getValue() ) );
}
bind( MapperRetriever.class ); // See MapperRetriever later
// bind your DAO classes here. By wrapping MyBatis Mapper use in DAO implementations, theoretically we
// can fairly easily change from MyBatis to any other database library just by changing the DAO implementation.
// The rest of our codebase would remain the same.
//
// i.e.
//
// bind( FredDao.class ).to( FredDaoMyBatis.class );
// bind( GingerDao.class).to( GingerDaoMyBatis.class );
}
}
Now we just need some way of getting the right Mapper class (which itself is associated with the right datasource). To do this, we actually need to call a method on the Guice Injector. I don't really like the idea of passing that around, so I wrapped it in MapperRetriever
. You need to implement a retrieval method for each of your Mapper
s.
public class MapperRetriever {
private final Injector injector;
@Inject
public MapperRetriver( Injector injector ) {
this.injector = injector;
}
// The follwing two methods use the example Mappers referenced in the MyMyBatisModule implementation above
public FredMapper getFredMapper( String datasourceLabel ) {
return this.injector.getInstance( Key.get( FredMapper.class, Names.named( datasourceLabel ) ) );
}
public GingerMapper getGingerMapper( String datasourceLabel ) {
return this.injector.getInstance( Key.get( GingerMapper.class, Names.named( datasourceLabel ) ) );
}
}
And an example DAO implementation ...
public interface FredDao {
Fred selectFred( String datasourceLable, String fredId );
}
public class FredDaoMyBatis implements FredDao {
private MapperRetriever mapperRetriever;
@Inject
public FredDaoMyBatis( MapperRetriever mapperRetriever ) {
this.mapperRetriever = mapperRetriever;
}
@Override
public Fred selectFred( String datasourceLabel, String fredId ) {
FredMapper fredMapper = this.mapperRetriever.getFredMapper( datasourceLabel );
return fredMapper.getFred( fredId );
}
}
You can also create a custom SqlSessionFactoryProvider which returns a SqlSessionFactory which delegates to the correct DataSource's SqlSessionFactory. Using a ThreadLocal to determine the underlying SqlSessionFactory.
public class DelegatingSqlSessionFactory implements SqlSessionFactory {
private final Map<String, SqlSessionFactory> factories = new HashMap<>();
public DelegatingSqlSessionFactory(Map<String, DataSource> dataSources) throws ClassNotFoundException {
dataSources.forEach((key, ds) -> {
factories.put(key, createSqlSessionFactory(ds));
});
}
private SqlSessionFactory delegate() {
// Read from a ThreadLocal to determine correct SqlSessionFactory key
String key = findKey();
return factories.get(key);
}
@Override
public SqlSession openSession() {
return delegate().openSession();
}
@Override
public SqlSession openSession(boolean autoCommit) {
return delegate().openSession(autoCommit);
}
@Override
public SqlSession openSession(Connection connection) {
return delegate().openSession(connection);
}
@Override
public SqlSession openSession(TransactionIsolationLevel level) {
return delegate().openSession(level);
}
@Override
public SqlSession openSession(ExecutorType execType) {
return delegate().openSession(execType);
}
@Override
public SqlSession openSession(ExecutorType execType, boolean autoCommit) {
return delegate().openSession(execType, autoCommit);
}
@Override
public SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level) {
return delegate().openSession(execType, level);
}
@Override
public SqlSession openSession(ExecutorType execType, Connection connection) {
return delegate().openSession(execType, connection);
}
@Override
public Configuration getConfiguration() {
return delegate().getConfiguration();
}
}
来源:https://stackoverflow.com/questions/33917115/equivalent-of-mybatis-xml-multiple-environments-in-mybatis-guice