基于Spring Boot+MyBatis的数据增删改查模板

📚专栏

「Java数据集成」专栏

基础支撑
辅助(可选)
进阶
基础支撑
基础支撑
辅助(可选)
辅助(可选)
辅助(可选)
测试(可选)
《请求和解析》
《依批分增删改查》
《生成代码脚本》
《增删改查模板》
《同异步请求和处理》
《集成模板》
《HTTP请求工具类》
《JSON处理工具类》
《XML处理工具类》
《生成随机数据脚本》

💬相关

本文涉及的模板代码已放在 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 的随机字符串,记录不同方案、不同数据量级下数据写入的耗时。

以下每项耗时数据至少测试三次并取平均值。 此外,测试数据是在较为理想的环境下测试获取的,而在实际使用场景中的数据应该会比测试数据略高。

记录数依次写入耗时批量写入耗时分页写入耗时
10227ms56ms58ms
100019.45s329ms315ms
10000034m29.7s31.26s22.62s

而下图出自一篇研究,观察基于 MyBatis 批量写入数据下平均每条记录的耗时,横坐标是记录数,纵坐标是平均处理时间,它俩关系就像是倒过来的抛物线,类似的情况也可以出现在其他场景。

image-20230216101119466

先记住这个分页处理函数 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;
}

数据处理场景

后文将给出一个示例场景,具体步骤为:

  1. 在数据库 data_base 建立数据表 data_table,含有字段 idfield1field2
  2. 基于 Spring Boot + MyBatis 实现,采用多层架构(Controller 层、Service 层、Mapper层)
  3. 建立 Java 类 Data,含有属性 idattr1attr2
  4. 编写查询函数 selectData(), 作用为查询表数据,对应 SQL 的 SELECT 语句
  5. 编写插入函数 insertData() ,作用为插入表新数据,对应 SQL 的 INSERT INTO 语句
  6. 编写更新函数 updateData() ,作用为更新表旧数据至新数据,对应 SQL 的 UPDATE 语句
  7. 编写插入或更新函数 insertOrUpdateData() ,作用为若不存在重复数据(以主键或唯一键作为判断依据),则插入,若存在,则更新,对应 SQL 的 INSERT INTO ... ON DUPLICATE KEY UPDATE ... 语句
  8. 编写删除函数 deleteData() ,作用为删除表内的某项数据,对应 SQL 的 DELETE 语句
  9. 编写清空函数 clearData() ,作用为清空表原有数据,对应 SQL 的 TRUNCATE 语句
  10. 各个增删改查方法都有依次、批量、分页处理的实现

建立数据表

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
  • useUnicodecharacterEncoding :编码方式,如 trueutf8
  • 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);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值