芝法酱躺平攻略(2)——SpringBoot下JDBC,mybatis,JPA,mybatis-plus对比与技术选型

SpringBoot下JDBC,mybatis,JPA,mybatis-plus对比与技术选型

  • 一、背景与目标
    • 1.1 常见的Java-web数据库操作方式
    • 1.2 在实际工作中,对数据库的需求有哪些
      • 1.2.1 防止注入
      • 1.2.2 事物出错回滚
      • 1.2.3 让代码和sql分离,让sql编写更容易
      • 1.2.4 基本的审计功能
      • 1.2.5 假删除
      • 1.2.7 乐观锁
      • 1.2.8 样例Entity
      • 1.2.9 测试接口
      • 1.2.10 数据生成与service共同类
  • 二、JDBC实现
    • 2.1 JDBC的maven引用
    • 2.2 JDBC实现代码片段
  • 三、mybatis实现
    • 3.1 JDBC的弊端
    • 3.2 mybatis的动态sql
      • 3.2.1 官网
    • 3.3 mybatis的maven引用和yml配置
    • 3.4 mybatis的xml配置
    • 3.5 mybatis的代码实现
  • 四、JPA实现
    • 4.1 mybatis的问题
    • 4.2 JPA的简单介绍
      • 4.2.1 CrudRepository
    • 4.3 querydsl的简单介绍
    • 4.4 querydsl代码生成以及配置
    • 4.5 JPA中,如何处理审计、假删除和乐观锁
      • 4.5.1 审计
      • 4.5.2 假删除
      • 4.5.3 乐观锁
    • 4.6 代码实现
      • 4.6.1 JPA的maven pom的dependency和 querydsl配置
      • 4.6.2 处理审计的handler
      • 4.6.3 处JPA下使用雪花算法
      • 4.6.4 处理假删除的AOP
      • 4.6.5 Reposity
      • 4.6.6 Service
      • 4.6.7 启动类需要的注解
  • 五、mybatis-plus实现
    • 5.1 为什么会出现mybatis-plus
    • 5.2 mybatis-plus的代码生成
    • 5.3 mybatis-plus的wrapper模式
    • 5.4 mybatis-plus提供的方便service
    • 5.5 mybatis-plus的填充
    • 5.6 mybatis-plus的假删除、乐观锁
    • 5.7 代码实现
    • 5.7.1 mapper
    • 5.7.2 dbService
    • 5.7.3 service
  • 六、对比与总结
    • 6.1 技术适用场景
      • 6.1.1 JDBC的适用场景
      • 6.1.2 Mybatis的适用场景
      • 6.1.3 JPA的适用场景
      • 6.1.4 mybatis-plus的适用场景
    • 6.2 技术对比

一、背景与目标

1.1 常见的Java-web数据库操作方式

提到Java-web,我们首先想到的连接数据库,对数据库表做一些增删改查的业务。常见的数据库操作方式主要有4种,一种是JDBC操作数据库,一种是SpringBoot官方推荐的JPA方式操作数据库,一种是mybatis方式操作数据库,还有一种是mybatis-plus的操作方式。

1.2 在实际工作中,对数据库的需求有哪些

1.2.1 防止注入

我们常常会听到注入攻击这种手段。所谓注入攻击,就是一些不法分子,通过分析服务器接口,在前端请求参数中加入sql相关的语句,使后台程序运行时,产生违背设计初衷的结果。
举个简单的栗子,比如一个用户所在部门节点为aa,级联字段为-A-b-aa。程序接口希望查询部门下,年龄大于35的的程序员。前端参数有2个字段,一个是major,一个是age。部门级联字段放在用户的token中,无法更改。正常情况下语句如下:

select * from auth_staff where dep_cast_id like "%-A-b-aa" and age > 35 and major = "cs"

但如果某同学使用postman,把major字段改为 1" or “1”="1,在不做特殊处理时,生成的sql会是这样

select * from auth_staff where dep_cast_id like "%-A-b-aa" and age > 35 and major = "1" or "1"="1"

画面突然滑稽了起来,公司所有年龄大于35岁的程序员就都被查到了。

1.2.2 事物出错回滚

我们在开发时,程序难免发生各种异常,比如空指针什么的。但如果我们写的接口是一个插入数据或修改数据的功能,且一个接口中有多条插入或修改的语句。只有所有的语句都被执行时,业务才是正确的;否则数据将会错乱,需要人工修改。这时,如果中间某句话发生了空指针,偶买噶,可以删库跑路了~~
幸好,Spring框架,给我们提供了简单的处理方法,在Service上加上这段代码,当出现错误时程序将会回滚,也就很难发生数据库错乱的问题了:

	@Transactional(rollbackFor = Exception.class)
    @Override
    public void someFun() {
        //巴拉巴拉
    }

1.2.3 让代码和sql分离,让sql编写更容易

在开发中,我们并不希望代码中大量出现sql语句。一方面,这样做使应用与数据库的耦合度太高,如果想换一个数据库,就需要整篇改动代码。另一方面,手动编写sql,也很容易出现错误。然而,最重要的是,大多数业务只需要编写个增删改查的接口,这些sql都是重复的机械劳动,真的好烦的。
一个好的ORM,应该可以让开发者大多数时候不必亲手编写sql,把精力聚焦在具体业务逻辑的开发上来。

1.2.4 基本的审计功能

在实际开发中,数据库表中的数据,通常不仅仅存业务相关的数据,还要存放一些审计数据,以给予系统最基本的追踪功能。常见的审计字段包括,什么时候创建的,创建者的id,创建者的名字,创建者的ip,什么时候修改的,修改者的id,修改者的名字,修改者的ip。

1.2.5 假删除

通常情况下,在删除时,尤其是主表数据,通常不希望真的删除。会在表中增加一个del字段,为1则是删除,为0则是正常。在查询时,要过滤掉del为1的数据。

1.2.7 乐观锁

web应用,常常会遇到这种问题。当两个人同时操作1条数据时,就会发生相互覆盖的问题。举个简单的例子:
某个名叫小可爱的数据实体,被戳一下能量就会+1,当能量达到5时就会释放洪荒之力。我们代码可能会这样写:

    public void touch(Long pId){
        XiaoKeAi xiaokeai = mXiaoKeAiService.getById(pId);
        Long playerId = TokenUtil.getTokenObject().getId();
        int energy = XiaoKeAi.setEnegergy();
        if(energy + 1 > 5){
            sendMsg(MsgType.LAUNCH_HONG_HUANG,xiaokeai,playerId);
            energy = 0;
        }else{
            // 本过程十分耗时
            handleChuoChuo(xiaokeai,playerId);
            energy = energy + 1;
        }
        XiaoKeAi.setEnegergy(energy);
        mXiaoKeAiService.save(energy);
    }

大家试想,如果小可爱的能量在4时,有两个玩家在很近的时间一起发起戳戳的请求。emm,小可爱的洪荒之力就要连续爆发2遍,真是可怜。如果需求再改改,除了戳戳,小可爱还唱歌,每唱一次歌就会降低一点能量。如果两个玩家同时分别发起戳戳和点歌的需求,又会发生什么呢?
为了解决这种问题,我们会在小可爱身上加一个version字段,每进行一次操作,version字段就+1。在进行更新时,添加where version = current_version。这样,让后来的玩家请求失败,至少避免了两者相互覆盖数据错乱的悲剧。
这种乐观锁,通常用于对同一实体的并发概率不是很高的情况下。如果同一实体的操作频繁并发,比如投票系统,使用Redison这一类的可重入悲观锁更好些。

1.2.8 样例Entity

