mybatis操作多数据源实现的方法
本文讲解"mybatis操作多数据源实现的方法",希望能够解决相关问题。
现在有一个mysql数据源和一个postgresql数据源,使用mybatis对两个数据源进行操作:
1. 注入多数据源
可以对两个数据源分别实现其service层和mapper层,以及mybatis的配置类:
@configuration
// 这里需要配置扫描包路径,以及sqlsessiontemplateref
@mapperscan(basepackages = "com.example.mybatisdemo.mapper.mysql", sqlsessiontemplateref = "mysqlsqlsessiontemplate")
public class mysqlmybatisconfigurer {
/**
* 注入mysql数据源
*/
@bean
@configurationproperties(prefix = "spring.datasource.mysql")
public datasource mysqldatasource() {
return new druiddatasource();
}
/**
* 注入mysqlsqlsessionfactory
*/
@bean
public sqlsessionfactory mysqlsqlsessionfactory(datasource mysqldatasource) throws exception {
sqlsessionfactorybean factorybean = new sqlsessionfactorybean();
factorybean.setdatasource(mysqldatasource);
// 设置对应的mapper文件
factorybean.setmapperlocations(new pathmatchingresourcepatternresolver().getresources("classpath:" +
"/mappers/mysqlmapper.xml"));
return factorybean.getobject();
}
/**
* 注入mysqlsqlsessiontemplate
*/
@bean
public sqlsessiontemplate mysqlsqlsessiontemplate(sqlsessionfactory mysqlsqlsessionfactory) {
return new sqlsessiontemplate(mysqlsqlsessionfactory);
}
/**
* 注入mysqltransactionalmanager
*/
@bean
public datasourcetransactionmanager mysqltransactionalmanager(datasource mysqldatasource) {
return new datasourcetransactionmanager(mysqldatasource);
}
}@configuration
// 这里需要配置扫描包路径,以及sqlsessiontemplateref
@mapperscan(basepackages = "com.example.mybatisdemo.mapper.postgresql", sqlsessiontemplateref = "postgresqlsqlsessiontemplate")
public class postgresqlmybatisconfigurer {
/**
* 注入postgresql数据源
*/
@bean
@configurationproperties(prefix = "spring.datasource.postgresql")
public datasource postgresqldatasource() {
return new druiddatasource();
}
/**
* 注入postgresqlsqlsessionfactory
*/
@bean
public sqlsessionfactory postgresqlsqlsessionfactory(datasource postgresqldatasource) throws exception {
sqlsessionfactorybean factorybean = new sqlsessionfactorybean();
factorybean.setdatasource(postgresqldatasource);
// 设置对应的mapper文件
factorybean.setmapperlocations(new pathmatchingresourcepatternresolver().getresources("classpath:" +
"/mappers/postgresqlmapper.xml"));
return factorybean.getobject();
}
/**
* 注入postgresqlsqlsessiontemplate
*/
@bean
public sqlsessiontemplate postgresqlsqlsessiontemplate(sqlsessionfactory postgresqlsqlsessionfactory) {
return new sqlsessiontemplate(postgresqlsqlsessionfactory);
}
/**
* 注入postgresqltransactionalmanager
*/
@bean
public datasourcetransactionmanager postgresqltransactionalmanager(datasource postgresqldatasource) {
return new datasourcetransactionmanager(postgresqldatasource);
}
}在配置类中,分别注入了一个事务管理器transactionmanager,这个和事务管理是相关的。在使用@transactional注解时,需要配置其value属性指定对应的事务管理器。
2. 动态数据源
spring中提供了abstractroutingdatasource抽象类,可以用于动态地选择数据源。
public abstract class abstractroutingdatasource extends abstractdatasource implements initializingbean {
@nullable
private map targetdatasources;
@nullable
private object defaulttargetdatasource;
private boolean lenientfallback = true;
private datasourcelookup datasourcelookup = new jndidatasourcelookup();
@nullable
private map resolveddatasources;
@nullable
private datasource resolveddefaultdatasource;
// 略
}通过源码可以看到,该抽象类实现了initializingbean接口,并在其afterpropertiesset方法中将数据源以的形式放入一个map中。
public void afterpropertiesset() {
if (this.targetdatasources == null) {
throw new illegalargumentexception("property 'targetdatasources' is required");
} else {
this.resolveddatasources = collectionutils.newhashmap(this.targetdatasources.size());
this.targetdatasources.foreach((key, value) -> {
object lookupkey = this.resolvespecifiedlookupkey(key);
datasource datasource = this.resolvespecifieddatasource(value);
// 将数据源以的形式放入map中
this.resolveddatasources.put(lookupkey, datasource);
});
if (this.defaulttargetdatasource != null) {
this.resolveddefaultdatasource = this.resolvespecifieddatasource(this.defaulttargetdatasource);
}
}
}该类中还有一个determinetargetdatasource方法,是根据lookupkey从map中获取对应的数据源,如果没有获取到,则使用默认的数据源。
protected datasource determinetargetdatasource() {
assert.notnull(this.resolveddatasources, "datasource router not initialized");
object lookupkey = this.determinecurrentlookupkey();
// 根据lookupkey从map中获取对应的数据源
datasource datasource = (datasource)this.resolveddatasources.get(lookupkey);
if (datasource == null && (this.lenientfallback || lookupkey == null)) {
datasource = this.resolveddefaultdatasource;
}
if (datasource == null) {
throw new illegalstateexception("cannot determine target datasource for lookup key [" + lookupkey + "]");
} else {
return datasource;
}
}lookupkey是通过determinetargetdatasource方法获取到的,而它是一个抽象方法,我们要做的就是通过实现这个方法,来控制获取到的数据源。
@nullable protected abstract object determinecurrentlookupkey();
(1) 创建并注入动态数据源
创建abstractroutingdatasource的子类,实现determinecurrentlookupkey方法
public class routingdatasource extends abstractroutingdatasource {
@override
protected object determinecurrentlookupkey() {
return datasourcecontextholder.get();
}
}这里的datasourcecontextholder是一个操作threadlocal对象的工具类
public class datasourcecontextholder {
/**
* 数据源上下文
*/
private static final threadlocal contextholder = new threadlocal<>();
/**
* 设置数据源类型
*/
public static void set(datasourcetype type) {
contextholder.set(type);
}
/**
* 获取数据源类型
*
* @return datasourcetype
*/
public static datasourcetype get() {
return contextholder.get();
}
/**
* 使用mysql数据源
*/
public static void mysql() {
set(datasourcetype.mysql);
}
/**
* 使用postgresql数据源
*/
public static void postgresql() {
set(datasourcetype.postgresql);
}
public static void remove() {
contextholder.remove();
}
}通过调用datasourcecontextholder.mysql()或者datasourcecontextholder.postgresql()就能修改contextholder的值,从而在动态数据源的determinetargetdatasource方法中就能获取到对应的数据源。
在数据源配置类中,将mysql和postgresql的数据源设置到动态数据源的map中,并注入容器。
@configuration
public class datasourceconfigurer {
@bean
@configurationproperties(prefix = "spring.datasource.mysql")
public datasource mysqldatasource() {
return new druiddatasource();
}
@bean
@configurationproperties(prefix = "spring.datasource.postgresql")
public datasource postgresqldatasource() {
return new druiddatasource();
}
@bean
public routingdatasource routingdatasource(datasource mysqldatasource, datasource postgresqldatasource) {
map datasources = new hashmap<>();
datasources.put(datasourcetype.mysql, mysqldatasource);
datasources.put(datasourcetype.postgresql, postgresqldatasource);
routingdatasource routingdatasource = new routingdatasource();
routingdatasource.setdefaulttargetdatasource(mysqldatasource);
// 设置数据源
routingdatasource.settargetdatasources(datasources);
return routingdatasource;
}
}(2) mybatis配置类
由于使用了动态数据源,所以只需要编写一个配置类即可。
@configuration
@mapperscan(basepackages = "com.example.mybatisdemo.mapper", sqlsessiontemplateref = "sqlsessiontemplate")
public class mybatisconfigurer {
// 注入动态数据源
@resource
private routingdatasource routingdatasource;
@bean
public sqlsessionfactory sqlsessionfactory() throws exception {
sqlsessionfactorybean sqlsessionfactorybean = new sqlsessionfactorybean();
sqlsessionfactorybean.setdatasource(routingdatasource);
// 这里可以直接设置所有的mapper.xml文件
sqlsessionfactorybean.setmapperlocations(new pathmatchingresourcepatternresolver().getresources("classpath" +
":mappers/*.xml"));
return sqlsessionfactorybean.getobject();
}
@bean
public sqlsessiontemplate sqlsessiontemplate(sqlsessionfactory sqlsessionfactory) {
return new sqlsessiontemplate(sqlsessionfactory);
}
@bean
public datasourcetransactionmanager transactionalmanager(datasource mysqldatasource) {
return new datasourcetransactionmanager(mysqldatasource);
}
}(3) 使用注解简化数据源切换
我们虽然可以使用datasourcecontextholder类中的方法进行动态数据源切换,但是这种方式有些繁琐,不够优雅。可以考虑使用注解的形式简化数据源切换。
我们先定义两个注解,表示使用mysql数据源或postgresql数据源:
@target({elementtype.type, elementtype.method})
@retention(retentionpolicy.runtime)
public @interface mysql {
}@target({elementtype.type, elementtype.method})
@retention(retentionpolicy.runtime)
public @interface postgresql {
}再定义一个切面,当使用了注解时,会先调用切换数据源的方法,再执行后续逻辑。
@component
@aspect
public class datasourceaspect {
@pointcut("@within(com.example.mybatisdemo.aop.mysql) || @annotation(com.example.mybatisdemo.aop.mysql)")
public void mysqlpointcut() {
}
@pointcut("@within(com.example.mybatisdemo.aop.postgresql) || @annotation(com.example.mybatisdemo.aop.postgresql)")
public void postgresqlpointcut() {
}
@before("mysqlpointcut()")
public void mysql() {
datasourcecontextholder.mysql();
}
@before("postgresqlpointcut()")
public void postgresql() {
datasourcecontextholder.postgresql();
}
}在使用动态数据源的事务操作时有两个需要注意的问题:
问题一 同一个事务操作两个数据源
mybatis使用executor执行sql时需要获取连接,baseexecutor类中的getconnection方法调用了springmanagedtransaction中的getconnection方法,这里优先从connection字段获取连接,如果connection为空,才会调用openconnection方法,并把连接赋给connection字段。
也就是说,如果你使用的是同一个事务来操作两个数据源,那拿到的都是同一个连接,会导致数据源切换失败。
protected connection getconnection(log statementlog) throws sqlexception {
connection connection = this.transaction.getconnection();
return statementlog.isdebugenabled() ? connectionlogger.newinstance(connection, statementlog, this.querystack) : connection;
}public connection getconnection() throws sqlexception {
if (this.connection == null) {
this.openconnection();
}
return this.connection;
}private void openconnection() throws sqlexception {
this.connection = datasourceutils.getconnection(this.datasource);
this.autocommit = this.connection.getautocommit();
this.isconnectiontransactional = datasourceutils.isconnectiontransactional(this.connection, this.datasource);
logger.debug(() -> {
return "jdbc connection [" + this.connection + "] will" + (this.isconnectiontransactional ? " " : " not ") + "be managed by spring";
});
}问题二 两个独立事务分别操作两个数据源
(1) 在开启事务的时候,datasourcetransactionmanager中的dobegin方法会先获取connection,并保存到connectionholder中,将数据源和connectionholder的对应关系绑定到transactionsynchronizationmanager中。
protected void dobegin(object transaction, transactiondefinition definition) {
datasourcetransactionmanager.datasourcetransactionobject txobject = (datasourcetransactionmanager.datasourcetransactionobject)transaction;
connection con = null;
try {
if (!txobject.hasconnectionholder() || txobject.getconnectionholder().issynchronizedwithtransaction()) {
// 获取连接
connection newcon = this.obtaindatasource().getconnection();
if (this.logger.isdebugenabled()) {
this.logger.debug("acquired connection [" + newcon + "] for jdbc transaction");
}
// 保存到connectionholder中
txobject.setconnectionholder(new connectionholder(newcon), true);
}
txobject.getconnectionholder().setsynchronizedwithtransaction(true);
// 从connectionholder获取连接
con = txobject.getconnectionholder().getconnection();
// 略
// 将数据源和connectionholder的关系绑定到transactionsynchronizationmanager中
if (txobject.isnewconnectionholder()) {
transactionsynchronizationmanager.bindresource(this.obtaindatasource(), txobject.getconnectionholder());
}
// 略
}(2) transactionsynchronizationmanager的bindresource方法将数据源和connectionholder的对应关系存入线程变量resources中。
public abstract class transactionsynchronizationmanager {
// 线程变量
private static final threadlocal 

