Bài viết này trình bày cách triển khai tính năng kết nối và quản lý động nhiều cơ sở dữ liệu (MySQL và SQL Server) trong ứng dụng Spring Boot, sử dụng Druid làm pool kết nối.
1. Thiết lập cơ sở dữ liệu
Tạo các cơ sở dữ liệu cần thiết (ví dụ: test1, test2, test3) và các bảng tương ứng.
Bảng databasesource (trong cơ sở dữ liệu chính, ví dụ test1)
CREATE TABLE `databasesource` (
`datasource_id` varchar(255) DEFAULT NULL COMMENT 'Mã định danh nguồn dữ liệu',
`url` varchar(255) DEFAULT NULL COMMENT 'Thông tin kết nối',
`user_name` varchar(255) DEFAULT NULL COMMENT 'Tên người dùng',
`pass_word` varchar(255) DEFAULT NULL COMMENT 'Mật khẩu',
`code` varchar(255) DEFAULT NULL COMMENT 'Trường dự phòng',
`databasetype` varchar(255) DEFAULT NULL COMMENT 'Loại cơ sở dữ liệu'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `test1`.`databasesource` (`datasource_id`, `url`, `user_name`, `pass_word`, `code`, `databasetype`) VALUES
('db_mysql_2', 'jdbc:mysql://localhost:3306/test2?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&zeroDateTimeBehavior=convertToNull', 'root', 'your_mysql_password', NULL, 'mysql'),
('db_mssql_3', 'jdbc:sqlserver://localhost:1433;databaseName=test3', 'sa', 'your_sqlserver_password', NULL, 'sqlserver');
Bảng user (trong các cơ sở dữ liệu khác, ví dụ test2 và test3)
-- Trong cơ sở dữ liệu test2 (MySQL)
CREATE TABLE `user` (
`user_name` varchar(255) DEFAULT NULL,
`age` int(3) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `test2`.`user` (`user_name`, `age`) VALUES
('User từ DB MySQL 2', 20),
('User khác từ DB MySQL 2', 17);
-- Trong cơ sở dữ liệu test3 (SQL Server)
CREATE TABLE `user` (
`user_name` varchar(255) DEFAULT NULL,
`age` int DEFAULT NULL
);
INSERT INTO `test3`.`user` (`user_name`, `age`) VALUES
('User từ DB SQL Server 3', 11),
('User khác từ DB SQL Server 3', 12);
2. Cấu hình application.yml
Định nghĩa thông tin kết nối cho cơ sở dữ liệu mặc định (ví dụ: test1) và cấu hình chi tiết cho Druid connection pool.
server:
port: 9002
spring:
aop:
proxy-target-class: true # Sử dụng CGLIB proxy
datasource:
# Cấu hình cho cơ sở dữ liệu mặc định (ví dụ: MySQL)
url: jdbc:mysql://localhost:3306/test1?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&zeroDateTimeBehavior=convertToNull
username: root
password: your_mysql_password
driverClassName: com.mysql.cj.jdbc.Driver # Driver cho MySQL mới
# Cấu hình chi tiết cho Druid Connection Pool
type: com.alibaba.druid.pool.DruidDataSource
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL # Hoặc SELECT 1 cho các DB khác
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
filters: stat,wall,log4j
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
useGlobalDataSourceStat: true
3. Định nghĩa các lớp Entity và Mapper
DataSource.java (Entity cho bảng databasesource)
package com.pojo;
import javax.persistence.Table;
@Table(name="databasesource")
public class DataSource {
private String datasourceId;
private String url;
private String userName;
private String passWord;
private String code;
private String databasetype;
// Getters and Setters
public String getDatasourceId() { return datasourceId; }
public void setDatasourceId(String datasourceId) { this.datasourceId = datasourceId; }
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
public String getUserName() { return userName; }
public void setUserName(String userName) { this.userName = userName; }
public String getPassWord() { return passWord; }
public void setPassWord(String passWord) { this.passWord = passWord; }
public String getCode() { return code; }
public void setCode(String code) { this.code = code; }
public String getDatabasetype() { return databasetype; }
public void setDatabasetype(String databasetype) { this.databasetype = databasetype; }
@Override
public String toString() {
return "DataSource{" +
"datasourceId='" + datasourceId + '\'' +
", url='" + url + '\'' +
", userName='" + userName + '\'' +
", passWord='" + passWord + '\'' +
", code='" + code + '\'' +
", databasetype='" + databasetype + '\'' +
'}';
}
}
User.java (Entity cho bảng user)
package com.pojo;
import javax.persistence.Table;
import java.io.Serializable;
@Table(name="user")
public class User implements Serializable {
private String userName;
private Integer age; // Sử dụng Integer thay vì String cho age
// Getters and Setters
public String getUserName() { return userName; }
public void setUserName(String userName) { this.userName = userName; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
@Override
public String toString() {
return "User{" +
"userName='" + userName + '\'' +
", age=" + age +
'}';
}
}
DataSourceMapper.java
package com.dao;
import com.pojo.DataSource;
import tk.mybatis.mapper.common.Mapper;
public interface DataSourceMapper extends Mapper<DataSource> {
}
UserMapper.java
package com.dao;
import com.pojo.User;
import tk.mybatis.mapper.common.Mapper;
public interface UserMapper extends Mapper<User> {
}
4. Cấu hình Nguồn dữ liệu Động
DBContextHolder.java
Lớp này quản lý việc lưu trữ khóa (ID của nguồn dữ liệu) cho luồng hiện tại, cho phép chuyển đổi nguồn dữ liệu.
package com.configs;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DBContextHolder {
private static final Logger log = LoggerFactory.getLogger(DBContextHolder.class);
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDataSource(String dataSourceId) {
contextHolder.set(dataSourceId);
log.info("Đã chuyển sang nguồn dữ liệu: {}", dataSourceId);
}
public static String getDataSource() {
return contextHolder.get();
}
public static void clearDataSource() {
contextHolder.remove();
log.info("Đã chuyển về nguồn dữ liệu mặc định");
}
}
DynamicDataSource.java
Đây là lớp cốt lõi, kế thừa từ AbstractRoutingDataSource để xác định nguồn dữ liệu hiện tại dựa trên khóa từ DBContextHolder. Nó cũng cung cấp các phương thức để thêm, xóa và kiểm tra kết nối cơ sở dữ liệu động.
package com.configs;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.stat.DruidDataSourceStatManager;
import com.pojo.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.util.StringUtils;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class DynamicDataSource extends AbstractRoutingDataSource {
private final Logger log = LoggerFactory.getLogger(getClass());
private Map<Object, Object> dynamicTargetDataSources;
private Object dynamicDefaultTargetDataSource;
@Override
protected Object determineCurrentLookupKey() {
String dataSourceId = DBContextHolder.getDataSource();
if (!StringUtils.isEmpty(dataSourceId)) {
if (this.dynamicTargetDataSources.containsKey(dataSourceId)) {
log.info("--- Sử dụng nguồn dữ liệu: {} ---", dataSourceId);
} else {
log.warn("Nguồn dữ liệu không tồn tại: {}", dataSourceId);
// Có thể ném exception hoặc trả về null để sử dụng mặc định
return null;
}
} else {
log.info("--- Sử dụng nguồn dữ liệu mặc định ---");
}
return dataSourceId;
}
@Override
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
super.setTargetDataSources(targetDataSources);
this.dynamicTargetDataSources = targetDataSources;
}
@Override
public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
super.setDefaultTargetDataSource(defaultTargetDataSource);
this.dynamicDefaultTargetDataSource = defaultTargetDataSource;
}
// Phương thức tạo và cấu hình một DruidDataSource mới
private DruidDataSource createDruidDataSource(String key, String driverClass, String url, String username, String password, String databaseType) {
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setName(key);
druidDataSource.setDriverClassName(driverClass);
druidDataSource.setUrl(url);
druidDataSource.setUsername(username);
druidDataSource.setPassword(password);
// Cấu hình cơ bản cho pool
druidDataSource.setInitialSize(1);
druidDataSource.setMaxActive(20);
druidDataSource.setMaxWait(60000);
druidDataSource.setMinIdle(5);
// Cấu hình kiểm tra kết nối
String validationQuery = "SELECT 1"; // Query mặc định, có thể điều chỉnh
if ("mysql".equalsIgnoreCase(databaseType)) {
validationQuery = "SELECT 1";
} else if ("sqlserver".equalsIgnoreCase(databaseType)) {
validationQuery = "SELECT 1";
}
druidDataSource.setValidationQuery(validationQuery);
druidDataSource.setTestOnBorrow(true);
druidDataSource.setTestWhileIdle(true);
// Cấu hình khác
druidDataSource.setFilters("stat"); // Bao gồm stat filter
druidDataSource.setTimeBetweenEvictionRunsMillis(60000);
druidDataSource.setMinEvictableIdleTimeMillis(180000);
druidDataSource.setRemoveAbandoned(true);
druidDataSource.setRemoveAbandonedTimeout(3600);
druidDataSource.setLogAbandoned(true);
try {
druidDataSource.init();
log.info("Khởi tạo thành công DataSource: {}", key);
} catch (SQLException e) {
log.error("Lỗi khi khởi tạo DataSource {}: {}", key, e.getMessage());
throw new RuntimeException("Không thể khởi tạo DataSource: " + key, e);
}
return druidDataSource;
}
// Thêm một nguồn dữ liệu mới
public void addDataSource(DataSource dataSourceConfig) throws Exception {
String datasourceId = dataSourceConfig.getDatasourceId();
if (dynamicTargetDataSources.containsKey(datasourceId)) {
log.warn("Nguồn dữ liệu '{}' đã tồn tại. Không cần thêm lại.", datasourceId);
return;
}
log.info("Đang tạo nguồn dữ liệu: {}", datasourceId);
String driverClass = "com.mysql.cj.jdbc.Driver"; // Mặc định
String databaseType = dataSourceConfig.getDatabasetype().toLowerCase();
if ("mysql".equalsIgnoreCase(databaseType)) {
driverClass = "com.mysql.cj.jdbc.Driver";
} else if ("sqlserver".equalsIgnoreCase(databaseType)) {
driverClass = "com.microsoft.sqlserver.jdbc.SQLServerDriver";
// Có thể cần cấu hình thêm cho SQL Server nếu có
} else {
log.warn("Loại cơ sở dữ liệu không được hỗ trợ: {}", databaseType);
return;
}
// Kiểm tra kết nối cơ bản trước khi tạo
if (!isDataSourceValid(driverClass, dataSourceConfig.getUrl(), dataSourceConfig.getUserName(), dataSourceConfig.getPassWord())) {
log.error("Kiểm tra kết nối cho '{}' thất bại.", datasourceId);
throw new RuntimeException("Cấu hình kết nối cho nguồn dữ liệu '" + datasourceId + "' không hợp lệ.");
}
DruidDataSource newDataSource = createDruidDataSource(
datasourceId,
driverClass,
dataSourceConfig.getUrl(),
dataSourceConfig.getUserName(),
dataSourceConfig.getPassWord(),
databaseType
);
dynamicTargetDataSources.put(datasourceId, newDataSource);
// Cập nhật lại các nguồn dữ liệu cho AbstractRoutingDataSource
super.setTargetDataSources(this.dynamicTargetDataSources);
super.afterPropertiesSet(); // Quan trọng để cập nhật các resolvedDataSources
log.info("Đã thêm và kích hoạt nguồn dữ liệu: {}", datasourceId);
}
// Kiểm tra tính hợp lệ của một kết nối cơ sở dữ liệu
private boolean isDataSourceValid(String driverClass, String url, String username, String password) {
try {
Class.forName(driverClass);
try (Connection conn = DriverManager.getConnection(url, username, password)) {
return conn != null && !conn.isClosed();
}
} catch (Exception e) {
log.error("Lỗi kiểm tra kết nối: {}", e.getMessage());
return false;
}
}
// Xóa một nguồn dữ liệu động
public void removeDataSource(String datasourceId) {
if (dynamicTargetDataSources.containsKey(datasourceId)) {
DataSource dataSource = (DataSource) dynamicTargetDataSources.get(datasourceId);
if (dataSource instanceof DruidDataSource) {
DruidDataSource druidDataSource = (DruidDataSource) dataSource;
DruidDataSourceStatManager.removeDataSource(druidDataSource);
log.info("Đã gỡ bỏ và đóng nguồn dữ liệu: {}", datasourceId);
}
dynamicTargetDataSources.remove(datasourceId);
super.setTargetDataSources(this.dynamicTargetDataSources);
super.afterPropertiesSet();
} else {
log.warn("Không tìm thấy nguồn dữ liệu để gỡ bỏ: {}", datasourceId);
}
}
// Lấy danh sách các nguồn dữ liệu đang hoạt động
public Map<Object, Object> getActiveDataSources() {
return dynamicTargetDataSources;
}
}
DruidDBConfig.java
Cấu hình Bean cho nguồn dữ liệu mặc định, Druid connection pool, và DynamicDataSource. Quan trọng là cấu hình Bean DataSourceTransactionManager để hỗ trợ giao dịch trên nguồn dữ liệu động.
package com.configs;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import org.apache.ibatis.session.Configuration;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
@Configuration
@EnableTransactionManagement // Kích hoạt quản lý giao dịch
public class DruidDBConfig {
private final Logger log = LoggerFactory.getLogger(getClass());
@Value("${spring.datasource.url}")
private String defaultDbUrl;
@Value("${spring.datasource.username}")
private String defaultUsername;
@Value("${spring.datasource.password}")
private String defaultPassword;
@Value("${spring.datasource.driverClassName}")
private String defaultDriverClassName;
// Bean cho nguồn dữ liệu mặc định
@Bean(name = "defaultDataSource")
@Primary // Đánh dấu là nguồn dữ liệu chính
@Qualifier("defaultDataSource")
public DataSource defaultDataSource() throws SQLException {
DruidDataSource datasource = new DruidDataSource();
datasource.setUrl(this.defaultDbUrl);
datasource.setUsername(defaultUsername);
datasource.setPassword(defaultPassword);
datasource.setDriverClassName(defaultDriverClassName);
// Cấu hình Druid Pool từ application.yml
// (Các thuộc tính như initialSize, minIdle, maxActive, maxWait, filters... được cấu hình qua @ConfigurationProperties hoặc trực tiếp tại đây)
// Ví dụ: datasource.setInitialSize(5); ...
// Cấu hình validationQuery có thể cần điều chỉnh tùy DB
datasource.setValidationQuery("SELECT 1");
datasource.setTestOnBorrow(true);
datasource.setTestWhileIdle(true);
datasource.setFilters("stat,wall"); // Ví dụ cấu hình filter
datasource.init();
log.info("Khởi tạo nguồn dữ liệu mặc định thành công.");
return datasource;
}
// Bean cho DynamicDataSource
@Bean(name = "dynamicDataSource")
@Qualifier("dynamicDataSource")
public DynamicDataSource dynamicDataSource(@Qualifier("defaultDataSource") DataSource defaultDataSource) {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(new HashMap<>()); // Khởi tạo Map rỗng, sẽ thêm sau
dynamicDataSource.setDefaultTargetDataSource(defaultDataSource); // Thiết lập nguồn mặc định
dynamicDataSource.afterPropertiesSet(); // Quan trọng để khởi tạo các thành phần nội bộ
log.info("Khởi tạo DynamicDataSource thành công.");
return dynamicDataSource;
}
// Cấu hình SqlSessionFactory sử dụng DynamicDataSource
@Bean
public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dynamicDataSource);
// Cấu hình Mapper XML hoặc Entity package
// sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mybatis/mapper/*.xml"));
// sqlSessionFactoryBean.setTypeAliasesPackage("com.pojo");
// Sử dụng cấu hình MyBatis tùy chỉnh nếu cần
sqlSessionFactoryBean.setConfiguration(mybatisConfiguration());
return sqlSessionFactoryBean;
}
// Bean cho cấu hình MyBatis
@Bean
@ConfigurationProperties(prefix = "mybatis.configuration")
public Configuration mybatisConfiguration() {
return new Configuration();
}
// Bean cho TransactionManager, sử dụng DynamicDataSource
@Bean
public DataSourceTransactionManager transactionManager(@Qualifier("dynamicDataSource") DynamicDataSource dynamicDataSource) {
return new DataSourceTransactionManager(dynamicDataSource);
}
// --- Cấu hình Druid Monitor ---
@Bean
public ServletRegistrationBean druidStatViewServlet() {
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
servletRegistrationBean.addInitParameter("loginUsername", "admin");
servletRegistrationBean.addInitParameter("loginPassword", "123456");
servletRegistrationBean.addInitParameter("resetEnable", "false");
return servletRegistrationBean;
}
@Bean
public FilterRegistrationBean druidStatFilter() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(new WebStatFilter());
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return filterRegistrationBean;
}
}
5. Service và Controller để quản lý chuyển đổi
DBChangeService.java và DBChangeServiceImpl.java
Service này truy vấn thông tin các nguồn dữ liệu từ bảng databasesource và sử dụng DynamicDataSource để thêm mới hoặc quản lý các kết nối.
// DBChangeService.java
package com.service;
import com.pojo.DataSource;
import java.util.List;
public interface DBChangeService {
List<DataSource> getAllDataSourceConfigs();
void addOrUpdateDataSource(DataSource dataSourceConfig) throws Exception;
void removeDataSource(String datasourceId);
boolean switchToDataSource(String datasourceId) throws Exception;
}
// DBChangeServiceImpl.java
package com.service.impl;
import com.configs.DBContextHolder;
import com.configs.DynamicDataSource;
import com.dao.DataSourceMapper;
import com.pojo.DataSource;
import com.service.DBChangeService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class DBChangeServiceImpl implements DBChangeService {
private final Logger log = LoggerFactory.getLogger(getClass());
@Autowired
private DataSourceMapper dataSourceMapper;
@Autowired
private DynamicDataSource dynamicDataSource; // Inject DynamicDataSource
@Override
public List<DataSource> getAllDataSourceConfigs() {
// Lấy cấu hình từ DB
return dataSourceMapper.selectAll();
}
@Override
public void addOrUpdateDataSource(DataSource dataSourceConfig) throws Exception {
String datasourceId = dataSourceConfig.getDatasourceId();
log.info("Xử lý nguồn dữ liệu: {}", datasourceId);
// Kiểm tra xem đã tồn tại trong dynamicDataSource chưa
if (dynamicDataSource.getActiveDataSources().containsKey(datasourceId)) {
log.info("Nguồn dữ liệu '{}' đã tồn tại trong cache. Đang kiểm tra lại kết nối.", datasourceId);
// Có thể thực hiện kiểm tra kết nối lại và remove/add nếu cần
// Tạm thời, chúng ta giả định nếu đã có thì không làm gì hoặc chỉ cần kiểm tra
// Để đơn giản, ta remove và add lại để đảm bảo nó được khởi tạo đúng
dynamicDataSource.removeDataSource(datasourceId);
}
// Thêm cấu hình vào DB nếu chưa có (hoặc cập nhật)
// Giả sử dataSourceMapper đã có phương thức update hoặc bạn tự xử lý logic này
// Ví dụ đơn giản:
if (dataSourceMapper.selectByPrimaryKey(dataSourceConfig.getDatasourceId()) == null) {
dataSourceMapper.insert(dataSourceConfig);
} else {
dataSourceMapper.updateByPrimaryKey(dataSourceConfig); // Cần định nghĩa updateByPrimaryKey trong Mapper
}
// Thêm DataSource vào DynamicDataSource
dynamicDataSource.addDataSource(dataSourceConfig);
}
@Override
public void removeDataSource(String datasourceId) {
log.info("Đang gỡ bỏ nguồn dữ liệu: {}", datasourceId);
// Xóa khỏi DynamicDataSource
dynamicDataSource.removeDataSource(datasourceId);
// Xóa khỏi DB
dataSourceMapper.deleteByPrimaryKey(datasourceId); // Cần định nghĩa deleteByPrimaryKey
log.info("Đã gỡ bỏ nguồn dữ liệu: {}", datasourceId);
}
@Override
@Transactional // Đảm bảo tính nhất quán khi thay đổi cấu hình và chuyển đổi
public boolean switchToDataSource(String datasourceId) throws Exception {
log.info("Yêu cầu chuyển sang nguồn dữ liệu: {}", datasourceId);
// 1. Lấy cấu hình từ DB
DataSource dataSourceConfig = dataSourceMapper.selectByPrimaryKey(datasourceId); // Cần định nghĩa selectByPrimaryKey
if (dataSourceConfig == null) {
log.error("Không tìm thấy cấu hình cho nguồn dữ liệu: {}", datasourceId);
// Tùy chọn: Chuyển về mặc định hoặc ném lỗi
DBContextHolder.clearDataSource();
return false;
}
// 2. Đảm bảo nguồn dữ liệu đã được thêm vào DynamicDataSource
// Nếu chưa có trong cache của DynamicDataSource, hãy thêm nó vào
if (!dynamicDataSource.getActiveDataSources().containsKey(datasourceId)) {
log.info("Nguồn dữ liệu '{}' chưa được tải vào cache. Đang tải...", datasourceId);
dynamicDataSource.addDataSource(dataSourceConfig);
} else {
// Nếu đã có, có thể kiểm tra lại kết nối nếu cần thiết
log.info("Nguồn dữ liệu '{}' đã có trong cache.", datasourceId);
}
// 3. Thiết lập khóa cho DBContextHolder để chỉ định nguồn dữ liệu cần sử dụng
DBContextHolder.setDataSource(datasourceId);
return true;
}
}
AdminController.java
Ví dụ Controller sử dụng service để chuyển đổi và truy vấn dữ liệu từ các nguồn khác nhau.
package com.controller;
import com.pojo.User;
import com.service.DBChangeService;
import com.service.UserService; // Giả định có UserService để truy vấn User
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/admin")
public class AdminController {
@Autowired
private UserService userService; // Service để truy vấn User
@Autowired
private DBChangeService dbChangeService; // Service để quản lý nguồn dữ liệu
@GetMapping("/query/{id}")
public String queryAcrossDatabases(@PathVariable Long id) throws Exception {
// 1. Truy vấn từ nguồn dữ liệu mặc định (ví dụ: test1)
// DBContextHolder.clearDataSource(); // Đảm bảo về mặc định nếu chưa chắc chắn
List<User> usersFromDefault = userService.findUsersById(id); // Giả định phương thức này
System.out.println("Dữ liệu từ DB mặc định: " + usersFromDefault);
// 2. Chuyển sang nguồn dữ liệu MySQL (ví dụ: db_mysql_2)
String mysqlDataSourceId = "db_mysql_2";
dbChangeService.switchToDataSource(mysqlDataSourceId);
List<User> usersFromMysql = userService.findUsersById(id);
System.out.println("Dữ liệu từ DB MySQL (" + mysqlDataSourceId + "): " + usersFromMysql);
// 3. Chuyển sang nguồn dữ liệu SQL Server (ví dụ: db_mssql_3)
String mssqlDataSourceId = "db_mssql_3";
dbChangeService.switchToDataSource(mssqlDataSourceId);
List<User> usersFromMssql = userService.findUsersById(id);
System.out.println("Dữ liệu từ DB SQL Server (" + mssqlDataSourceId + "): " + usersFromMssql);
// 4. Quay về nguồn dữ liệu mặc định
DBContextHolder.clearDataSource();
List<User> usersBackToDefault = userService.findUsersById(id);
System.out.println("Dữ liệu sau khi quay về DB mặc định: " + usersBackToDefault);
return "Truy vấn đa cơ sở dữ liệu hoàn tất. Kiểm tra console.";
}
// Ví dụ về cách thêm một nguồn dữ liệu mới (thông qua API hoặc khi khởi động)
// @PostMapping("/add-datasource")
// public String addDataSource(@RequestBody DataSource dataSourceConfig) throws Exception {
// dbChangeService.addOrUpdateDataSource(dataSourceConfig);
// return "Nguồn dữ liệu đã được thêm/cập nhật.";
// }
// Ví dụ về cách xóa một nguồn dữ liệu
// @DeleteMapping("/remove-datasource/{id}")
// public String removeDataSource(@PathVariable String id) {
// dbChangeService.removeDataSource(id);
// return "Nguồn dữ liệu đã được gỡ bỏ.";
// }
}
6. Cập nhật Transaction Manager
Để đảm bảo giao dịch hoạt động đúng trên các nguồn dữ liệu động, cần cấu hình DataSourceTransactionManager trong DruidDBConfig.java như sau:
// Trong lớp DruidDBConfig.java
@Bean
public DataSourceTransactionManager transactionManager(@Qualifier("dynamicDataSource") DynamicDataSource dynamicDataSource) {
return new DataSourceTransactionManager(dynamicDataSource);
}
Khi sử dụng annotation @Transactional trên các phương thức service, Spring sẽ sử dụng DataSourceTransactionManager này để quản lý giao dịch trên nguồn dữ liệu hiện đang được chọn bởi DBContextHolder.
Lưu ý
- Đảm bảo bạn đã cài đặt driver JDBC cho cả MySQL và SQL Server trong project của mình.
- Các tham số kết nối (URL, username, password) cần được cấu hình chính xác.
- Logic xử lý lỗi và kiểm tra kết nối có thể cần được tinh chỉnh tùy theo yêu cầu cụ thể của ứng dụng.
- Việc thêm/xóa nguồn dữ liệu động có thể được thực hiện thông qua API, cấu hình ban đầu hoặc các cơ chế khác tùy thuộc vào nghiệp vụ.