本篇的目的是讲解SpringBoot下常见的4重ORM的用法,然后做对比。为方便起见,本小节给出数据库操作的样例Entity
嗯,可能有点长,因为把4种ORM用到的注解都放上了。。。

@EntityListeners(AuditingEntityListener.class)
@Entity   //表示这个类是一个实体类
@Table(name = "c2_orm_test")
@TableName("c2_orm_test")
@Data
public class OrmTestEntity {


    @Column(name = "id")
    @GeneratedValue(generator = "orderIdGenerator", strategy = GenerationType.SEQUENCE)
    @GenericGenerator(name = "orderIdGenerator", strategy = "indi.zhifa.recipe.bailan.busy.handler.SnowIdGenerator")
    @Id
    @Schema(title = "主键")
    @TableId(type = IdType.ASSIGN_ID)
    Long id;
    String appCode;
    String appName;
    String moduleCode;
    String moduleName;
    String code;
    String name;
    EOrmEntityStatus status;
    Integer value;
    String description;
    @JSONField(serialize = false, deserialize = false)
    @TableField(fill = FieldFill.INSERT)
    @Schema(title = "创建时间")
    @CreatedDate
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @QueryType(PropertyType.NONE)
    LocalDateTime createTime;
    @JSONField(serialize = false, deserialize = false)
    @TableField(fill = FieldFill.INSERT)
    @Schema(title = "创建用户Id")
    @QueryType(PropertyType.NONE)
    Long createBy;
    @JSONField(serialize = false, deserialize = false)
    @TableField(fill = FieldFill.INSERT)
    @Schema(title = "创建用户昵称")
    @QueryType(PropertyType.NONE)
    String createName;
    @JSONField(serialize = false, deserialize = false)
    @TableField(fill = FieldFill.INSERT)
    @Schema(title = "创建者Ip")
    @QueryType(PropertyType.NONE)
    String createIp;
    @JSONField(serialize = false, deserialize = false)
    @TableField(fill = FieldFill.UPDATE)
    @Schema(title = "修改时间")
    @LastModifiedDate
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @QueryType(PropertyType.NONE)
    LocalDateTime modifyTime;
    @JSONField(serialize = false, deserialize = false)
    @TableField(fill = FieldFill.UPDATE)
    @Schema(title = "修改用户Id")
    @QueryType(PropertyType.NONE)
    Long modifyBy;
    @JSONField(serialize = false, deserialize = false)
    @TableField(fill = FieldFill.UPDATE)
    @Schema(title = "修改用户昵称")
    @QueryType(PropertyType.NONE)
    String modifyName;
    @JSONField(serialize = false, deserialize = false)
    @TableField(fill = FieldFill.UPDATE)
    @Schema(title = "创建者Ip")
    @QueryType(PropertyType.NONE)
    String modifyIp;

    @Schema(title = "版本")
    @com.baomidou.mybatisplus.annotation.Version
    @Version
    Integer version;
    @JSONField(serialize = false, deserialize = false)
    @TableLogic
    @Schema(title = "是否删除")
    Boolean del;

    public void initWithCreate(){
        id = SnowflakeIdWorker.generateId();
        createTime = LocalDateTime.now();
        createBy = 0L;
        createName = "sys";
        createIp = "localhost";
        modifyTime = null;
        modifyBy = null;
        modifyName = null;
        modifyIp = null;
        version = 0;
        del = false;
    }
    public Object[] toParamList(){
        return Arrays.asList(
                id, appCode, appName, moduleCode, moduleName, code, name, status.getCode(), description, value,
                createTime, createBy, createName, createIp, modifyTime, modifyBy, modifyName, modifyIp, version, del?1:0).toArray();
    }
}

这里的枚举类:

@RequiredArgsConstructor
public enum EOrmEntityStatus {

    DEFAULT(0,"DEFAULT"),
    STATUS_1(1,"STATUS1"),
    STATUS_2(2,"STATUS2"),
    STATUS_3(3,"STATUS3"),
    STATUS_4(4,"STATUS4"),
    STATUS_5(5,"STATUS5")
    ;

    @EnumValue
    @Getter
    final Integer code;
    @Getter
    final String name;
}

1.2.9 测试接口

由于篇幅限制,在这里我们对于每种ORM只做两件事,一件是生成一批数据,插入数据库。一个是分页查询。
controller接口类如以下代码:

@Api(tags = "ORM测试")
@RequestMapping("/api/ormTest")
@Slf4j
@ZfRestController
@RequiredArgsConstructor
public class OrmTestApi {

    private final IOrmJDBCService mOrmService;
    private final IOrmMybatisService mOrmMybatisService;
    private final IOrmJpaService mOrmJpaService;

    private final IOrmMybatisPlusService mOrmMybatisPlusService;

    @Operation(summary = "JDBC创建测试数据")
    @GetMapping("/jdbc/init")
    public String jdbcInit(){
        mOrmService.initTestData();
        return "初始化数据成功";
    }

    @Operation(summary = "JDBC分页查询")
    @GetMapping("/jdbc/page")
    public PageData<OrmTestEntity> jdbcPage(
            @Parameter(description = "当前页") @RequestParam(name = "current") int pCurrent,
            @Parameter(description = "页大小") @RequestParam(name = "size") int pSize,
            @Parameter(description = "应用码") @RequestParam(name = "appCode",required = false) String pAppCode,
            @Parameter(description = "模块码") @RequestParam(name = "moduleCode",required = false) String pModuleCode,
            @Parameter(description = "主码") @RequestParam(name = "code",required = false) String pCode,
            @Parameter(description = "名字") @RequestParam(name = "name",required = false) String pName,
            @Parameter(description = "状态") @RequestParam(name = "status",required = false) EOrmEntityStatus pStatus,
            @Parameter(description = "最小值") @RequestParam(name = "valueMin",required = false) Integer pMin,
            @Parameter(description = "最大值") @RequestParam(name = "valueMax",required = false) Integer pMax) {

        PageData<OrmTestEntity> pageData = mOrmService.page(pCurrent,pSize,pAppCode,pModuleCode,pCode,pName,pStatus,pMin,pMax);
        return pageData;
    }

    @Operation(summary = "mybatis创建测试数据")
    @GetMapping("/mybatis/init")
    public String mybatisInit(){
        mOrmMybatisService.initTestData();
        return "初始化数据成功";
    }

    @Operation(summary = "mybatis分页查询")
    @GetMapping("/mybatis/page")
    public PageData<OrmTestEntity> mybatisPage(
            @Parameter(description = "当前页") @RequestParam(name = "current") int pCurrent,
            @Parameter(description = "页大小") @RequestParam(name = "size") int pSize,
            @Parameter(description = "应用码") @RequestParam(name = "appCode",required = false) String pAppCode,
            @Parameter(description = "模块码") @RequestParam(name = "moduleCode",required = false) String pModuleCode,
            @Parameter(description = "主码") @RequestParam(name = "code",required = false) String pCode,
            @Parameter(description = "名字") @RequestParam(name = "name",required = false) String pName,
            @Parameter(description = "状态") @RequestParam(name = "status",required = false) EOrmEntityStatus pStatus,
            @Parameter(description = "最小值") @RequestParam(name = "valueMin",required = false) Integer pMin,
            @Parameter(description = "最大值") @RequestParam(name = "valueMax",required = false) Integer pMax) {

        PageData<OrmTestEntity> pageData = mOrmMybatisService.page(pCurrent,pSize,pAppCode,pModuleCode,pCode,pName,pStatus,pMin,pMax);
        return pageData;
    }

