---
title: 数据持久化
tags: [数据持久化, MyBatis, MyBatis-Plus]
sidebar_position: 5
slug: /advanced-guide/data-persistance
---
<head>
  <title>盘古开发框架 | 数据持久化 | 自动填充字段 | 逻辑删除 | 乐观锁 | MyBatis Plus</title>
  <meta name="keywords" content="盘古开发框架 | 数据持久化 | 自动填充字段 | 逻辑删除 | 乐观锁 | MyBatis Plus" />
</head>

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

盘古开发框架数据持久化 API 依赖 MyBatis Plus 提供。在 MyBatis 的基础上提供了强大的内置通用 Mapper CRUD 操作、支持 Lambda 表达式、内置屏蔽不通数据库方言差异的分页插件、自动填充字段值、多种主键自动生成策略、逻辑删除、乐观锁插件等。

<details>
  <summary><b>快速 QA：</b>可以支持兼容哪些数据库类型？</summary>
  <div>
  与 MyBatis Plus 一致。支持所有 JDBC 标准数据库。分页方言兼容如下数据库：
  mysql、oracle、db2、h2、hsql、sqlite、postgresql、sqlserver、phoenix、gauss、clickhouse、sybase、oceanbase、firebird、cubrid、goldilocks、csiidb、达梦数据库、虚谷数据库、人大金仓数据库、南大通用(华库)数据库、南大通用数据库、神通数据库、瀚高数据库。
  </div>
</details>

## 安装相关盘古模块

<Tabs>
<TabItem value="parent" label="盘古 Parent">

```jsx
<parent>
	<groupId>com.gitee.pulanos.pangu</groupId>
	<artifactId>pangu-parent</artifactId>
	<version>latest.version.xxx</version>
	<relativePath/>
</parent>
```
</TabItem>
<TabItem value="dependency1" label="基础模块">

```jsx
<dependency>
    <groupId>com.gitee.pulanos.pangu</groupId>
    <artifactId>pangu-spring-boot-starter</artifactId>
</dependency>
```
</TabItem>
<TabItem value="dependency2" label="JDBC 模块">

```jsx
<dependency>
  <groupId>com.gitee.pulanos.pangu</groupId>
  <artifactId>pangu-framework-jdbc-spring-boot-starter</artifactId>
</dependency>
```
</TabItem>
</Tabs>

## 本地配置

> 为便于理解，本文基于本地配置的方式编写。若改为标准的Nacos配置中心模式，请参阅:[配置中心](/docs/advanced-guide/nacos-config-center)章节。

<Tabs defaultValue="application-dev">
<TabItem value="application" label="application.properties">

```jsx
spring.profiles.active=${spring.profiles.active:dev}
```
</TabItem>
<TabItem value="application-dev" label="application-dev.properties">

```jsx
spring.application.name=pangu-examples-crud

spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/pangu-examples?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&failOverReadOnly=false&allowMultiQueries=true&useSSL=false&rewriteBatchedStatements=true
spring.datasource.username=root
spring.datasource.password=root123456
mybatis-plus.mapperLocations=classpath*:/mapper/**/*.xml
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.slf4j.Slf4jImpl
# 分页SQL方言数据库类型标识（缺省：自动识别）
pangu.jdbc.db-type=mysql

logging.level.root=INFO
logging.level.com.gitee.pulanos.pangu=INFO
logging.level.com.gitee.pulanos.pangu.showcases.crud.dao=debug
```
</TabItem>
</Tabs>

## 生成持久化所需的模版代码
基于实践经验，我们建议使用盘古代码生成器，而不是使用 Mybatis Plus 官方生成器。关于生成代码相关内容，请参阅 [《代码生成器插件》](/docs/code-generator) 章节，这里不再赘述。（针对每一个数据表会生成一个 `*Mapper.java` 文件和一个 `*Entity.java` 文件）

## 基本数据持久化操作
 
