前文只做了基本的数据迁移功能,然而在实际生产环境下,很可能会因为业务的不同,设计和针对性开发相对应的功能以实现定制性需求;
事实上,由于是定制性需求,这方面的代码参数可以写死,只对接目标服务。当然也可以编写为配置的方式,但还是那句话,定制!并不见得其他项目就会用到,或者说用到的情况也不多
目录结构大同小异,新增两个vo方便参数传递
存放控制器,对外开放访问接口,尽可能只编写主要逻辑,具体实现交给service层
存放服务实现和映射(操作数据库),service负责业务功能的具体实现,需要与数据库交互时在mapper层编写响应的接口
存放自定义JavaBean,根据业务需求为了方便参数的传递而创建的自定义JavaBean
启动配置类,同时也存放数据迁移所需的一些配置,如接收、发送者数据库连接信息和定期任务等
存放对配置文件的读取类,使用配置文件保存配置信息,对于使用者来说远比根据需求传入一堆参数更友好
server:
port: 9020
# 配置定时任务执行时间(cron表达式)
schedules:
# 目前默认执行频率为每分钟一次
runBatch: 0 0/1 * * * ?
# 数据转移参数
transfer:
# 发送者数据库地址
sendUrl: jdbc:mysql://地址:端口/数据库?useUnicode=true&characterEncoding=utf8
# 发送者数据库用户名
sendName: 用户名
# 发送者数据库密码
sendPassword: 密码
# 发送者数据库查询语句
# tableName为表名全称,shortName为表名简称——非必要不用动
sendSql: select * from tableName where shortName_flag_state != '删除'
# 接收者数据库地址
acceptUrl: jdbc:mysql://地址:端口/数据库?useUnicode=true&characterEncoding=utf8
# 接收者数据库用户名
acceptName: 用户名
# 接收者数据库密码
acceptPassword: 密码
# 需要修改值的列名
# shortName为表名简称——非必要不用动
updateColumn: shortName_flag_state
# 需要修改为的值
updateString: 删除
# 是否开启定时转移任务
# 只有配置值为 true 时开启定时任务
schedule: false
spring:
application:
name: transfer #服务名
# 接收者數據庫信息
datasource:
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://地址:端口/数据库?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC
username: 用户名
password: 密码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>test</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>3.1.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
import com.rongyi.transfer.service.TransferService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
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.sql.*;
@Component
@RestController
@RequestMapping("/transfer")
public class TransferController {
@Autowired
private TransferService transferService;
// 迁移
@GetMapping("/moveTable/{shortName}")
public String moveTable(@PathVariable("shortName") String shortName) throws SQLException {
// c01、c02表需要额外处理
if (shortName.equals("c01") || shortName.equals("c02")) {
transferService.moveCustom(shortName);
// 其他表照常处理
} else {
transferService.moveData(shortName);
}
return shortName + "迁移完成";
}
}
import com.rongyi.transfer.mapper.TransferMapper;
import com.rongyi.transfer.vo.InsertVo;
import com.rongyi.transfer.vo.UpdateVo;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class TransferService {
@Resource
private TransferMapper transferMapper;
@Autowired
private ApplicationProperties properties;
// 第一个定制点(其实这部分可能会使用到的项目应该也不少,可以抽取出表名为配置)
// 发送者一张表的数据,接收者两张表接收——需要额外新增
public void extraInsert(InsertVo insertVo) {
if (insertVo.getTableName().equals("ctlm01")) {
// 如果是c01表需要额外新增数据到c11表
insertVo.setTableName("ctlm11");
insertVo.setShortName("c11");
} else { // ctlm02
// 如果是c02表需要额外新增数据到c12表
insertVo.setTableName("ctlm12");
insertVo.setShortName("c12");
}
// 判断表中是否已存在该数据(id判断)
Integer count = transferMapper.extraCheck(insertVo);
// 存在则删除
if (count > 0) {
transferMapper.extraDelete(insertVo);
}
// 新增数据
transferMapper.extraInsert(insertVo);
}
public void moveData(String shortName) throws SQLException {
// 创建变量保存表名
String tableName = "";
// 表名简称不为空时,通过表名简称前缀确定表名
if (shortName == null || "".equals(shortName)) {
// 表名简称为空直接返回
// return "表名不能为空";
System.out.println("表名不能为空");
}
// 表名拼接
if (shortName.contains("c")) {
tableName = shortName.replace("c", "ctlm");
} else if (shortName.contains("i")) {
tableName = shortName.replace("i", "inv");
} else if (shortName.contains("w")) {
tableName = shortName.replace("w", "weight");
}
// 表名不为空时才可进行数据转移操作
if (tableName == null || "".equals(tableName)) {
// 表名为空说明表名简称前缀无法识别
// return "无法识别表";
System.out.println("无法识别表");
} else {
move(tableName, shortName);
}
// 正常执行完成返回成功消息
System.out.println(shortName + "数据转移完毕");
}
// 接收到表全名、简称,迁移数据
private void move(String tableName, String shortName) throws SQLException {
// 建立发送者连接
Connection conSend = DriverManager.getConnection(properties.getSendUrl(), properties.getSendName(), properties.getSendPassword());
Statement stmtSend = conSend.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
// 创建变量保存查询sql
String sql = properties.getSendSql();
// 拼接
sql = sql.replace("tableName", tableName);
sql = sql.replace("shortName", shortName);
try {
// 通过拼接好的sql查询数据并保存到结果集
ResultSet send = stmtSend.executeQuery(sql);
if (send == null) {
// 结果集为空说明没有新数据,不进行后续操作
// return "null";
System.out.println("没有需要进行迁移的数据");
return;
}
// 建立接收者连接
Connection conAccept = DriverManager.getConnection(properties.getAcceptUrl(), properties.getAcceptName(), properties.getAcceptPassword());
Statement stmtAccept = conAccept.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
// 定位接收表
ResultSet accept = stmtAccept.executeQuery("select * from " + tableName);
// 遍历发送者结果集
while (send.next()) {
// 开启新增
accept.moveToInsertRow();
// 获取当前表结构
ResultSetMetaData metaData = send.getMetaData();
// 通过表结构字段数遍历并设置当前行数据
for (int i = 1; i <= metaData.getColumnCount(); i++) {
accept.updateString(metaData.getColumnLabel(i), send.getString(i));
}
// 保存新增内容
accept.insertRow();
// 创建column保存需要修改的字段名
String column = properties.getUpdateColumn();
column = column.replace("shortName", shortName);
// 修改发送者表需要的字段数据
send.updateString(column, properties.getUpdateString());
send.updateRow();
}
// 有异常时抛出
} catch (SQLException e) {
System.out.println(e.getMessage());
}
}
// 第二个定制点:数据源获取数据多表联查获取,发送者(在这里是接收者)比数据源表字段多
// 这个处理没有想到很好的代码抽取办法,所以在控制器(controller)层进行了表名判断走不同的方法
// 有兴趣的码友可以抽取一下看看,可行希望能告知一声(相互学习!)
public void moveCustom(String shortName) throws SQLException {
// 创建变量保存表名
String tableName = "";
// 表名简称不为空时,通过表名简称前缀确定表名
if (shortName == null || "".equals(shortName)) {
// 表名简称为空直接返回
// return "表名不能为空";
System.out.println("表名不能为空");
}
if (shortName.contains("c")) {
tableName = shortName.replace("c", "ctlm");
} else if (shortName.contains("i")) {
tableName = shortName.replace("i", "inv");
} else if (shortName.contains("w")) {
tableName = shortName.replace("w", "weight");
}
// 表名不为空时才可进行数据转移操作
if (tableName == null || "".equals(tableName)) {
// 表名为空说明表名简称前缀无法识别
// return "无法识别表";
System.out.println("无法识别表");
}
// 建立发送者连接
Connection conSend = DriverManager.getConnection(properties.getSendUrl(), properties.getSendName(), properties.getSendPassword());
Statement stmtSend = conSend.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
// 创建变量保存查询sql
String sql = properties.getSendSql();
// 拼接
sql = sql.replace("tableName", tableName);
sql = sql.replace("shortName", shortName);
try {
// 通过拼接好的sql查询数据并保存到结果集
ResultSet send = stmtSend.executeQuery(sql);
if (send == null) {
// 结果集为空说明没有新数据,不进行后续操作
// return "null";
System.out.println("没有需要进行迁移的数据");
}
// 建立接收者连接
Connection conAccept = DriverManager.getConnection(properties.getAcceptUrl(), properties.getAcceptName(), properties.getAcceptPassword());
Statement stmtAccept = conAccept.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
// 定位接收表
ResultSet accept = stmtAccept.executeQuery("select * from " + tableName);
// 遍历发送者结果集
while (send.next()) {
// 开启新增
accept.moveToInsertRow();
// 获取当前表结构
ResultSetMetaData metaData = send.getMetaData();
// 通过表结构字段数遍历并设置当前行数据
for (int i = 1; i <= metaData.getColumnCount(); i++) {
accept.updateString(metaData.getColumnLabel(i), send.getString(i));
}
// 保存新增内容
accept.insertRow();
// 创建column保存需要修改的字段名
String column = properties.getUpdateColumn();
column = column.replace("shortName", shortName);
// 修改发送者表需要的字段数据
send.updateString(column, properties.getUpdateString());
send.updateRow();
// 额外添加到表c202或c209
if (shortName.equals("c511") || shortName.equals("c512")) {
InsertVo insertVo = new InsertVo();
insertVo.setTableName(tableName);
insertVo.setShortName(shortName);
for (int i = 1; i <= metaData.getColumnCount(); i++) {
// 获取到相应的两个字段数据
if (metaData.getColumnLabel(i).equals(shortName + "_id_supp")) {
insertVo.setIdSupp(send.getString(i));
}
if (metaData.getColumnLabel(i).equals(shortName + "_var_supp")) {
insertVo.setVarSupp(send.getString(i));
}
}
extraInsert(insertVo);
}
}
// 有异常时抛出
} catch (SQLException e) {
System.out.println(e.getMessage());
}
// 正常执行完成返回成功消息
System.out.println(shortName + "数据转移完毕");
}
}
我这里mapper使用的注解方式,并且为了通用还使用了${}的方式替换SQL语句
当然这有一定的风险,可以对传入的shortName先做一个防注入处理
import com.rongyi.transfer.vo.InsertVo;
import com.rongyi.transfer.vo.UpdateVo;
import org.apache.ibatis.annotations.*;
@Mapper
public interface TransferMapper {
// c202、c209表额外操作
@Insert("insert into ${tableName} (${shortName}_id, ${shortName}_id_supp, ${shortName}_var_supp, ${shortName}_date_create, ${shortName}_flag_state) values(#{idSupp}, #{idSupp}, #{varSupp}, sysdate(), '正常')")
void extraInsert(InsertVo insertVo);
@Select("select count(*) from ${tableName} where ${shortName}_id = #{idSupp}")
Integer extraCheck(InsertVo insertVo);
@Delete("delete from ${tableName} where ${shortName}_id = #{idSupp}")
void extraDelete(InsertVo insertVo);
// 反迁移操作原表——配置文件的接收表与当前发送表一致
@Update("update ${tableName} set ${column} = #{value} where ${shortName}_id = #{id}")
void extraUpdate(UpdateVo updateVo);
}
import lombok.Data;
import lombok.ToString;
@Data
@ToString
public class InsertVo {
String tableName;
String shortName;
String idSupp;
String varSupp;
}
import lombok.Data;
import lombok.ToString;
@Data
@ToString
public class UpdateVo {
String tableName;
String shortName;
String id;
String column;
String value;
}
之所以叫定制拓展,是因为并不见得是大众项目所需要用到的,甚至就当前项目(服务)需要用到;
当然也有一些拓展功能可能是会有不少项目用到,但是又不好抽取通用代码的,我也归类于定制
码友们如果也有遇到什么有意思或者有困难的定制需求,欢迎一块沟通解决(手动笑脸)