    @Operation(summary = "JPA创建测试数据")
    @GetMapping("/jpa/init")
    public String jpaInit(){
        mOrmJpaService.initTestData();
        return "初始化数据成功";
    }

    @JpaQsdl
    @Operation(summary = "jpa分页查询")
    @GetMapping("/jpa/page")
    public Page<OrmTestEntity> jpaPage(
            @QuerydslPredicate(root = OrmTestEntity.class) Predicate predicate, Pageable page) {
        Page<OrmTestEntity> pageData = mOrmJpaService.page(page,predicate);
        return pageData;
    }

    @Operation(summary = "mybatis-plu创建测试数据")
    @GetMapping("/mp/init")
    public String mpInit(){
        mOrmMybatisPlusService.initTestData();
        return "初始化数据成功";
    }

    @JpaQsdl
    @Operation(summary = "mybatis-plu分页查询")
    @GetMapping("/mp/page")
    public com.baomidou.mybatisplus.extension.plugins.pagination.Page<OrmTestEntity> mpPage(
            @Parameter(description = "当前页") @RequestParam(name = "current") int pCurrent,
            @Parameter(description = "页大小") @RequestParam(name = "size") int pSize,
            @Parameter(description = "应用码") @RequestParam(name = "appCode",required = false) String pAppCode,
            @Parameter(description = "模块码") @RequestParam(name = "moduleCode",required = false) String pModuleCode,
            @Parameter(description = "主码") @RequestParam(name = "code",required = false) String pCode,
            @Parameter(description = "名字") @RequestParam(name = "name",required = false) String pName,
            @Parameter(description = "状态") @RequestParam(name = "status",required = false) EOrmEntityStatus pStatus,
            @Parameter(description = "最小值") @RequestParam(name = "valueMin",required = false) Integer pMin,
            @Parameter(description = "最大值") @RequestParam(name = "valueMax",required = false) Integer pMax) {
        com.baomidou.mybatisplus.extension.plugins.pagination.Page<OrmTestEntity> pageData = mOrmMybatisPlusService.page(pCurrent,pSize,pAppCode,pModuleCode,pCode,pName,pStatus,pMin,pMax);
        return pageData;
    }

}

1.2.10 数据生成与service共同类

由于各Service都需要生成数据,故把这部分代码提取出来。

public class BaseOrmServiceImpl {
    protected List<OrmTestEntity> getInitOrmTestEntityList(String pPrefix) {

        final int appCnt = 3;
        final int[] moduleCnts = new int[]{2,3,5};

        final int statusCnt = EOrmEntityStatus.values().length - 1;
        final int[] entityCnts = new int[]{8,3,5,2,1};

        final int valueMin = 1;
        final int valueMax = 100;

        int appNo = 0;
        int moduleNo = 0;
        int entityNo = 0;

        List<OrmTestEntity> ormTestEntityList = new ArrayList<>();
        for(int appIdx=0;appIdx<appCnt;appIdx++){
            int moduleCnt = moduleCnts[appIdx];
            appNo++;
            String appCode = pPrefix+"_app_"+(appIdx+1);
            String appName = pPrefix+"-应用-"+appNo;
            for (int moduleIdx=0;moduleIdx<moduleCnt;moduleIdx++){
                moduleNo++;
                String moduleCode = pPrefix+"_mode_"+(moduleIdx+1);
                String moduleName = pPrefix+"-模块-"+moduleNo;
                for(int statusIdx = 0;statusIdx<statusCnt;statusIdx++){
                    int entityCnt = entityCnts[statusIdx];
                    for(int i=0;i<entityCnt;i++){
                        entityNo++;
                        String entityCode = pPrefix+"_entity_"+entityNo;
                        String entityName = pPrefix+"-ORM测试实体-"+entityNo;
                        String description = "这个是"+entityName+"。是"+appName+"下的"+moduleName+"的一个应用。由"+pPrefix+"创建";
                        int status = statusIdx+1;
                        int value = RandomUtil.randomInt(valueMin,valueMax);
                        OrmTestEntity ormTestEntity = new OrmTestEntity();
                        ormTestEntity.initWithCreate();
                        ormTestEntity.setAppCode(appCode);
                        ormTestEntity.setAppName(appName);
                        ormTestEntity.setModuleCode(moduleCode);
                        ormTestEntity.setModuleName(moduleName);
                        ormTestEntity.setCode(entityCode);
                        ormTestEntity.setName(entityName);
                        ormTestEntity.setDescription(description);
                        ormTestEntity.setStatus(EOrmEntityStatus.values()[statusIdx+1]);
                        ormTestEntity.setValue(value);
                        ormTestEntityList.add(ormTestEntity);
                    }
                }
            }
        }
        return ormTestEntityList;
    }
}

二、JDBC实现

在这一节,我们介绍JDBC的实现。所谓JDBC,其实就是帮助程序员执行sql的一套ORM框架。
JDBC可以通过sql参数化,解决1.2.1中所讲的防注入问题。
这一部分没有太多好讲的,小编打算直接放出代码供大家参考比对。

2.1 JDBC的maven引用

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-jdbcartifactId>
        dependency>
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
        dependency>

2.2 JDBC实现代码片段

@RequiredArgsConstructor
@Service
public class OrmJDBCServiceImpl extends BaseOrmServiceImpl implements IOrmJDBCService {

    private final JdbcTemplate mJdbcTemplate;

    enum EParamType{
        EQ,
        LIKE,
        BIG,
        LESS,
        BETWEEN;
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void initTestData() {
        List<OrmTestEntity> ormTestEntityList = getInitOrmTestEntityList("JDBC");
        String sql = "insert into c2_orm_test("+
                "id,app_code,app_name,module_code,module_name,code,name,status,description,value,"+
                "create_time,create_by,create_name,create_ip,modify_time,modify_by,modify_name,modify_ip,version,del) values(?,?,?,?,?,?,?,?,?,?," +
                "?,?,?,?,?,?,?,?,?,?)";
        List<Object[]> params = ormTestEntityList.stream().map(entity->entity.toParamList()).collect(Collectors.toList());
        mJdbcTemplate.batchUpdate(sql,params);
    }

    @Override
    public PageData<OrmTestEntity> page(int pCurrent, int pSize,
                                              String pAppCode, String pModuleCode, String pCode, String pName,
                                              EOrmEntityStatus pStatus, Integer pMin, Integer pMax) {
        StringBuilder sb = new StringBuilder();
        sb.append("select * from c2_orm_test");
        StringBuilder whereSb = new StringBuilder().append(" ");
        List<Object> params = new ArrayList<>();
        Boolean hasParam = false;
        addParam(whereSb,hasParam,params,"del",EParamType.EQ,0,null);
        addParam(whereSb,hasParam,params,"app_code",EParamType.EQ,pAppCode,null);
        addParam(whereSb,hasParam,params,"module_code",EParamType.EQ,pModuleCode,null);
        addParam(whereSb,hasParam,params,"code",EParamType.EQ,pCode,null);
        addParam(whereSb,hasParam,params,"name",EParamType.LIKE,pName,null);
        addParam(whereSb,hasParam,params,"status",EParamType.EQ,null !=pStatus ? pStatus.getCode(): null,null);
        if(null != pMin && null != pMax){
            addParam(whereSb,hasParam,params,"value",EParamType.BETWEEN,pMin,pMax);
        }else if(null != pMin){
            addParam(whereSb,hasParam,params,"value",EParamType.BIG,pMin,null);
        }else if(null != pMax){
            addParam(whereSb,hasParam,params,"value",EParamType.LESS,pMax,null);
        }
        sb.append(whereSb);
        PageData<OrmTestEntity> pageData = new PageData<OrmTestEntity>();
        pageData.setCurrent(pCurrent);
        pageData.setSize(pSize);
        String cntSql = String.format("select count(*) as cnt from (%s) as tmp", sb);
        Map<String,Object> cntRes = mJdbcTemplate.queryForMap(cntSql,params.toArray());
        int cnt = ((Long)cntRes.get("cnt")).intValue();
        if(cnt > 0){
            if(pSize > 0){
                sb.append(" LIMIT ").append((pCurrent-1) * pSize).append(", ").append(pSize);
            }
            List<Map<String, Object>> mapData = mJdbcTemplate.queryForList(sb.toString(),params.toArray());
            List<OrmTestEntity> data = mapData.stream().map(theMapData->
                    BeanUtil.mapToBean(theMapData,OrmTestEntity.class,true, CopyOptions.create())
            ).collect(Collectors.toList());
            pageData.setTotal(cnt);
            int pages = (int)Math.ceil((double)cnt/pSize);
            pageData.setPages(pages);
            pageData.setPageData(data);
        }else{
            pageData.setTotal(0);
            pageData.setPages(0);
        }

        return pageData;
    }