> 这里只是给出几个简单典型的操作代码，更多内容请参阅范例或阅读 [Mybatis Plus 官方文档](https://baomidou.com/pages/24112f/)。

### 新增
```jsx
UserEntity userEntity = new UserEntity();
userEntity.setName("XC").setAge(18).setUserType("1");
int row = userMapper.insert(userEntity);
log.info("成功插入{}条数据。{}", row, userEntity);
```

### 修改
```jsx
//方式1
userMapper.updateById(new UserEntity().setId(1L).setName("XC2"));

//方式2
LambdaUpdateWrapper<UserEntity> updateWrapper = Wrappers.lambdaUpdate();
updateWrapper.set(UserEntity::getAge, 100);
updateWrapper.eq(UserEntity::getId, 2L);
userMapper.update(null, updateWrapper);
//方式2简写
userMapper.update(null, Wrappers.<UserEntity>lambdaUpdate().set(UserEntity::getName, "XC2").eq(UserEntity::getId, 3L));

//方式3
UserEntity userEntity = new UserEntity();
userEntity.setName("XC2");
userMapper.update(userEntity, Wrappers.<UserEntity>lambdaUpdate().eq(UserEntity::getId, 4L));
```

### 删除
```jsx
//方式1
userMapper.deleteById(1000L);

//方式2
userMapper.deleteBatchIds(Arrays.asList(1000L, 1001L));

//方式3
userMapper.delete(Wrappers.<UserEntity>lambdaQuery().ge(UserEntity::getAge, 150));
userMapper.delete(Wrappers.lambdaQuery(UserEntity.class).ge(UserEntity::getAge, 150));

//方式4
userMapper.deleteById(new UserEntity().setId(2000L));
```

### 简单查询
```jsx
//方式1
UserEntity userEntity = userMapper.selectById(1L);

//方式2
UserEntity userEntity1 = userMapper.selectOne(Wrappers.<UserEntity>lambdaQuery().eq(UserEntity::getId, 1L));

//方式3 (需要注意对传入 ID 集合为非空判断，否则生成的 SQL in() 语法将报错)
if(CollUtil.isNotEmpty(userIds)){
	userEntities = userMapper.selectBatchIds(userIds);
}

//方式4
LambdaQueryWrapper<UserEntity> lambdaQueryWrapper = Wrappers.lambdaQuery();
//动态组合查询条件的简便写法
lambdaQueryWrapper.between(ObjectUtil.isNotEmpty(age), UserEntity::getAge, 1, age);
lambdaQueryWrapper.eq(UserEntity::getUserType, "1");
lambdaQueryWrapper.orderByDesc(UserEntity::getId);
List<UserEntity> userEntities1 = userMapper.selectList(lambdaQueryWrapper);

//方式5
LambdaQueryWrapper<UserRoleEntity> lambdaQueryWrapper = Wrappers.lambdaQuery();
lambdaQueryWrapper.eq(UserRoleEntity::getUserId, userId).orderByDesc(UserRoleEntity::getRoleId);
List<UserRoleEntity> userRoleEntities = userRoleMapper.selectList(lambdaQueryWrapper);
List<Long> roleIds = userRoleEntities.stream().map(UserRoleEntity::getRoleId).collect(Collectors.toList());

//方式6
List<Map<String, Object>> userMaps = userMapper.selectMaps(Wrappers.<UserEntity>lambdaQuery().eq(UserEntity::getUserType, "1"));

//方式7 count 查询
Long cnt = userMapper.selectCount(Wrappers.<UserEntity>lambdaQuery().le(UserEntity::getGmtCreate, DateUtil.date()));

//方式8 group 查询
QueryWrapper<UserEntity> queryWrapper1 = Wrappers.query();
queryWrapper1.select("age, count(id) as cnt").groupBy("age");
List<Map<String, Object>> mapList = userMapper.selectMaps(queryWrapper1);

//方式9 or 查询
if (ObjectUtil.isNotEmpty(keyword)){
	lambdaQueryWrapper.and(w -> w.like( UserEntity::getName, keyword).or().like(UserEntity::getUserName, keyword));
}

//方式10 指定查询字段
LambdaQueryWrapper<UserEntity> lambdaQueryWrapper = Wrappers.lambdaQuery();
lambdaQueryWrapper.select(UserEntity::getId, UserEntity::getUserName);
lambdaQueryWrapper.eq(...);

//方式11 排除查询字段
lambdaQueryWrapper.select(UserEntity.class, info -> !info.getColumn().equals("password") && !info.getColumn().equals("password2"));
```

### 分页查询
盘古框架已经做了 Mybatis Plus 分页插件的自动装配工作，直接使用即可。无需额外配置。

#### 基于 Mapper API 的分页查询

```jsx
public void aPageQuery(){
	log.info("MyBatisPlus API 分页查询数据...");
	Page page = new Page<UserEntity>(1,3);
	userMapper.selectPage(page, Wrappers.<UserEntity>lambdaQuery().ge(UserEntity::getAge, 10).orderByAsc(UserEntity::getId));
	Console.log("总数：{}", page.getTotal());
	List<UserEntity> userEntities = page.getRecords();
	userEntities.forEach(System.out::println);
}
```
#### 基于手工映射 SQL 的分页查询
继续阅读本文后面的 [《手工编写SQL映射》](#手工编写sql映射) 章节可以找到。

#### 一个完整的分页查询例子
```jsx
// 分页入参对象
public class RoleIn extends Page {
    private String name;
}

// Controller
@GetMapping("/list")
public Result<PageResult<RoleEntity>> list(RoleIn roleIn) {
	PageResult<RoleEntity> pageResult = roleService.list(roleIn);
	return Result.success(pageResult);
}

// Service实现
public PageResult<RoleEntity> list(RoleIn roleIn) {
	Page<RoleEntity> page = PagingUtil.createPage(roleIn);
	LambdaQueryWrapper<RoleEntity> lambdaQueryWrapper = Wrappers.lambdaQuery();
	lambdaQueryWrapper.like(ObjectUtil.isNotEmpty(roleIn.getName()), RoleEntity::getName, roleIn.getName());
	roleMapper.selectPage(page, lambdaQueryWrapper);
	return PagingUtil.getPageResult(page);
}
```

### 手工编写SQL映射
:::tip
一般来说在日常开发中我们是不需要再编写SQL代码的，Mapper API已经能满足我们的需求了。但对于一些特殊需求，我们也可以手工写SQL映射的方式来处理。如下范例演示了手工SQL查询的操作，新增、修改、删除也是同理。
:::

```jsx
public void bPageQuery(){
	log.info("自定义SQL映射分页查询数据...");
	Page page = new Page<UserEntity>(1,3);
	Map<String, Object> params = Maps.newHashMap();
	params.put("userType", "1");
	List<UserEntity> userEntities = crudMapper.listUsersByPage(page, params);
	Console.log("总数：{}", page.getTotal());
	userEntities.forEach(System.out::println);
}

public void cPageQuery(){
	log.info("自定义SQL映射分页查询数据...");
	Page page = new Page<Map<String, Object>>(1,3);
	String userType = "1";
	List<Map<String, Object>> userMaps = crudMapper.listUserMapsByPage(page, userType);
	Console.log("总数：{}", page.getTotal());
	userMaps.forEach(System.out::println);
}

public void bSelect() {
	log.info("自定义SQL映射查询数据...");
	Map<String, Object> params = Maps.newHashMap();
	params.put("userType", "1");
	List<UserEntity> userEntities = crudMapper.listUsersByMap(params);
	userEntities.forEach(System.out::println);
}
```

```jsx title="src/main/java/com/gitee/pulanos/pangu/showcases/crud/dao/mapper/CrudMapper.java"
/**
 * 提示：自定义sql手工映射大部分时候为复杂的多表联合查询的SQL，单表操作都应该统一使用mybatis plus的API
 */
@Mapper
public interface CrudMapper {

    List<UserEntity> listUsersByPage(Page<UserEntity> page, Map<String, Object> param);

    List<Map<String, Object>> listUserMapsByPage(Page<Map<String, Object>> page, String userType);

    List<UserEntity> listUsersByMap(Map<String, Object> param);

}
```

```jsx title="src/main/resources/mapper/CrudMapper.xml"
<mapper namespace="com.gitee.pulanos.pangu.showcases.crud.dao.mapper.CrudMapper">
<!-- 分页查询 -->
<select id="listUsersByPage" resultType="com.gitee.pulanos.pangu.showcases.crud.dao.entity.UserEntity">
	select id,name,age,user_type,gmt_create,gmt_update from user
	<where>
		<if test="param.userType!=null and param.userType!=''">
			user_type = #{param.userType}
		</if>
	</where>
	ORDER BY id
</select>

<!-- 分页查询 -->
<select id="listUserMapsByPage" resultType="java.util.Map">
	select age, count(id) as userCnt from user
	<where>
		<if test="userType!=null and userType!=''">
			user_type = #{userType}
		</if>
	</where>
	GROUP BY age
	ORDER BY age DESC
</select>

<!-- 普通查询 -->
<select id="listUsersByMap" resultType="com.gitee.pulanos.pangu.showcases.crud.dao.entity.UserEntity">
	select id,name,age,user_type,gmt_create,gmt_update from user
	<where>
		<if test="userType!=null and userType!=''">
			user_type = #{userType}
		</if>
	</where>
</select>
</mapper>
```

## 高级特性

### 事务
单一数据源事务，直接使用 Spring 事务相关注解即可（ `@EnableTransactionManagement`、`@Transactional` ）。对于分布式事务请参阅 [《分布式事务》](/docs/advanced-guide/distributed-transaction) 章节。

### 逻辑删除
盘古框架借助 Mybatis Plus 对逻辑删除提供了非常友好的支持。通过如下简单配置即可实现从物理删除到逻辑删除在 CRUD 上面的无感体验。
```jsx
# 指定逻辑删除的标识字段
mybatis-plus.global-config.db-config.logic-delete-field=deleted
# 删除前的缺省值
mybatis-plus.global-config.db-config.logic-not-delete-value=0
# 删除后的值
mybatis-plus.global-config.db-config.logic-delete-value=id
```
#### 配置说明
- 逻辑删除字段 deleted 建议通过数据库设置默认值 0。
- 删除后的值建议不用 1，这会带来某字段需要唯一索引时的尴尬问题。因此建议将删除后的值更新为当前记录的 ID 字段值，如上配置所示。这时如果表中某字段需要建立唯一索引，则可以和 deleted 字段一起创建联合唯一索引。
- 查询和更新操作，框架会自动拼接逻辑删除字段作为条件。（AND deleted = 0）

## 本文相关范例源码
- [pangu-examples-crud](https://gitee.com/pulanos/pangu-framework/tree/master/pangu-examples/pangu-examples-crud)：数据持久化参考范例

## 下一步
继续阅读其它章节获取你想要的答案或通过我们的 [开发者社区](/community) 寻求更多帮助。