从零开始 Spring Boot 25:MyBatis II
创始人
2024-03-31 20:34:36
0次

从零开始 Spring Boot 25:MyBatis II

spring boot

图源:简书 (jianshu.com)

在从零开始 Spring Boot 23:MyBatis - 红茶的个人站点 (icexmoon.cn)中我介绍了MyBatis的一些基本功能和用法,下文介绍遗漏的部分。

本文的示例代码都基于从零开始 Spring Boot 24:处理时间 - 红茶的个人站点 (icexmoon.cn)的最终示例修改而来,可以通过learn_spring_boot/ch24 (github.com)获取相应的代码。

TypeHandler

在之前的文章中,已经介绍过类型处理器(type handler)的基本用法,可以通过在resultMap中对特定字段映射添加typeHandler的方式来实现读取特定字段的特殊处理:



INSERT INTO coupon(add_user_id,add_time,amount,expire_time,enough_amount,`type`,del_flag)VALUES (#{coupon.addUserId},#{coupon.addTime},#{coupon.amount,javaType=double,jdbcType=NUMERIC,numericScale=2},#{coupon.expireTime},#{coupon.enoughAmount},#{coupon.type,typeHandler=cn.icexmoon.books2.book.entity.typehandler.CouponTypeHandler},0)

当然,写入的时候也一样,上边的示例同样说明了这一点。

之所以这里需要这么做,是因为对于枚举类型,MyBatis默认会使用EnumOrdinalTypeHandler或EnumTypeHandler处理器来进行处理,前者会让枚举转化为枚举在enum中的定义顺序,后者会将枚举转换为枚举的字面量。

可以从mybatis – MyBatis 3 | 配置页面查看MyBatis所有默认的类型处理器列表。

更常见的是,我们会采用以下方式定义的枚举:

package cn.icexmoon.books2.book.entity.enums;
// ...
public enum CouponType implements IDesc {FREE_COUPON(1, "无门槛券"), ENOUGH_COUPON(2, "满减券");@JsonValueprivate Integer value;private String desc;public Integer getValue() {return value;}public String getDesc() {return desc;}CouponType(Integer value, String desc) {this.value = value;this.desc = desc;}public static CouponType match(Integer value) {for (CouponType ct : CouponType.values()) {if (ct.getValue().equals(value)) {return ct;}}return null;}
}

这里的IDesc是一个为了方便获取自定义枚举值和中文名称而定义的接口:

package cn.icexmoon.books2.system;
// ...
public interface IDesc {T getValue();String getDesc();
}

所以这里我们希望MyBatis能默认将实现了IDesc接口的枚举转换为IDesc.getValue返回的值存储,或者以相反的方式读取。

就像之前一样,我们可以创建一个类型处理器来完成这件事:

package cn.icexmoon.books2.system.typehandler;
// ...
@MappedTypes(IDesc.class)
public class IDescEnumTypeHandler & IDesc> extends BaseTypeHandler {private Class type;public IDescEnumTypeHandler(Class type) {this.type = type;}@Overridepublic void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {if (jdbcType == null) {ps.setInt(i, parameter.getValue());} else {ps.setObject(i, parameter.getValue(), jdbcType.TYPE_CODE);}}@Overridepublic E getNullableResult(ResultSet rs, String columnName) throws SQLException {int i = rs.getInt(columnName);return MyEnumUtil.getIEnumInstance(type, i);}@Overridepublic E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {int i = rs.getInt(columnIndex);return MyEnumUtil.getIEnumInstance(type, i);}@Overridepublic E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {int i = cs.getInt(columnIndex);return MyEnumUtil.getIEnumInstance(type, i);}
}

不同的是,这里使用了泛型,表明这是一个能够处理实现了IDesc接口的枚举类型的类型处理器:

IDescEnumTypeHandler & IDesc> extends BaseTypeHandler

实际上应当使用IDesc而非IDesc,但前者需要在具体实现中处理类型转换的问题,暂时没想到如何处理,所以只能特异化采用后者。

为了让MyBatis能够在需要的时候自动使用这个处理器,而不是手动指定,需要通过@MappedTypes注解添加该处理器能够处理的类型。

此外,还需要通过配置文件将处理器所在包加入MyBatis相关配置,以添加到MyBatis的默认处理器中:

mybatis.type-handlers-package=cn.icexmoon.books2.system.typehandler

现在重启项目后MyBatis就可以自动处理实现了IDesc接口的自定义枚举类型,而不需要在XML中手动指定typeHandler属性了。

MyBatisPlus实现了上述功能,如果集成了MyBatisPlus,可以通过实现IEnum接口来实现自定义枚举,不需要自己实现类型处理器。

动态SQL

这里所谓的动态SQL实际上就是MyBatis拼接SQL时的流程控制语句,其用途和写法和编程语言中的控制语句相似。

if

if标签常用于在SQL中拼接查询条件:


这里dto对应以下类型:

package cn.icexmoon.books2.book.entity.dto;
// ...
@Data
@NoArgsConstructor
public class BookQueryDTO {private String userName;private String bookName;private String desc;private CouponType type;
}

BookQueryDTO表示一个查询书籍的条件,包含书籍名称、录入书籍人员姓名等,显然这些条件是可选的,在查询时可能有一个或没有,也可能都存在。所以在XML中使用if来判断条件是否存在,进而完成查询语句的拼接。

需要注意的是,在if的test属性中,如果存在多个条件,使用and或or进行逻辑运算,而不是Java常见的&&和||,这是因为&是XML的保留字符,需要进行转义,所以最好不要使用。

choose

choose标签的用途类似于编程语言中的switch语句,可以从众多执行块中匹配一个条件符合的执行:


在这个修改后的示例中,添加了一个新的查询条件bookId,并且通过引入choose标签,实现了如果查询条件中有有效的bookId,则无视其他查询条件,按照id查询图书信息并返回。

这里when标签的作用类似于case,用于匹配一个条件。otherwise标签的用途类似于default。

当然,就像switch...case那样,你可以添加多个when标签,这里仅有一个when和otherwise的情况其实起到了if...else的用途。

where

如果你已经很熟悉在代码中拼接SQL的写法,那肯定对在WHERE语句开头保留至少一个固定条件的必要性有深刻认识,比如上边示例中的:

        WHERE b.del_flag=0AND u.del_flag=0

如果所有条件都是动态生成的,比如:

        WHERE  0">AND b.id = #{dto.bookId}AND b.name LIKE CONCAT('%',#{dto.bookName},'%')AND u.name LIKE CONCAT('%',#{dto.userName},'%')AND b.desc LIKE CONCAT('%',#{dto.desc},'%')AND b.type = #{dto.type.value}

当所有条件为空时,就会产生一个类似SELECT * FROM book WHERE这样的语句,显然是不符合SQL语法的,会无法执行。或者因为第一个条件为空,而产生SELECT * FROM book WHERE AND xxx这样的语句,同样无法执行。

如果你的表设计中有逻辑删除字段,可以放在WHERE后避免这一点,就像之前的示例中那样。如果没有,也可以添加一个1=1:

        WHERE 1 = 1 0">AND b.id = #{dto.bookId}AND b.name LIKE CONCAT('%',#{dto.bookName},'%')AND u.name LIKE CONCAT('%',#{dto.userName},'%')AND b.desc LIKE CONCAT('%',#{dto.desc},'%')AND b.type = #{dto.type.value}

新手可能会对这种写法很困惑,但其实是一种很实际的写法。

如果你不愿意这么做,MyBatis提供一个额外的选择——where标签:


where标签可以在所有条件为空和第一个条件为空这两种情况下都拼接出正常的SQL。

trim

where标签可以看做是特异化的trim标签,其等效于:

...

trim标签的用途是将包裹内容中以prefixOverrides值开头的内容用prefix中的值代替,当然如果包裹的内容为空,就不处理。

所以上边的示例也可以用trim标签替代:


set

在UPDATE语句中,同样存在上边类似的问题:

    UPDATE bookSETname=#{name},description=#{description},user_id=#{userId},type=#{type.value},extra=#{extra},del_flag=#{delFlag}WHERE id=#{id} AND del_flag=0

只要映射器入参的delFlag属性为空,就会出现类似UPDATE xxx SET xxx=xxx, WHERE这样的语句出现,会报错。这个问题比之前更棘手,不能简单添加1=1之类的去规避,好在MyBatisPlus提供一个set标签:

 UPDATE bookname=#{name},description=#{description},user_id=#{userId},type=#{type.value},extra=#{extra},del_flag=#{delFlag}WHERE id=#{id} AND del_flag=0

同样的,set标签可以看做是特异化的trim标签,和下边的trim标签等效:

...

需要注意,因为是要特殊处理最后一个,,所以这里是suffixOverrides属性而非prefixOverrides。

foreach

foreach标签类似于Java中的for语句,常用于生成SQL中的IN语句。

假设我们的查询条件要求能够一次性查询出多种类型的书籍:

package cn.icexmoon.books2.book.entity.dto;
// ...
@Data
@NoArgsConstructor
public class BookQueryDTO {private Integer bookId;private String userName;private String bookName;private String desc;private List types;
}

对应的XML:


只要指定相关拼接字符,foreach就可以自动生成合适的IN语句。

需要注意的是,常见的下边的写法是有问题的:

                  AND type IN#{type.value}

如果dto.types不为空,可以正常执行。但如果为空列表,就会导致生成WHERE XXX=xxx AND type IN LIMIT ?,?这样的SQL。

bind

bind标签可以创建一个变量,并将其绑定到当前的上下文:

              AND b.name LIKE #{bookNameLike}

前边说过,${}实际上是调用JDBC的SQL参数预处理进行参数替换,所以我们不能直接写LIKE %#{dto.bookName}%这样的语句,必须使用MySQL的字符串连接函数CONCAT。除此之外,还可以像上面那样使用bind标签,这里相当于用Java生成了一个bookNameLike变量,该变量的值是'%'+dto.bookName+'%'语句执行后的结果。然后可以将该变量用于参数替换LIKE #{bookNameLike}。

当然这个示例并不比直接使用CONCAT函数方便和直观,但可以看到bind标签的强大,我们甚至可以在value属性中调用其它函数获取一个值来绑定。

谢谢阅读。

本文最终的完整示例代码可以从learn_spring_boot/ch25 (github.com)获取。

参考资料

  • mybatis – MyBatis 3 | 动态 SQL

相关内容

热门资讯

前端-session、jwt 目录:   (1)session (2&#x...
linux入门---制作进度条 了解缓冲区 我们首先来看看下面的操作: 我们首先创建了一个文件并在这个文件里面添加了...
关于测试,我发现了哪些新大陆 关于测试 平常也只是听说过一些关于测试的术语,但并没有使用过测试工具。偶然看到编程老师...
前缀和与对数器与二分法 1. 前缀和 假设有一个数组,我们想大量频繁的去访问L到R这个区间的和,...
nodejs:本地安装nvm实... 一、背景-使用不同版本node的原因 vue3+ts、nuxt3版本,node...
JAVA集合知识整理 Java集合知识整理 HashMap相关 HashMap的底层数据结构:jdk1.8之...
无刷直流电机介绍及单片机控制实... 无刷直流电机介绍及单片机控制实例前言基本概念优势与劣势使用寿命基本结构使用单片机控制实例电子调速器&...
fwdiary(2) dp2 1.传纸条  AcWing 275. 传纸条 - AcWing 走两条路,走一条最大的...
常用的DOS命令 常用的DOS命令 DOS(Disk Operating System,磁...
<C++> 类和对象(下) 1.const成员函数将const修饰的“成员函数”称之为const成员函数,cons...