    private void addParam(StringBuilder pParamSb, Boolean pHasParam, List<Object> pParams, String pParamStr, EParamType pParamType, Object pParam1, Object pParam2){

        if(null != pParam1){
            if(pHasParam){
                pParamSb.append(" and ");
            }else{
                pParamSb.append("where ");
            }
            pHasParam = true;
            switch (pParamType){
                case EQ:
                    pParamSb.append(pParamStr + " = ?");
                    pParams.add(pParam1);
                    break;
                case LIKE:
                    pParamSb.append(pParamStr + " like ?");
                    pParams.add("%"+pParam1+"%");
                    break;
                case BIG:
                    pParamSb.append(pParamStr + " > ?");
                    pParams.add(pParam1);
                    break;
                case LESS:
                    pParamSb.append(pParamStr + " < ?");
                    pParams.add(pParam1);
                    break;
                case BETWEEN:
                    pParamSb.append(pParamStr + " between ? and ?");
                    pParams.add(pParam1);
                    pParams.add(pParam2);
                    break;
            }
        }
    }
}

三、mybatis实现

3.1 JDBC的弊端

我们从二可以看到,JDBC有个明显的弊端,就是sql的拼接都放到了代码里,这使得代码与数据库完全耦合。
另一方面,由于代码拼sql,使很多逻辑变得不易读,也就难以维护了。
所以,我们就会想,有没有办法可以动态生成sql呢,mybatis就呼之欲出了。

3.2 mybatis的动态sql

3.2.1 官网

学习任何一个新的技术框架,我都推荐大家先去官网上看看。
本篇用到技术点有3个,Handling Enums;Result Maps;Dynamic SQL

3.3 mybatis的maven引用和yml配置

由于小编的代码框架的ORM接入的mybatis-plus,所以引用mybatis-plus的就可以了

<dependency>
     <groupId>com.baomidougroupId>
     <artifactId>mybatis-plus-boot-starterartifactId>
dependency>
mybatis-plus:
  typeEnumsPackage: indi.zhifa.recipe.bailan.busy.enums
  mapper-locations: classpath*:mapping/**/*Mapper.xml

3.4 mybatis的xml配置


DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="indi.zhifa.recipe.bailan.busy.dao.mapper.OrmTestMybatisMapper">
    <resultMap id="OrmTestEntity" type="indi.zhifa.recipe.bailan.busy.entity.po.OrmTestEntity">
        <id column="id" property="id"/>
        <result property="appCode" column = "app_code"/>
        <result property="appName" column = "app_name"/>
        <result property="moduleCode" column = "module_code"/>
        <result property="moduleName" column = "module_name"/>
        <result property="code" column = "code"/>
        <result property="name" column = "name"/>
        <result property="status" column = "status" javaType="indi.zhifa.recipe.bailan.busy.enums.EOrmEntityStatus" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
        <result property="value" column = "value"/>
        <result property="description" column = "description"/>
        <result property="createTime" column = "createTime"/>
        <result property="createBy" column = "createBy"/>
        <result property="createName" column = "createName"/>
        <result property="createIp" column = "createIp"/>
        <result property="modifyTime" column = "modifyTime"/>
        <result property="modifyBy" column = "modifyBy"/>
        <result property="modifyName" column = "modifyName"/>
        <result property="modifyIp" column = "modifyIp"/>
        <result property="version" column = "version"/>
        <result property="del" column = "del"/>
    resultMap>

    <insert id="saveBatch" parameterType="java.util.List">
        INSERT INTO c2_orm_test
        (id,app_code,app_name,module_code,module_name,code,name,status,description,value,create_time,create_by,create_name,create_ip,modify_time,modify_by,modify_name,modify_ip,version,del)
        VALUES
        <foreach collection="pOrmTestEntityList" item="ormTestEntity" separator=",">
            (#{ormTestEntity.id},#{ormTestEntity.appCode},#{ormTestEntity.appName},#{ormTestEntity.moduleCode},#{ormTestEntity.moduleName},#{ormTestEntity.code},#{ormTestEntity.name},#{ormTestEntity.status},#{ormTestEntity.description},#{ormTestEntity.value},#{ormTestEntity.createTime},#{ormTestEntity.createBy},#{ormTestEntity.createName},#{ormTestEntity.createIp},#{ormTestEntity.modifyTime},#{ormTestEntity.modifyBy},#{ormTestEntity.modifyName},#{ormTestEntity.modifyIp},#{ormTestEntity.version},#{ormTestEntity.del})
        foreach>
    insert>

    <select id="page" resultMap="OrmTestEntity">
        <bind name="name_like" value="'%' + _parameter.name + '%'" />
        select * from c2_orm_test
        <where>
            del = 0
            <if test="appCode != null">
                and app_code = #{appCode}
            if>
            <if test="moduleCode != null">
                and module_code = #{moduleCode}
            if>
            <if test="code != null">
                and code = #{code}
            if>
            <if test="name != null">
                and name like #{name_like}
            if>
            <if test="status != null">
                and status = #{status}
            if>
            <if test="min != null and max != null">
                and `value` between #{min} and #{max}
            if>
            <if test="min != null and max == null">
                and `value` > #{min}
            if>
            <if test="min == null and max != null">
                and `value`  #{max}
            if>
        where>
    select>

mapper>

3.5 mybatis的代码实现

看到mybatis的代码部分,是否有种豁然开朗的感觉,真的太干净了。

@Data
public class PageData<T> {
    int total;
    int pages;
    int current;
    int size;
    List<T> pageData;
}
@RequiredArgsConstructor
@Service
public class OrmMybatisServiceImpl extends BaseOrmServiceImpl implements IOrmMybatisService {

    private final OrmTestMybatisMapper mOrmTestMybatisMapper;

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void initTestData() {
        List<OrmTestEntity> iniOrmTestEntityList = getInitOrmTestEntityList("mybatis");
        mOrmTestMybatisMapper.saveBatch(iniOrmTestEntityList);
    }

    @Override
    public PageData<OrmTestEntity> page(int pCurrent, int pSize,
                                        String pAppCode, String pModuleCode, String pCode, String pName,
                                        EOrmEntityStatus pStatus, Integer pMin, Integer pMax) {
        List<OrmTestEntity> OrmTestEntityList = mOrmTestMybatisMapper.page(pCurrent,pSize,
                pAppCode,pModuleCode,pCode,pName,
                null !=pStatus ? pStatus.getCode():null,pMin,pMax);
        PageData<OrmTestEntity> data = new PageData<>();
        data.setCurrent(pCurrent);
        data.setSize(pSize);
        data.setPageData(OrmTestEntityList);
        return data;
    }

}

四、JPA实现

4.1 mybatis的问题

我们可以看到,mybatis虽然灵活,但对于一些简单的page查询,仍然需要配置大量的xml,拖慢了开发速度。
同时,使用mybatis就强制要求程序员数量掌握sql的语法,这样在招收新人时就增加了一些成本。
另外,手动配置xml的sql,也难免会发生错误,有时可能因为手滑写出一些难以发现调试的奇怪BUG,这也增加了开发成本。

4.2 JPA的简单介绍

于是,JPA就呼之欲出。试想,如果程序员只需要和产品对接,设计好数据库。用类似freemark的模板技术,把数据库对应的实体生成出来。实体字段和数据库字段以一定规则对应。一些简单的page,findByXxx,updateById,removeById等操作,不需要程序员做额外的配置,只简单的调用一下接口,岂不妙哉。
JPA正式这样一种ORM,程序员只需要在生成的Entity上加上一些注解,搞一个Reposity接口继承CrudRepository、QuerydslPredicateExecutor,QuerydslBinderCustomizer。在Service中注入一下,就可以简单使用了。

4.2.1 CrudRepository

CrudRepository 是JPA的一个基础查询接口,他提供了诸如save、saveAll、findById、findAllById、deleteById等基础的接口实现。如果想要做其他查询,只需要在自己写的Repository中,按一定规则声明接口(不必自己写实现),就可以生效了。
举个例子,拿我们的1.2.8的样例Entity来说,如果想实现查找所有状态是EOrmEntityStatus.STATUS_2 并且value在10~20的所有实体,只需要这样就可以了:

public interface OrmTestReposity extends CrudRepository<OrmTestEntity,Long>, QuerydslPredicateExecutor<OrmTestEntity>, QuerydslBinderCustomizer<QOrmTestEntity> {
    List<OrmTestEntity> findAllByStatusAndValueBetween(EOrmEntityStatus status, Integer min, Integer max);
    }
}

具体语法规则,可以参见官网

4.3 querydsl的简单介绍

有些时候,可能Repository的接口没什么共通性,在Service里只用1次,我们不想在Repository中定义,那该怎么做呢?
这时,QuerydslPredicateExecutor。在这里我不想做过多讲解,还是拿4.2的例子,在这里实现一遍。

