📚专栏
「Java数据集成」专栏
- 《Java发起HTTP请求并解析JSON返回数据》:下图简称为《请求和解析》
- 《基于MyBatis实现依次、批量、分页增删改查操作》:下图简称为《依批分增删改查》
- 《用Python根据JSON生成Java类代码和数据库建表SQL语句》:下图简称为《生成代码脚本》
- 《基于SpringBoot+MyBatis的数据增删改查模板》:下图简称为《增删改查模板》
- 《Java发起同异步HTTP请求和处理数据》:下图简称为《同异步请求和处理》
- 《基于SpringBoot+MyBatis的数据集成模板》:下图简称为《数据集成模板》
- 《JavaHTTP请求工具类HTTPUtils》:下图简称为《HTTP请求工具类》
- 《JavaJSON处理工具类JSONUtils》:下图简称为《JSON处理工具类》
- 《JavaXML处理工具类XMLUtils》:下图简称为《XML处理工具类》
- 《用Python生成随机JSON数据》:下图简称为《生成随机数据脚本》
💬相关
本文涉及的模板代码已放在 Git 仓库,供学习交流(下面二选一,都一样)
https://gitee.com/dreature1328/springboot-mybatis-crud-template
https://github.com/dreature1328/springboot-mybatis-crud-template
由于作者最近频繁在集成数据,因而基于 Spring Boot + MyBatis 写了两套模板:数据增删改查模板和数据集成模板,辅之以两篇博客文章作为姊妹篇进行说明,前者可以说是后者的基础。
💬相关
博客文章《基于Spring Boot + MyBatis的数据增删改查模板》
https://blog.youkuaiyun.com/weixin_42077074/article/details/128868655
博客文章《基于Spring Boot + MyBatis的数据集成模板》
https://blog.youkuaiyun.com/weixin_42077074/article/details/129802650
本文涉及各种增删改查数据的方法,而且针对不同的数据量级,还分为依次、批量、分页三类操作。
依次、批量、分页处理
我们知道处理数据有三种思路:依次、批量、分页
- 依次处理:对数据一条一条进行逐个处理
- 批量处理:一次性处理一定量的数据
- 分页处理:上面两种方案进行折中,将大量数据分割成若干个小的数据块(即分页),每次只处理一页数据,即“总量分页,每页批量,依次换页”
而具体选取哪种方法取决于你的数据量(记录数 × 字段数,也就是行数 × 列数)大小
- 当有一定的数据量后(千量级),依次处理非常慢,最好使用批量处理
- 而数据量过于庞大时(十万量级)如果还全部进行批量处理,平均处理时间反而会增大
- 通常需要进行折中,使用分页处理,效果综合来看最优
笔者做了个简单的测试,测试方案如下,一个 JSON 对象对应数据表中的一条记录,对象含有两个键,取值分别为长度为 10 的和长度为 1000 的随机字符串,记录不同方案、不同数据量级下数据写入的耗时。
以下每项耗时数据至少测试三次并取平均值。 此外,测试数据是在较为理想的环境下测试获取的,而在实际使用场景中的数据应该会比测试数据略高。
记录数 | 依次写入耗时 | 批量写入耗时 | 分页写入耗时 |
---|---|---|---|
10 | 227ms | 56ms | 58ms |
1000 | 19.45s | 329ms | 315ms |
100000 | 34m29.7s | 31.26s | 22.62s |
而下图出自一篇研究,观察基于 MyBatis 批量写入数据下平均每条记录的耗时,横坐标是记录数,纵坐标是平均处理时间,它俩关系就像是倒过来的抛物线,类似的情况也可以出现在其他场景。
先记住这个分页处理函数 pageHandle()
,后文会频繁调用它。它意为以页面长度 pageSize
将传入的列表进行分页,每页依次交由函数 handleFunction()
进行处理。
// 分页处理
public <T> void pageHandle(List<T> list, int pageSize, Consumer<List<T>> handleFunction){
int count = 0;
while (!list.isEmpty()) {
// 取出当前页数据
List<T> subList = list.subList(0, Math.min(pageSize, list.size()));
// 执行插入或更新操作
handleFunction.accept(subList);
// 统计插入或更新的记录数
count += subList.size();
// 从列表中移除已处理的数据
list.subList(0, subList.size()).clear();
}
}
// 分页处理
public <T, R> List<R> pageHandle(List<T> list, int pageSize, Function<List<T>, List<R>> handleFunction) {
List<R> resultList = new ArrayList<>();
int count = 0;
while (!list.isEmpty()) {
// 取出当前页数据
List<T> subList = list.subList(0, Math.min(pageSize, list.size()));
// 执行查询操作
List<R> subResultList = handleFunction.apply(subList);
// 将结果添加到总结果列表中
resultList.addAll(subResultList);
// 统计查询的记录数
count += subList.size();
// 从列表中移除已处理的数据
list.subList(0, subList.size()).clear();
}
return resultList;
}
数据处理场景
后文将给出一个示例场景,具体步骤为:
- 在数据库
data_base
建立数据表data_table
,含有字段id
、field1
、field2
- 基于 Spring Boot + MyBatis 实现,采用多层架构(Controller 层、Service 层、Mapper层)
- 建立 Java 类
Data
,含有属性id
、attr1
、attr2
- 编写查询函数
selectData()
, 作用为查询表数据,对应 SQL 的SELECT
语句 - 编写插入函数
insertData()
,作用为插入表新数据,对应 SQL 的INSERT INTO
语句 - 编写更新函数
updateData()
,作用为更新表旧数据至新数据,对应 SQL 的UPDATE
语句 - 编写插入或更新函数
insertOrUpdateData()
,作用为若不存在重复数据(以主键或唯一键作为判断依据),则插入,若存在,则更新,对应 SQL 的INSERT INTO ... ON DUPLICATE KEY UPDATE ...
语句 - 编写删除函数
deleteData()
,作用为删除表内的某项数据,对应 SQL 的DELETE
语句 - 编写清空函数
clearData()
,作用为清空表原有数据,对应 SQL 的TRUNCATE
语句 - 各个增删改查方法都有依次、批量、分页处理的实现
建立数据表
DROP TABLE IF EXISTS `data_table`;
CREATE TABLE `data_table` (
`id` VARCHAR(255) NOT NULL PRIMARY KEY,
`field1` VARCHAR(255) DEFAULT NULL,
`field2` VARCHAR(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
数据库配置
配置文件 application.properties
常见配置
serverTimezone
:时区,如亚洲/上海时区Asia/Shanghai
useUnicode
和characterEncoding
:编码方式,如true
和utf8
allowMultiQueries
:是否支持”;"号分隔的多条 SQL 语句的执行,如true
autoReconnect
:是否超时重连(当一个连接的空闲时间超过 8 小时后,MySQL就会断开该连接),如true
useSSL
:是否使用 SSL,如true
spring.datasource.url=jdbc:mysql://<域名或IP地址>:<端口号>/<数据库名>?autoReconnect=true&useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&serverTimezone=Asia/Shanghai&useSSL=false&allowMultiQueries=true
建立 Java 类
public class Data {
// 类的属性
private String id;
private String attr1;
private String attr2;
// 类的成员列表构造函数
public Data(
String id,
String attr1,
String attr2
){
this.id = id;
this.attr1 = attr1;
this.attr2 = attr2;
}
// 类的复制构造函数
public Data(Data data) {
this.id = data.getId();
this.attr1 = data.getAttr1();
this.attr2 = data.getAttr2();
}
// 类的 Getter 方法
public String getId() {
return id;
}
public String getAttr1() {
return attr1;
}
public String getAttr2() {
return attr2;
}
// 类的 Setter 方法
public void setId(String id) {
this.id = id;
}
public void setAttr1(String attr1) {
this.attr1 = attr1;
}
public void setAttr2(String attr2) {
this.attr2 = attr2;
}
// 重写类的 toString 方法
@Override
public String toString() {
return
"Data["
+ "id=" + id + ", "
+ "attr1=" + attr1 + ", "
+ "attr2=" + attr2
+ "]";
}
}
若根据个人偏好引入了 Lombok 依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>
此处还可以用注解进一步简化成
@lombok.Data
@lombok.AllArgsConstructor
@lombok.NoArgsConstructor
public class Data {
// 类的属性
private String id;
private String attr1;
private String attr2;
}
不过注意,使用 Lombok 的弊端也很明显,慎用,如代码可读性低,影响升级,需要别人也安装否则报错(不过现在 IDEA 已经内置支持了 Lombok),其使用非标准注释处理方法易有遗患等
搭建多层架构
本模板采用分层架构,其中包括服务层(Service)、数据访问层(Mapper)、控制层(Controller),这种 SMC 架构是基于传统 MVC 架构的一种衍生架构,一般在前后端分离的应用开发中比较常见
- Service 层具体实现了业务逻辑
- Mapper 层中的 Java 接口在 MyBatis 框架下与 XML 映射文件相结合,映射 Java 方法至 SQL 语句,从而实现了对数据库的操作
本文仅涉及纯 Java 部分,而对应的 MyBatis 实现则在另一篇博客文章中有详细介绍
💬相关
博客文章《基于MyBatis实现依次、批量、分页增删改查操作》
https://blog.youkuaiyun.com/weixin_42077074/article/details/129405833
- Controller 层负责接收和处理 URL 请求并调用相应服务层函数
后文的 Service 层函数都有在 Controller 层对应的测试函数,通过发起一个请求快速调用并进行测试,函数返回类型均为 HTTP 响应结果类 HTTPResult
,并使用 @ResponseBody
注解将其序列化为 JSON 字符串
更简便的方法是,把 Controller 层中类最前面的 @Controller
换成 @RestController
(相当于 @Controller
注解和 @ResponseBody
注解的结合体),这样类中的所有方法都默认使用 @ResponseBody
注解,不需要再在方法上额外添加 @ResponseBody
注解
当然,你也可以用 Fastjson 包中 JSON.toJSONString()
达到同样的效果,不过前者更为简洁
💬相关
博客文章《JavaHTTP响应结果类HTTPResult》
https://blog.youkuaiyun.com/weixin_42077074/article/details/130054057
考虑参数列表
实际增删改查数据过程中,一般我们会需要各种各样的参数列表:单个对象、多个对象、对象数组、对象列表,示例场景如下
Data data1 = new Data("id1","value11","value12");
Data data2 = new Data("id2","value21","value22");
Data data3 = new Data("id3","value31","value32");
Data[] dataArray = {data1, data2, data3};
List<Data> dataList = new ArrayList<Data>(){{
add(data1);
add(data2);
add(data3);
}};
// 后文有这些函数的实现
insertData(data1);
insertData(data1, data2);
insertData(dataArray);
batchInsertData(dataList);
前三者可以通过可变参数(本质上是数组)实现,后者则直接将 List 作为参数实现,还可以专门写一个生成对象列表的函数方便测试
// 生成对象
public List<Data> generateDatas() {
// 自己按需求生成自定义对象列表
List<Data> dataList = new ArrayList<>();
Data data1 = new Data();
Data data2 = new Data();
Data data3 = new Data();
dataList.add(data1);
dataList.add(data2);
dataList.add(data3);
return dataList;
}
查询数据
Service 层
// 单项查询
public Data selectData(String id) {
return dataMapper.selectData(id);
}
// 依次查询
public List<Data> selectData(String... idArray) {
List<Data> dataList = new ArrayList<>();
for(String id : idArray){
dataList.add(dataMapper.selectData(id));
}
return dataList;
}
// 批量查询
public List<Data> batchSelectData(List<String> idList) {
return dataMapper.batchSelectData(idList);
}
// 分页查询
public List<Data> pageSelectData(List<String> idList) {
int pageDataSize = 12000; // 页面数据量大小,即每页记录数 × 字段数,可自行设置
int totalFields = Data.class.getDeclaredFields().length; // 总字段数,即数据表中的列数
int pageSize = pageDataSize / totalFields; // 页面大小,即每页记录数
return pageHandle(idList, pageSize, dataMapper::batchSelectData);
}
Mapper 层
// 依次查询
public Data selectData(String id);
// 批量查询
public List<Data> batchSelectData(List<String> idList);
Controller 层
// 依次查询
@RequestMapping("/data/select")
public HTTPResult selectData(String id) throws Exception {
return HTTPResult.success(dataService.selectData(id));
}
// 批量查询
@RequestMapping("/data/bselect")
public HTTPResult batchSelectData(String ids) throws Exception {
List<String> idList = Arrays.asList(ids.split(","));
return HTTPResult.success(dataService.batchSelectData(idList));
}
// 分页查询
@RequestMapping("/data/pselect")
public HTTPResult pageSelectData(String ids) throws Exception {
List<String> idList = Arrays.asList(ids.split(","));
return HTTPResult.success(dataService.batchSelectData(idList));
}
插入数据
Service 层
// 单项插入
public void insertData(Data data) {
dataMapper.insertData(data);
return ;
}
// 依次插入
public void insertData(Data... dataArray) {
for(Data data : dataArray) {
dataMapper.insertData(data);
}
return ;
}
// 批量插入
public void batchInsertData(List<Data> dataList) {
dataMapper.batchInsertData(dataList);
return ;
}
// 分页插入
public void pageInsertData(List<Data> dataList) throws Exception {
int pageDataSize = 12000; // 页面数据量大小,即每页记录数 × 字段数,可自行设置
int totalFields = Data.class.getDeclaredFields().length; // 总字段数,即数据表中的列数
int pageSize = pageDataSize / totalFields; // 页面大小,即每页记录数
pageHandle(dataList, pageSize, dataMapper::batchInsertData);
return ;
}
Mapper 层
// 依次插入
public void insertData(Data data);
// 批量插入
public void batchInsertData(List<Data> dataList);
Controller 层
// 依次插入
@RequestMapping("/data/insert")
public HTTPResult insertData() throws Exception {
dataService.insertData(dataService.generateDatas().toArray(new Data[0]));
return HTTPResult.success(null);
}
// 批量插入
@RequestMapping("/data/binsert")
public HTTPResult batchInsertData() throws Exception {
dataService.batchInsertData(dataService.generateDatas());
return HTTPResult.success(null);
}
// 分页插入
@RequestMapping("/data/pinsert")
public HTTPResult pageInsertData() throws Exception {
dataService.pageInsertData(dataService.generateDatas());
return HTTPResult.success(null);
}
更新数据
Service 层
// 单项更新
public void updateData(Data data) {
dataMapper.updateData(data);
return ;
}
// 依次更新
public void updateData(Data... dataArray) {
for(Data data : dataArray) {
dataMapper.updateData(data);
}
return ;
}
// 批量更新
public void batchUpdateData(List<Data> dataList) {
dataMapper.batchUpdateData(dataList);
return ;
}
// 分页更新
public void pageUpdateData(List<Data> dataList) throws Exception {
int pageDataSize = 12000; // 页面数据量大小,即每页记录数 × 字段数,可自行设置
int totalFields = Data.class.getDeclaredFields().length; // 总字段数,即数据表中的列数
int pageSize = pageDataSize / totalFields; // 页面大小,即每页记录数
pageHandle(dataList, pageSize, dataMapper::batchUpdateData);
return ;
}
Mapper 层
// 依次更新
public void updateData(Data data);
// 批量更新
public void batchUpdateData(List<Data> dataList);
Controller 层
// 依次插入或更新
@RequestMapping("/data/insertOrUpdate")
public HTTPResult insertOrUpdateData() throws Exception {
dataService.insertOrUpdateData(dataService.generateDatas().toArray(new Data[0]));
return HTTPResult.success(null);
}
// 批量插入或更新
@RequestMapping("/data/binsertOrUpdate")
public HTTPResult batchInsertOrUpdateData() throws Exception {
dataService.batchInsertOrUpdateData(dataService.generateDatas());
return HTTPResult.success(null);
}
// 分页插入或更新
@RequestMapping("/data/pinsertOrUpdate")
public HTTPResult pageInsertOrUpdateData() throws Exception {
dataService.pageInsertOrUpdateData(dataService.generateDatas());
return HTTPResult.success(null);
}
插入或更新数据
插入、更新、插入或更新的 Java 实现是几乎一样的,区别只在于 MyBatis 实现
Service 层
// 单项插入或更新
public void insertOrUpdateData(Data data) {
dataMapper.insertOrUpdateData(data);
return ;
}
// 依次插入或更新
public void insertOrUpdateData(Data... dataArray) {
for(Data data : dataArray) {
dataMapper.insertOrUpdateData(data);
}
return ;
}
// 批量插入或更新
public void batchInsertOrUpdateData(List<Data> dataList) {
dataMapper.batchInsertOrUpdateData(dataList);
return ;
}
// 分页插入或更新
public void pageInsertOrUpdateData(List<Data> dataList) throws Exception {
int pageDataSize = 12000; // 页面数据量大小,即每页记录数 × 字段数,可自行设置
int totalFields = Data.class.getDeclaredFields().length; // 总字段数,即数据表中的列数
int pageSize = pageDataSize / totalFields; // 页面大小,即每页记录数
pageHandle(dataList, pageSize, dataMapper::batchInsertOrUpdateData);
return ;
}
Mapper 层
// 依次插入或更新
public void insertOrUpdateData(Data data);
// 批量插入或更新
public void batchInsertOrUpdateData(List<Data> dataList);
Controller 层
// 依次插入或更新
@RequestMapping("/data/insertOrUpdate")
public HTTPResult insertOrUpdateData() throws Exception {
dataService.insertOrUpdateData(dataService.generateDatas().toArray(new Data[0]));
return HTTPResult.success(null);
}
// 批量插入或更新
@RequestMapping("/data/binsertOrUpdate")
public HTTPResult batchInsertOrUpdateData() throws Exception {
dataService.batchInsertOrUpdateData(dataService.generateDatas());
return HTTPResult.success(null);
}
// 分页插入或更新
@RequestMapping("/data/pinsertOrUpdate")
public HTTPResult pageInsertOrUpdateData() throws Exception {
dataService.pageInsertOrUpdateData(dataService.generateDatas());
return HTTPResult.success(null);
}
删除数据
删除与查询的 Java 实现是几乎一样的,区别只在于 MyBatis 实现
Service 层
// 单项删除
public void deleteData(String id) {
dataMapper.deleteData(id);
return ;
}
// 依次删除
public void deleteData(String... idArray) {
for(String id : idArray){
dataMapper.deleteData(id);
}
return ;
}
// 批量删除
public void batchDeleteData(List<String> idList) {
dataMapper.batchDeleteData(idList);
return ;
}
// 分页删除
public void pageDeleteData(List<String> idList) {
int pageDataSize = 12000; // 页面数据量大小,即每页记录数 × 字段数,可自行设置
int totalFields = Data.class.getDeclaredFields().length; // 总字段数,即数据表中的列数
int pageSize = pageDataSize / totalFields; // 页面大小,即每页记录数
pageHandle(idList, pageSize, dataMapper::batchDeleteData);
return ;
}
Mapper 层
// 依次删除
public void deleteData(String id);
// 批量删除
public void batchDeleteData(List<String> idList);
Controller 层
// 依次删除
@RequestMapping("/data/delete")
public HTTPResult deleteData(String id) throws Exception {
dataService.deleteData(id);
return HTTPResult.success(null);
}
// 批量删除
@RequestMapping("/data/bdelete")
public HTTPResult batchDeleteData(String ids) throws Exception {
List<String> idList = Arrays.asList(ids.split(","));
dataService.batchDeleteData(idList);
return HTTPResult.success(null);
}
// 分页删除
@RequestMapping("/data/pdelete")
public HTTPResult pageDeleteData(String ids) throws Exception {
List<String> idList = Arrays.asList(ids.split(","));
dataService.pageDeleteData(idList);
return HTTPResult.success(null);
}
清空数据
Service 层
// 清空
public void clearData(){
dataMapper.clearData();
return ;
}
Mapper 层
// 清空
public void clearData();
Controller 层
// 清空
@RequestMapping("/data/clear")
public HTTPResult clearData() throws Exception {
dataService.clearData();
return HTTPResult.success(null);
}