需求背景
日前经历了一个项目,客户有不同的数据源,目前有Oracle和MySQL,数据库的表结构一样,所以业务代码没有区别,数据源是动态变化的,用户可以在使用的过程中切换数据源。
期望的效果
目前有两个数据源:Oracle和MySQL的数据源,其中一个表的数据分别如下: Oracle:
MySQL:
不同数据源下的表结构相同,但是数据不同。
期望的效果就是用户通过类似下面的界面切换数据源,后续的业务都使用新切换的数据源。
代码实现
MyBatis Plus有个插件,实现了动态数据源,官方文档地址: github.com/baomidou/dy…
从这里索引,会有一个更详细的文档,不过这个更详细的文档大部分收费,这是我没有想到的。 我们这个需求比较简单,使用这个插件的一小部分功能即可。
Maven引入
Maven中引入最新版的插件
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
<version>4.3.1</version>
</dependency>
配置文件修改
动态数据源的插件一般的用法都是多数据源,但是我们不需要多数据源,在同一个时刻,只需要一个数据源,并且这个数据源是用户手动配置的,所以我们期望在程序启动时无数据源,为此配置文件的数据源做如下配置:
spring:
datasource:
dynamic:
primary: master
strict: false
我们数据源的名字固定为master,需要注意的是strict设置为false。这样程序启动时,不会因为没有数据源就报错,但是会出现这么一条log:
dynamic-datasource initial loaded [0] datasource,Please add your primary datasource or check your configuration
定义数据源连接DTO
/**
* 数据源连接DTO.
*
* @author liupeng
* @date 2025/3/6
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class DataSourceDTO {
public static final String DEFAULT_POOL_NAME = "master";
private String poolName;
private String driverClassName;
private String url;
private String username;
private String password;
}
连接数据源
定义一个Controller类,来连接数据源。
@RestController
@RequestMapping("/datasource")
@RequiredArgsConstructor
@Tag(name = "连接数据源", description = "连接数据源")
public class DataSourceController {
/**
* The Data source.
*/
final DataSource dataSource;
/**
* The Default data source creator.
*/
final DefaultDataSourceCreator defaultDataSourceCreator;
private final ModelService modelService;
/**
* 连接数据源.
*
* @param connRequest the data source dto
* @return the response entity
*/
@PostMapping("/connect")
@Operation(summary = "连接数据源", description = "连接数据源")
public ResponseEntity<ResultEntity<Boolean>> connect(@Valid @RequestBody DataSourceConnRequest connRequest) {
DataSourceProperty dataSourceProperty = new DataSourceProperty();
DataSourceDTO dataSourceDTO = DataSourceConnRequest.toDataSource(connRequest);
BeanUtils.copyProperties(dataSourceDTO, dataSourceProperty);
DynamicRoutingDataSource source = (DynamicRoutingDataSource) dataSource;
try {
// 如果连接信息不正确,这一步会抛出异常, 数据源创建失败
DataSource dataSourceLocal = defaultDataSourceCreator.createDataSource(dataSourceProperty);
source.addDataSource(dataSourceDTO.getPoolName(), dataSourceLocal);
// 数据源连接之后检索数据看看结果,这个对于数据库连接不上必须的
modelService.listAllModels();
} catch (Exception e) {
return ResponseEntity.ok(ResultEntity.error("连接失败, 请确认数据库连接参数是否正确"));
}
return ResponseEntity.ok(ResultEntity.ok(true));
}
/**
* 关闭数据源.
*
* @return the response entity
*/
@PostMapping("/close")
@Operation(summary = "关闭数据源", description = "关闭数据源")
public ResponseEntity<ResultEntity<Boolean>> close() {
DynamicRoutingDataSource source = (DynamicRoutingDataSource) dataSource;
source.removeDataSource(DataSourceDTO.DEFAULT_POOL_NAME);
return ResponseEntity.ok(ResultEntity.ok(true));
}
}
上面的代码也顺便创建了一个关闭数据源的方法,事实证明这个不是必须的。
用到了一个请求类,DataSourceConnRequest,代码如下:
/**
* 数据库连接请求.
*
* @author liupeng
* @date 2025 /3/6
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(title = "数据库连接请求对象")
public class DataSourceConnRequest {
/**
* The constant DB_TYPE_MYSQL.
*/
public static final String DB_TYPE_MYSQL = "mysql";
/**
* The constant DB_TYPE_ORACLE.
*/
public static final String DB_TYPE_ORACLE = "oracle";
/**
* The constant driver_type_thin.
*/
public static final String DRIVER_TYPE_THIN = "thin";
/**
* The constant driver_type_oci.
*/
public static final String DRIVER_TYPE_OCI = "oci";
/**
* The constant driver_type_oci8.
*/
public static final String DRIVER_TYPE_OCI8 = "oci8";
public static final String DRIVER_CLASS_NAME_ORACLE = "oracle.jdbc.driver.OracleDriver";
public static final String DRIVER_CLASS_NAME_MYSQL = "com.mysql.cj.jdbc.Driver";
@Schema(title = "数据库类型. 支持: MySql, Oracle")
@NotBlank(message = "必须选择数据库类型")
private String dbType;
@Schema(title = "主机")
@NotBlank(message = "必须选择主机")
private String host;
@Schema(title = "端口, 在不填写的情况下, MySql默认3306, Oracle默认1521")
private String port;
@Schema(title = "Oracle的情况请输入正确的SID, MySql的场合下输入数据库名称")
@NotBlank(message = "必须选择MySQL的数据库或者Oracle的SID")
private String database;
@Schema(title = "Oracle的情况下适用, 默认为Thin, 支持Thin, OCI, OCI8")
private String driverType;
@Schema(title = "用户名")
@NotBlank(message = "必须选择用户名")
private String username;
@Schema(title = "密码")
@NotBlank(message = "必须输入密码")
private String password;
/**
* To data source data source dto.
*
* @param request the request
* @return the data source dto
*/
public static DataSourceDTO toDataSource(DataSourceConnRequest request) {
if (request == null) {
return null;
}
if (!StringUtils.hasText(request.getDbType())) {
return null;
}
if (!DB_TYPE_MYSQL.equalsIgnoreCase(request.getDbType()) && !DB_TYPE_ORACLE.equalsIgnoreCase(request.getDbType())) {
return null;
}
// private String driverClassName;
String driverClassName = getDriverClassName(request.getDbType());
if (!StringUtils.hasText(driverClassName)) {
return null;
}
// private String url;
String url = getUrl(request);
if (!StringUtils.hasText(url)) {
return null;
}
// private String username;
String username = request.getUsername();
// private String password;
String password = request.getPassword();
return DataSourceDTO.builder()
.poolName(DataSourceDTO.DEFAULT_POOL_NAME)
.url(url)
.driverClassName(driverClassName)
.username(username)
.password(password)
.build();
}
private static String getUrl(DataSourceConnRequest request) {
if (!StringUtils.hasText(request.getDatabase())) {
return "";
}
if (DB_TYPE_MYSQL.equalsIgnoreCase(request.getDbType())) {
String port = request.getPort();
if (!StringUtils.hasText(port)) {
port = "3306";
}
return "jdbc:mysql://" + request.getHost()
+ ":"
+ port + "/"
+ request.getDatabase()
+ "?useUnicode=true&characterEncoding=UTF-8";
}
if (DB_TYPE_ORACLE.equalsIgnoreCase(request.getDbType())) {
String port = request.getPort();
if (!StringUtils.hasText(port)) {
port = "1521";
}
String driverType = request.getDriverType();
if (!StringUtils.hasText(driverType)) {
driverType = "thin";
}
return "jdbc:oracle:"
+ driverType
+ ":@" + request.getHost()
+ ":"
+ port
+ ":"
+ request.getDatabase();
}
return "";
}
private static String getDriverClassName(@NotBlank(message = "必须选择数据库类型") String dbType) {
if (DB_TYPE_MYSQL.equalsIgnoreCase(dbType)) {
return DRIVER_CLASS_NAME_MYSQL;
}
if (DB_TYPE_ORACLE.equalsIgnoreCase(dbType)) {
return DRIVER_CLASS_NAME_ORACLE;
}
return "";
}
}
这个只是检证用的代码,有些地方写得不够好,但是对于理解概念足够清晰了。
访问Model表数据业务逻辑
Controller
@RestController
@RequestMapping("/api/model")
@RequiredArgsConstructor
@Tag(name = "模型接口", description = "模型接口")
public class ModelController {
private final ModelService modelService;
/**
* Register response entity.
*
* @param id the id
* @return the response entity
*/
@PostMapping("/detail/{id}")
@Operation(summary = "模型详情", description = "根据ID查看详情")
@Parameters({
@Parameter(in = ParameterIn.PATH, name = "id", description = "ID", required = true)
})
public ResponseEntity<ResultEntity<ModelDTO>> register(@PathVariable(name = "id") String id) {
ResultEntity<ModelDTO> detail = ResultEntity.ok(modelService.detail(id));
return ResponseEntity.ok(detail);
}
@PostMapping("/all")
@Operation(summary = "列出所有模型", description = "列出所有模型")
@Parameters({
@Parameter(in = ParameterIn.PATH, name = "id", description = "ID", required = true)
})
public ResponseEntity<ResultEntity<List<ModelDTO>>> register() {
ResultEntity<List<ModelDTO>> models = ResultEntity.ok(modelService.listAllModels());
return ResponseEntity.ok(models);
}
}
Service
public interface ModelService extends IService<Model> {
/**
* 模型详情.
*
* @param id the id
* @return the model dto
*/
ModelDTO detail(String id);
/**
* 列出所有模型.
*
* @return the list
*/
List<ModelDTO> listAllModels();
}
实现类:
@Service
@Slf4j
@RequiredArgsConstructor
public class ModelServiceImpl extends ServiceImpl<ModelMapper, Model> implements ModelService {
/**
* 模型详情.
*
* @param id the id
* @return the model dto
*/
@Override
public ModelDTO detail(String id) {
Model entity = this.baseMapper.selectById(id);
return ModelConverter.INSTANCE.toDto(entity);
}
/**
* 列出所有模型.
*
* @return the list
*/
@Override
public List<ModelDTO> listAllModels() {
List<Model> entities = list();
return ModelConverter.INSTANCE.toDtoList(entities);
}
}
异常处理
没有连接数据源的时候如果试图访问数据库,应该报出合理的错误信息。
@ExceptionHandler(DataAccessException.class)
public ResponseEntity<ResultEntity<String>> dataAccessException(DataAccessException exception) {
log.error("[服务] - [捕获SQL异常]", exception);
String message = exception.getMessage();
if (StringUtils.hasText(message) && message.indexOf("CannotFindDataSourceException") > 0) {
message = "请先连接数据源再访问数据库";
}
return ResponseEntity.ok(ResultEntity.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), message));
}
验证结果
程序启动后不连接数据源
程序启动后不连接数据源,这时没有数据源,肯定会抛出异常,访问接口的结果:
连接Oracle数据源
请求连接Oracle数据源:
脱敏后的请求JSON对象如下:
{
"dbType": "oracle",
"host": "123.x.x.x",
"port": "",
"database": "XXXX",
"driverType": "",
"username": "XXX",
"password": "XXX"
}
数据源连接成功后再访问业务接口:
这时候就成功了, 跟我们文章开头给出的数据源一致。
连接MySQL数据源
我们重新连续MySQL数据源,由于代码中我们使用的键值都是master,会自动冲掉Oracle的数据源连接。
脱敏后端JSON请求对象:
{
"dbType": "mysql",
"host": "localhost",
"port": "",
"database": "XXX",
"driverType": "",
"username": "XXX",
"password": "XXX"
}
这时候再访问业务接口:
返回了MySQL数据源的业务数据。
至此,已经达成了我们期望的目标。
关闭数据源
目前业务上不需要关闭数据源,但是关闭数据源也是有效的。 不过我们的实现有点问题,大家可以试试看,使用本文中的代码关闭数据源会出现什么现象。