    public List<OrmTestEntity> listByStatusAndValueBetween(EOrmEntityStatus pOrmEntityStatus, Integer pMin, Integer pMax){
        BooleanExpression b2 = QOrmTestEntity.ormTestEntity.value.between(pMin,pMax);
        BooleanExpression b1 = QOrmTestEntity.ormTestEntity.status.eq(pOrmEntityStatus).and(b2);
        List<OrmTestEntity> ormTestEntityList = Lists.newArrayList(mOrmTestReposity.findAll(b1));
        return ormTestEntityList;
    }

那么,对于page接口呢?理论上我们也可以用这种方式写。但又没有更省事的方法呢?答案是有的。
querydsl提供了一种自动绑定的功能,前端可以根据Entity的属性名自由的传递条件,Java部分代码如下:
Service:

@RequiredArgsConstructor
@Service
public class OrmJpaServiceImpl extends BaseOrmServiceImpl implements IOrmJpaService {

    private final OrmTestReposity mOrmTestReposity;

    @Override
    public Page<OrmTestEntity> page(Pageable pPageable, Predicate predicate) {
        Page<OrmTestEntity> ormTestEntityPage =  mOrmTestReposity.findAll(predicate,pPageable);
        return ormTestEntityPage;
    }

}

controller:
@Operation(summary = “jpa分页查询”)
@GetMapping(“/jpa/page”)
public Page jpaPage(
@QuerydslPredicate(root = OrmTestEntity.class) Predicate predicate, Pageable page) {
Page pageData = mOrmJpaService.page(page,predicate);
return pageData;
}
repository:

public interface OrmTestReposity extends CrudRepository<OrmTestEntity,Long>, QuerydslPredicateExecutor<OrmTestEntity>, QuerydslBinderCustomizer<QOrmTestEntity> {
    @Override
    default void customize(QuerydslBindings bindings, QOrmTestEntity pQOrmTestEntity){
        // name 使用like
        bindings.bind(pQOrmTestEntity.name).first((path,value)->path.contains(value));
        bindings.bind(pQOrmTestEntity.appName).first((path,value)->path.contains(value));
        bindings.bind(pQOrmTestEntity.moduleName).first((path,value)->path.contains(value));
        bindings.bind(pQOrmTestEntity.description).first((path,value)->path.contains(value));
        bindings.bind(pQOrmTestEntity.value).all((path,value) -> {
            Iterator<? extends Integer> it = value.iterator();
            Integer from = it.next();
            if (value.size() >= 2) {
                Integer to = it.next();
                return Optional.of(path.between(from, to)); // between
            } else {
                return Optional.of(path.goe(from)); // greater or equal
            }
        });
    }
}

如果不重载customize,默认传入的所有参数都是以=的方式解析。
对于前端访问来说,可以这样:
http://localhost:8081/api/ormTest/jpa/page?page=1&size=10&name=JPA&value=10&value=30&status=STATUS_1
这段访问的意思是,查询name like “JPA”, 状态处于STATUS_1(数据库中存的是1),并且value在10~20的分页数据,分页大小是10,当前页0,
具体使用方法请参见官网文档

4.4 querydsl代码生成以及配置

我们在前面看到,有一个类叫QOrmTestEntity,这种Q开头的是什么东东。
这是JPA-querydsl的生成类,通过这个类我们才能使用querydsl的相关功能。
这个类的生成是这样的:

@Generated("com.querydsl.codegen.EntitySerializer")
public class QOrmTestEntity extends EntityPathBase<OrmTestEntity> {

    private static final long serialVersionUID = 863569505L;

    public static final QOrmTestEntity ormTestEntity = new QOrmTestEntity("ormTestEntity");

    public final StringPath appCode = createString("appCode");

    public final StringPath appName = createString("appName");

    public final StringPath code = createString("code");

    public final BooleanPath del = createBoolean("del");

    public final StringPath description = createString("description");

    public final NumberPath<Long> id = createNumber("id", Long.class);

    public final StringPath moduleCode = createString("moduleCode");

    public final StringPath moduleName = createString("moduleName");

    public final StringPath name = createString("name");

    public final EnumPath<indi.zhifa.recipe.bailan.busy.enums.EOrmEntityStatus> status = createEnum("status", indi.zhifa.recipe.bailan.busy.enums.EOrmEntityStatus.class);

    public final NumberPath<Integer> value = createNumber("value", Integer.class);

    public final NumberPath<Integer> version = createNumber("version", Integer.class);

    public QOrmTestEntity(String variable) {
        super(OrmTestEntity.class, forVariable(variable));
    }

    public QOrmTestEntity(Path<? extends OrmTestEntity> path) {
        super(path.getType(), path.getMetadata());
    }

    public QOrmTestEntity(PathMetadata metadata) {
        super(OrmTestEntity.class, metadata);
    }

}

我们只需要在项目的maven pom.xml中,添加如下配置。在工程编译的时候,就会自动生成对应的Q类

