跳到主要内容

# 数据持久化

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

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

## 安装相关盘古模块​

- 盘古 Parent
- 基础模块
- JDBC 模块

    <parent>
        <groupId>com.gitee.pulanos.pangu</groupId>
        <artifactId>pangu-parent</artifactId>
        <version>latest.version.xxx</version>
        <relativePath/>
    </parent>

    <dependency>
        <groupId>com.gitee.pulanos.pangu</groupId>
        <artifactId>pangu-spring-boot-starter</artifactId>
    </dependency>

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

## 本地配置​

> 为便于理解，本文基于本地配置的方式编写。若改为标准的Nacos配置中心模式，请参阅:配置中心章节。

- application.properties
- application-dev.properties

    spring.profiles.active=${spring.profiles.active:dev}

    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

## 生成持久化所需的模版代码​

基于实践经验，我们建议使用盘古代码生成器，而不是使用 Mybatis Plus 官方生成器。关于生成代码相关内容，请参阅 《代码生成器插件》 章节，这里不再赘述。（针对每一个数据表会生成一个 `*Mapper.java` 文件和一个 `*Entity.java` 文件）

## 基本数据持久化操作​

> 这里只是给出几个简单典型的操作代码，更多内容请参阅范例或阅读 Mybatis Plus 官方文档。

### 新增​

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

### 修改​

    //方式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));

### 删除​

    //方式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));

### 简单查询​

    //方式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 的分页查询​

    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映射》 章节可以找到。

#### 一个完整的分页查询例子​

    // 分页入参对象
    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映射​

提示

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

    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);
    }

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);

    }

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` ）。对于分布式事务请参阅 《分布式事务》 章节。

### 逻辑删除​

盘古框架借助 Mybatis Plus 对逻辑删除提供了非常友好的支持。通过如下简单配置即可实现从物理删除到逻辑删除在 CRUD 上面的无感体验。

    # 指定逻辑删除的标识字段
    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：数据持久化参考范例

## 下一步​

继续阅读其它章节获取你想要的答案或通过我们的 开发者社区 寻求更多帮助。