    <build>
        <plugins>
            <plugin>
                <groupId>com.mysema.mavengroupId>
                <artifactId>apt-maven-pluginartifactId>
                <version>1.1.3version>
                <executions>
                    <execution>
                        <goals>
                            <goal>processgoal>
                        goals>
                        <configuration>
                            <outputDirectory>target/generated-sources/javaoutputDirectory>
                            <processor>com.querydsl.apt.jpa.JPAAnnotationProcessorprocessor>
                        configuration>
                    execution>
                executions>
            plugin>
        plugins>
    build>

4.5 JPA中,如何处理审计、假删除和乐观锁

4.5.1 审计

JPA的审计功能,可以让程序员在entity上添加响应的注解,如CreateBy,CreatedDate,LastModifiedBy,LastModifiedDate
并重载AuditorAware< Long > 接口,用于填充CreateBy和LastModifiedBy字段即可
在启动类上,也要加上@EnableJpaAuditing注解

4.5.2 假删除

JPA其实不支持假删除,如果一定要用假删除,那就不要调用deleteById接口,使用save接口更改del字段就可以了。
但我们在查询时,谁都不想在每个接口中处理del字段,那怎么办呢?
我可以使用Spring的AOP机制,拦截带有Predicate的Controller的接口,在其中拼上del相关的条件

4.5.3 乐观锁

乐观锁很简单,在entity的version字段加上@Version注解即可

4.6 代码实现

4.6.1 JPA的maven pom的dependency和 querydsl配置

<dependencies>
	<dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-jdbcartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-jpaartifactId>
        dependency>
dependencies>
<build>
        <plugins>
            <plugin>
                <groupId>com.mysema.mavengroupId>
                <artifactId>apt-maven-pluginartifactId>
                <version>1.1.3version>
                <executions>
                    <execution>
                        <goals>
                            <goal>processgoal>
                        goals>
                        <configuration>
                            <outputDirectory>target/generated-sources/javaoutputDirectory>
                            <processor>com.querydsl.apt.jpa.JPAAnnotationProcessorprocessor>
                        configuration>
                    execution>
                executions>
            plugin>
        plugins>
    build>

4.6.2 处理审计的handler

@Component
public class JPAAuditorAware implements AuditorAware<Long> {
    @Override
    public Optional<Long> getCurrentAuditor() {
        Long id = SnowflakeIdWorker.generateId();
        return Optional.of(id);
    }
}

4.6.3 处JPA下使用雪花算法

@Slf4j
public class SnowIdGenerator implements IdentifierGenerator {
    /**
     * 终端ID
     */
    public static long WORKER_ID = 1;

    /**
     * 数据中心id
     */
    public static long DATACENTER_ID = 1;

    private Snowflake snowflake = IdUtil.createSnowflake(WORKER_ID, DATACENTER_ID);

    @PostConstruct
    public void init() {
        WORKER_ID = NetUtil.ipv4ToLong(NetUtil.getLocalhostStr());
        log.info("当前机器的workId:{}", WORKER_ID);
    }

    public synchronized long snowflakeId() {
        return snowflake.nextId();
    }

    public synchronized long snowflakeId(long workerId, long datacenterId) {
        Snowflake snowflake = IdUtil.createSnowflake(workerId, datacenterId);
        return snowflake.nextId();
    }


    @Override
    public Serializable generate(SharedSessionContractImplementor session, Object object)
            throws HibernateException {
        return snowflakeId(WORKER_ID, DATACENTER_ID);
    }
}

在entity的id上加如下注解

@GeneratedValue(generator = "orderIdGenerator", strategy = GenerationType.SEQUENCE)
@GenericGenerator(name = "orderIdGenerator", strategy = "indi.zhifa.recipe.bailan.busy.handler.SnowIdGenerator")

4.6.4 处理假删除的AOP

@Slf4j
@Aspect
@Component
@Order(1)
public class LogicDelApo {

    private final EntityPathResolver entityPathResolver;
    private final QuerydslBindingsFactory bindingsFactory;
    private final QuerydslPredicateBuilder predicateBuilder;

    public LogicDelApo(ObjectProvider<EntityPathResolver> resolver, Optional<ConversionService> conversionService){
        QuerydslBindingsFactory factory = new QuerydslBindingsFactory((EntityPathResolver)resolver.getIfUnique(() -> {
            return SimpleEntityPathResolver.INSTANCE;
        }));
        this.entityPathResolver = (EntityPathResolver)resolver.getIfUnique(() -> {
            return SimpleEntityPathResolver.INSTANCE;
        });
        this.bindingsFactory = factory;
        this.predicateBuilder = new QuerydslPredicateBuilder((ConversionService)conversionService.orElseGet(DefaultConversionService::new), factory.getEntityPathResolver());
    }


    @Around("execution(* indi.zhifa.recipe..*.*controller.api..*(..)) && @annotation(permission)")
    public Object around(ProceedingJoinPoint joinPoint, JpaQsdl permission) throws Throwable {
        Object[] args = joinPoint.getArgs();
        for(int i = 0; i < args.length; ++i) {
            if (args[i] instanceof Predicate) {
                Predicate query = (Predicate)args[i];
                MethodSignature signature = (MethodSignature)joinPoint.getSignature();
                Parameter[] methodParameters = signature.getMethod().getParameters();
                Parameter parameter = methodParameters[i];
                QuerydslPredicate predicate = (QuerydslPredicate)parameter.getDeclaredAnnotation(QuerydslPredicate.class);
                Assert.notNull(parameter, "Predicate参数的@QuerydslPredicate注解必须声明!");
                MultiValueMap<String, String> parameters = new LinkedMultiValueMap();
                parameters.add("del","0");

                args[i] = this.createPredicate(parameters, query, predicate);
                return joinPoint.proceed(args);
            }
        }
        return joinPoint.proceed();
    }

    private Predicate createPredicate(MultiValueMap<String, String> parameters, Predicate rightQuery, QuerydslPredicate predicate) {
        Optional<QuerydslPredicate> annotation = Optional.ofNullable(predicate);
        TypeInformation<?> domainType = (TypeInformation)annotation.filter((it) -> {
            return !Object.class.equals(it.root());
        }).map((it) -> {
            return ClassTypeInformation.from(it.root());
        }).orElse(null);
        Assert.notNull(domainType, "root不能为空");
        Optional<Class<? extends QuerydslBinderCustomizer<EntityPath<?>>>> bindingsAnnotation = annotation.map(QuerydslPredicate::bindings).map(CastUtils::cast);
        QuerydslBindings bindings = createBindings(bindingsAnnotation, domainType);
        Predicate permissionPredicate = this.predicateBuilder.getPredicate(domainType, parameters, bindings);
        BooleanBuilder builder = new BooleanBuilder(permissionPredicate);
        return builder.and(rightQuery);
    }

    private QuerydslBindings createBindings(Optional<Class<? extends QuerydslBinderCustomizer<EntityPath<?>>>> bindingsAnnotation, TypeInformation<?> domainType) {
        QuerydslBindings bindings = (QuerydslBindings)bindingsAnnotation.map((it) -> {
            return this.bindingsFactory.createBindingsFor(domainType, it);
        }).orElseGet(() -> {
            return this.bindingsFactory.createBindingsFor(domainType);
        });
        EntityPath path = this.entityPathResolver.createPath(domainType.getType());
        try {
            // 取得SimplePath的构造函数(源代码是protect的)
            Constructor<BooleanPath> appPathConstructor = BooleanPath.class.getDeclaredConstructor(Path.class, String.class);
            appPathConstructor.setAccessible(true);
            BooleanPath delPath = appPathConstructor.newInstance(path, "del");
            bindings.bind(delPath).first(SimpleExpression::eq);
        } catch (IllegalAccessException | InvocationTargetException | InstantiationException | NoSuchMethodException var12) {
            throw new ServiceException(var12.getMessage());
        }
        return bindings;
    }
}

4.6.5 Reposity

public interface OrmTestReposity extends CrudRepository<OrmTestEntity,Long>, QuerydslPredicateExecutor<OrmTestEntity>, QuerydslBinderCustomizer<QOrmTestEntity> {
    @Override
    default void customize(QuerydslBindings bindings, QOrmTestEntity pQOrmTestEntity){
        // name 使用like
        bindings.bind(pQOrmTestEntity.name).first((path,value)->path.contains(value));
        bindings.bind(pQOrmTestEntity.appName).first((path,value)->path.contains(value));
        bindings.bind(pQOrmTestEntity.moduleName).first((path,value)->path.contains(value));
        bindings.bind(pQOrmTestEntity.description).first((path,value)->path.contains(value));
        bindings.bind(pQOrmTestEntity.value).all((path,value) -> {
            Iterator<? extends Integer> it = value.iterator();
            Integer from = it.next();
            if (value.size() >= 2) {
                Integer to = it.next();
                return Optional.of(path.between(from, to)); // between
            } else {
                return Optional.of(path.goe(from)); // greater or equal
            }
        });
    }
}

4.6.6 Service

@RequiredArgsConstructor
@Service
public class OrmJpaServiceImpl extends BaseOrmServiceImpl implements IOrmJpaService {

    private final OrmTestReposity mOrmTestReposity;

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void initTestData() {
        List<OrmTestEntity> initOrmTestEntityList = getInitOrmTestEntityList("JPA");
        mOrmTestReposity.saveAll(initOrmTestEntityList);
    }

    @Override
    public Page<OrmTestEntity> page(Pageable pPageable, Predicate predicate) {
        Page<OrmTestEntity> ormTestEntityPage =  mOrmTestReposity.findAll(predicate,pPageable);
        return ormTestEntityPage;
    }

}

4.6.7 启动类需要的注解

@EnableJpaAuditing
@EnableJpaRepositories(basePackages = {"indi.zhifa.recipe.bailan.busy.**.repository"})

五、mybatis-plus实现

5.1 为什么会出现mybatis-plus

我们已经看到,JPA几乎满足了我们在1.2中讨论的所有问题,那为什么还要搞个mybatis-plus呢?
JPA是一个基于hibernate的一个框架,如果深入使用JPA的话,就会发现JPA的代码叠床架屋,十分难以驾驭。
而且JPA也无法像mybatis那样灵活操作sql。
mybatis在中国市场占有率如此之高,除了程序员时薪较发达国家偏低的原因外,肯定还是有其优势的。但在大多数情况下,mybatis的优势并无法体现出来,但却拖慢了开发的效率。
这时,mybatis-plus就呼之欲出。mybatis-plus基于mybatis编写,支持所有mybatis的功能。此外,mybatis-plus提供了类似JPA中CrudRepository+QuerydslPredicateExecutor的功能。同时对诸如审计字段、假删除、乐观锁等常见需求都有很好的支持。下面,就让我们来看看mybatis-plus吧。

5.2 mybatis-plus的代码生成

mybatis根据数据库生成相应代码,可以自己写个main函数,引用mybatis-plus-generator来实现。
更多细节请参见官网
pom引用:

        <dependency>
            <groupId>com.baomidougroupId>
            <artifactId>mybatis-plus-generatorartifactId>
        dependency>

代码:

public class MapperGenerator {
    public static void main(String[] args) throws FileNotFoundException {
        String outputPath =  "E:\\DOCUMENT\\generator\\bailan2";

        FastAutoGenerator.create("jdbc:mysql://localhost:3307/bailan?useSSL=false&useUnicode=true&characterEncoding=utf-8",
                        "root", "qqhilvMgAl@7")
                .globalConfig(builder -> {
                    // 设置作者
                    builder.author("织法")
                            // 开启 swagger 模式
                            .enableSwagger()
                            // 覆盖已生成文件
                            .fileOverride()
                            // 指定输出目录
                            .outputDir(outputPath);
                })
                .packageConfig(builder -> {
                    // 设置父包名
                    builder.parent("indi.zhifa.recipe.bailan.busy")
                            // 指定模块名称
                            .moduleName("dbgen")
                            .entity("entity.po")
                            .service("dao.service")
                            .serviceImpl("dao.service.impl")
                            .mapper("dao.mapper");

                })
                .strategyConfig(builder -> {
                    // 设置过滤表前缀
                    builder.addTablePrefix("bl_","sys_")
                            .addExclude("gc_user")
                            .entityBuilder()
                            .enableLombok()
                            .enableRemoveIsPrefix()
                            .logicDeleteColumnName("del")
                            .logicDeletePropertyName("del")
                            .addTableFills(new Column("create_time", FieldFill.INSERT))
                            .addTableFills(new Property("createTime", FieldFill.INSERT))
                            .addTableFills(new Column("create_by",FieldFill.INSERT))
                            .addTableFills(new Property("createBy",FieldFill.INSERT))
                            .addTableFills(new Column("create_name",FieldFill.INSERT))
                            .addTableFills(new Property("createName",FieldFill.INSERT))
                            .addTableFills(new Column("create_ip",FieldFill.INSERT))
                            .addTableFills(new Property("createIp",FieldFill.INSERT))

                            .addTableFills(new Column("modify_time", FieldFill.UPDATE))
                            .addTableFills(new Property("modifyTime", FieldFill.UPDATE))
                            .addTableFills(new Column("modify_by",FieldFill.UPDATE))
                            .addTableFills(new Property("modifyBy",FieldFill.UPDATE))
                            .addTableFills(new Column("modify_name",FieldFill.UPDATE))
                            .addTableFills(new Property("modifyName",FieldFill.UPDATE))
                            .addTableFills(new Column("modify_ip",FieldFill.UPDATE))
                            .addTableFills(new Property("modifyIp",FieldFill.UPDATE))

                            .idType(IdType.ASSIGN_ID)
                            .formatFileName("%sEntity")
                            .entityBuilder()
                            .superClass(BaseEntity.class)
                            .disableSerialVersionUID()
                            .enableLombok()
                            .versionColumnName("version")
                            .versionPropertyName("version")
                            .addSuperEntityColumns("id","create_time","create_by","create_name","create_ip","modify_by","modify_time","modify_name","modify_ip","version","del")
                            .serviceBuilder()
                            .formatServiceFileName("I%sDbService")
                            .formatServiceImplFileName("%sDbServiceImpl")
                            .serviceBuilder()
                            .superServiceClass(IZfDbService.class)
                            .superServiceImplClass(ZfDbServiceImpl.class);
                })
                // 使用Freemarker引擎模板,默认的是Velocity引擎模板
                .templateEngine(new FreemarkerTemplateEngine())
                .execute();
    }
}

5.3 mybatis-plus的wrapper模式

mybatis-plus不用生成Q类,也可以实现类似QuerydslPredicateExecutor的功能。使用方法非常简单,小伙伴们可以去官网看一下。代码样例可以去5.6看,这里就不做过多赘述了。

5.4 mybatis-plus提供的方便service

类似JPA的CrudRepository,mybatis-plus提供了一个方便的service基类,其中包括常见的实体操作。
我们只需要在接口上继承IService< T >, 实现类上继承 ServiceImpl< T >即可。

5.5 mybatis-plus的填充

mybatis中,可以写一个handler类继承自MetaObjectHandler,就可以实现字段填充。

@Component
@Slf4j
public class SimpleBaseMetaObjectHandler implements MetaObjectHandler {
    protected static final Long SYS_ID = 0L;
    protected static final String SYS_NAME = "sys";
    protected static final String SYS_IP = "localhost";

    protected String createTimeStr = "createTime";
    protected String createByStr= "createBy";
    protected String createNameStr = "createName";
    protected String createIpStr = "createIp";

    protected String modifyTimeStr = "modifyTime";
    protected String modifyByStr= "modifyBy";
    protected String modifyNameStr = "modifyName";
    protected String modifyIpStr = "modifyIp";
    @Override
    public void insertFill(MetaObject pMetaObject) {
        if(checkFieldNull(pMetaObject,createTimeStr)){
            this.strictInsertFill(pMetaObject,createTimeStr,()-> LocalDateTime.now(),LocalDateTime.class);
        }
        if(checkFieldNull(pMetaObject,createByStr)){
            this.strictInsertFill(pMetaObject,createByStr,()->SYS_ID,Long.class);
        }
        if(checkFieldNull(pMetaObject,createNameStr)){
            this.strictInsertFill(pMetaObject,createNameStr,()->SYS_NAME,String.class);
        }
        if(checkFieldNull(pMetaObject,createIpStr)){
            this.strictInsertFill(pMetaObject,createIpStr,()->SYS_IP,String.class);
        }
    }

    @Override
    public void updateFill(MetaObject pMetaObject) {
        if(checkFieldNull(pMetaObject,modifyTimeStr)){
            this.strictUpdateFill(pMetaObject,modifyTimeStr,()-> LocalDateTime.now(),LocalDateTime.class);
        }
        if(checkFieldNull(pMetaObject,modifyByStr)){
            this.strictUpdateFill(pMetaObject,modifyByStr,()->SYS_ID,Long.class);
        }
        if(checkFieldNull(pMetaObject,modifyNameStr)){
            this.strictUpdateFill(pMetaObject,modifyNameStr,()->SYS_NAME,String.class);
        }
        if(checkFieldNull(pMetaObject,modifyIpStr)){
            this.strictUpdateFill(pMetaObject,modifyIpStr,()->SYS_IP,String.class);
        }
    }

    protected boolean checkFieldNull(MetaObject pMetaObject, String pField){
        if(pMetaObject.hasSetter(pField)){
            Object orgModifyTime = pMetaObject.getValue(pField);
            if(null == orgModifyTime){
                return true;
            }
        }
        return false;
    }
}

5.6 mybatis-plus的假删除、乐观锁

假删除,只需要在entity相应字段上加上@TableLogic注解
乐观锁,只需要在entity相应字段上加上@Version注解

5.7 代码实现

5.7.1 mapper

public interface OrmTestMybatisPlusMapper extends BaseMapper<OrmTestEntity> {

}

5.7.2 dbService

public interface IOrmTestMybatisPlusDbService extends IService<OrmTestEntity> {
}
@Component
public class OrmTestMybatisPlusDbServiceImpl extends ServiceImpl<OrmTestMybatisPlusMapper, OrmTestEntity> implements IOrmTestMybatisPlusDbService {

}

5.7.3 service

@Slf4j
@RequiredArgsConstructor
@Service
public class OrmMybatisPlusServiceImpl extends BaseOrmServiceImpl implements IOrmMybatisPlusService {

    private final IOrmTestMybatisPlusDbService mOrmTestMybatisPlusDbService;

    @Override
    public void initTestData() {
        List<OrmTestEntity> initOrmTestEntityList = getInitOrmTestEntityList("MyBatis-Plus");
        mOrmTestMybatisPlusDbService.saveBatch(initOrmTestEntityList);
    }

    @Override
    public Page<OrmTestEntity> page(int pCurrent, int pSize,
                                        String pAppCode, String pModuleCode, String pCode, String pName, EOrmEntityStatus pStatus,
                                        Integer pMin, Integer pMax) {
        Page<OrmTestEntity> pageCfg = new Page<>(pCurrent,pSize);
        LambdaQueryWrapper<OrmTestEntity> queryWrapper = Wrappers.<OrmTestEntity>lambdaQuery()
                .eq(!StringUtils.isEmpty(pAppCode),OrmTestEntity::getAppCode,pAppCode)
                .eq(!StringUtils.isEmpty(pAppCode),OrmTestEntity::getAppCode,pAppCode)
                .eq(!StringUtils.isEmpty(pModuleCode),OrmTestEntity::getModuleCode,pModuleCode)
                .eq(!StringUtils.isEmpty(pCode),OrmTestEntity::getCode,pCode)
                .like(!StringUtils.isEmpty(pName),OrmTestEntity::getName,pName)
                .eq(null != pStatus,OrmTestEntity::getStatus,pStatus);
        if(null!= pMin && null != pMax){
            queryWrapper = queryWrapper.between(OrmTestEntity::getValue,pMin,pMax);
        }else if(null == pMin && null != pMax){
            queryWrapper = queryWrapper.ge(OrmTestEntity::getValue,pMax);
        }else if(null != pMax && null == pMax){
            queryWrapper = queryWrapper.le(OrmTestEntity::getValue,pMin);
        }
        Page<OrmTestEntity> pageData = mOrmTestMybatisPlusDbService.page(pageCfg,queryWrapper);
        return pageData;
    }
}

六、对比与总结

通过前面的样例与讲解,结合小编的工作经验,给出如下总结:

6.1 技术适用场景

6.1.1 JDBC的适用场景

JDBC虽然最麻烦,但也最灵活与轻量。在不确定数据源、不确定要执行具体什么逻辑的时候,比如写一些通用的框架,JDBC就是最适合的了。比如写一个报表系统,用户把要执行的sql存储到数据库中,调用相应接口执行sql。这个时候,无疑JDBC是最好的选择。

6.1.2 Mybatis的适用场景

Mybatis的核心就是灵活生成sql。一些诸如大屏、报表的业务,其核心就是写复杂的聚合sql,并做一些逻辑运算。这种时候选用Mybatis就是最合适的了。

6.1.3 JPA的适用场景

JPA更适合快速开发系统,把数据库设计好后,所有代码一并生成,快速部署交给前端。当产品和前端有一些新需求时,后端再介入开发。等前端开发告一段落,后端再做一次代码和安全性的优化。
也就是说,JPA更适合前端作为逻辑驱动,后端只是提供一个restful接口访问的数据仓库。

6.1.4 mybatis-plus的适用场景

mybatis-plus更适合产品->后端->前端这种方式驱动的项目。产品先设计业务逻辑,后端设计数据库,生成初步的代码。而后根据一个一个的页面,认真编写接口。接口编写完成后,交给前端开发,然后后端继续编写接口的实现。
由于mybatis-plus兼容mybatis,一些大屏报表的业务,可以使用Mybatis的方式做开发。

6.2 技术对比

ORM 学习成本 开发效率 运行效率 深入学习
JDBC B D A 容易
Mybatis C C A 较容易
JPA B A C 较难
Mybatis-plus A A B 较容易
ORM 审计支持 乐观锁 假删除 易维护性 安全性
JDBC × × × D C
Mybatis × × × C A
JPA o o × A D
Mybatis-plus o o o B B

注意,为什么小编认为JPA不安全呢,原因既是在于querydsl的通用分页接口的写法。
很多时候,程序员仅仅生成了代码,就把接口放了出去。entity中所有字段都可以被前端操作,想用什么字段查询就用什么字段查询,想修改哪个字段就修改哪个字段。
虽说JPA可以通过注解方式,使某些字段不参与条件。但在实际开发中,下面的程序员常常不去做这些,最后导致整个项目像是前端在写,后端只负责设计了个数据库和运维部署。

你可能感兴趣的:(springboot,mybatis-plus,JPA,mybatis,spring,boot,java)