玩转mybatis中的类型转换
1.场景
日常java开发中经常有这种需求,用0或者1这些代码(不局限于数字)来表示某种状态。比如用0表示女性,用1来表示男性。而且写入数据库可能是一个标识,从数据库读取又还原为具体的说明。而且一般情况下为了更好理解或者消除魔法值,通常的处理方案是定义一个枚举:
有些枚举是这样定义的
public enum GenderType{
FEMALE,MALE,UNKNOWN
}
那么通常很多人会这么入库(java伪代码)
if(GenderType.MALE){
// 写入 1
}else if(GenderType.FEMALE){
// 写入 0
}else{
//也可能是泰国回来的 那就 2
}
读取的时候要么同样按照上面的再反向处理一次或者使用数据库sql语法case when
来直接写入DTO
CASE gender
WHEN 1 THEN '男'
WHEN 0 THEN '女'
ELSE '未知' END
这种处理方式看起来不是很优雅。而且多了很多的判断和处理逻辑,和我们的业务并不是非常相关。所以我们可以选择更好的处理方式。
2.Mybatis中的TypeHandler
如果你ORM框架用的是Mybatis。那么将很容易通过TypeHandler<T>接口解决这个问题。
2.1 TypeHandler 分析
public interface TypeHandler<T> {
void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
T getResult(ResultSet rs, String columnName) throws SQLException;
T getResult(ResultSet rs, int columnIndex) throws SQLException;
T getResult(CallableStatement cs, int columnIndex) throws SQLException;
}
源码分析:
- setParameter 方法 通过 传入的T类型写你自己的逻辑,选择调用 PreparedStatement 对象的某个set方法将数据写入数据库。此方法用来写库。
- getResult(ResultSet rs, String columnName) 通过字段名来读库并转换为T类型。
- getResult(ResultSet rs, int columnIndex) 通过字段索引来读库并转换为T类型。
- getResult(CallableStatement cs, int columnIndex) 调用存储过程来获取结果并转换为T类型。
2.2 EnumOrdinalTypeHandler
我们发现TypeHandler有一个实现类EnumOrdinalTypeHandler。字面意思是可以通过枚举的序号来处理类型。
@Override
public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
ps.setInt(i, parameter.ordinal());
}
我们先不考虑setNull的情况。通过此方法我们发现确实存入的是枚举的顺序值(顺序从0开始),拿上面的例子来说 如果是GenderType.FEMALE是0,如果是GenderType.MALE是1,但是当GenderType.UNKNOWN时存入的是2。取的时候也是自然反向处理为具体的GenderType枚举。
2.3 EnumTypeHandler
我们还发现有另外一个枚举类型处理器。它的set方法是这样的:
@Override
public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
if (jdbcType == null) {
ps.setString(i, parameter.name());
} else {
ps.setObject(i, parameter.name(), jdbcType.TYPE_CODE); // see r3589
}
}
我们不考虑jdbcType问题发现都是将Enum.name()的值写入数据库。拿上面的例子来说 如果是GenderType.FEMALE是FEMALE,如果是GenderType.MALE是MALE,但是当GenderType.UNKNOWN时存入的是UNKNOWN。读库是通过Enum.valueOf(Class<T> enumType,String name)来进行反转操作。
2.4 自定义TypeHandler
如果说我们的枚举类型或者说我们使用其他方式来处理类别转换怎么办?当然Mybatis不会帮你干这么具体的事情。需要你自己来实现了。我们还拿枚举作为例子,然后模仿上面的两种TypeHandler。 还是拿开始的例子来说通常我个人比较喜欢这么定义枚举:
public enum GenderTypeEnum {
/**
* female.
*/
FEMALE(0, "女"),
/**
* male.
*/
MALE(1,"男"),
/**
* unknown.
*/
UNKNOWN(2, "未知");
private int value;
private String description;
GenderType(int value, String description) {
this.value = value;
this.description = description;
}
public int value() {
return this.value;
}
public String description() {
return this.description;
}
}
通过继承BaseTypeHandler实现该抽象类的3个钩子方法就行了:
@MappedTypes({GenderTypeEnum.class})
@MappedJdbcTypes({JdbcType.INTEGER})
public class GenderTypeEnumTypeHandler extends BaseTypeHandler<GenderTypeEnum> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, GenderTypeEnum parameter, JdbcType jdbcType) throws SQLException {
if (jdbcType == null) {
ps.setInt(i, parameter.value());
} else {
// see r3589
ps.setObject(i, parameter.value(), jdbcType.TYPE_CODE);
}
}
@Override
public GenderTypeEnum getNullableResult(ResultSet rs, String columnName) throws SQLException {
return getGenderType(rs.getInt(columnName));
}
@Override
public GenderTypeEnum getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return getGenderType(rs.getInt(columnIndex));
}
@Override
public GenderTypeEnum getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return getGenderType(cs.getInt(columnIndex));
}
private GenderTypeEnum getGenderType(int value) {
Class<GenderTypeEnum> genderTypeClass = GenderTypeEnum.class;
return Arrays.stream(genderTypeClass.getEnumConstants())
.filter(genderType -> genderType.value() == value)
.findFirst().orElse(GenderTypeEnum.UNKNOWN);
}
}
TypeHandler 实现写好了,那么如何让其发挥作用呢?我们接着往下走。
2.5 TypeHandler的核心要点
TypeHandler作用是javaType和jdbcType相互转换。所以在声明一个TypeHandler的时候一定要明确该TypeHandler处理的这两种类型。这是必须要明确的原则。MyBatis不会通过窥探数据库元信息来决定使用哪种JDBC类型,所以你必须在参数和结果映射中指明何种类型的字段,使其能够绑定到正确的类型处理器上。MyBatis直到语句被执行时才清楚数据类型。 通过上述例子中的@MappedJdbcTypes和@MappedTypes来进行绑定类型转换关系,也可以通过xml的typeHandler元素中的jdbcType或者javaType来指定。如果同时指定,xml的优先级要高。 注意有可能你会覆盖内置的TypeHandler。所以自定义时一定要去了解Mybatis提供的一些默认处理器。避免对其他业务的影响。所以使用自定义TypeHandler很重要的一个原则就是一定要声明JavaType和JdbcType.上面这些虽然比较生涩但是对于使用好TypeHandler非常重要。接下来我们来讲讲具体的配置。
2.6 免注册TypeHandler
我们这里只讲xml中的配置:
- 一种在rultMap元素中声明一般用来查询。一定要注意2.5中的一些原则。
<resultMap id="StudentMap" type="cn.felord.mybatis.entity.Student">
<id column="student_id" property="studentId"/>
<result column="student_name" property="studentName"/>
<result column="gender" property="genderType" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
<result column="age" property="age"/>
</resultMap>
- 然后是在插入、更新语句中使用。它们都是相同的,这里只举一个插入例子。
<insert id="saveStu">
insert into student (student_name, gender, age)
values (#{studentName},
#{genderType,javaType=cn.felord.mybatis.enums.GenderTypeEnum,jdbcType=INTEGER,typeHandler=cn.felord.mybatis.type.GenderTypeEnumTypeHandler},
#{age})
</insert>
如果注册了别名都可以使用别名。上面的好处就是不用在TypeHandlerRegistry中进行注册。
2.7 注册TypeHandler
在配置中声明注册TypeHandler,然后Mybatis根据两种类型会自动匹配。所以这里还是要强调2.5中的核心要点。
- 如果你是xml配置需要在Configuration配置文件中的<typeHandlers>标签中进行声明式注册
<typeHandlers>
<typeHandler jdbcType="JdbcType枚举存在的枚举" javaType="typeAliases的别名或者全限定类名" handler="类全限定名"/>
<package name="指定所有typeHandler所在的包的包名"/>
</typeHandlers>
- javaConfig 方式 ,第一你可以通过SqlSessionFactory对象取到Configuration对象将typeHandler注册进去。如果你使用mybatis-spring组件,可以在SqlSessionFactoryBean 的setTypeHandlersPackage方法中配置typeHandler的集中包路径,那么框架将会自动扫描并注册他们。springboot中对应的配置属性是mybatis.typeHandlersPackage。
如果你注册了TypeHandler。在Mapper.xml中只需要声明jdbcType和javaType,无需再声明具体的typeHandler。Mybatis会自动通过jdbcType、javaType来映射到具体注册的TypeHandler上去 。就像下面的例子
<insert id="saveAutomaticStu">
insert into student (student_name, gender, age)
values (#{studentName}, #{genderType,javaType=cn.felord.mybatis.enums.GenderTypeEnum,jdbcType=INTEGER}, #{age})
</insert>
3.总结
今天我们学习了mybatis开发中如何通过使用类型处理器进行类型的转换处理,如何处理枚举,如何自定义处理器并使用它。相信对你在java开发过程中会有很大的帮助。相关的代码在我的码云仓库中:https://gitee.com/felord/mybatis-test.git
多多关注我的公众号:码农小胖哥,可以得到更加及时的资讯和反馈。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
RocketMQ为什么要保证订阅关系的一致性?
微信公众号「后端进阶」,专注后端技术分享:Java、Golang、WEB框架、分布式中间件、服务治理等等。 前段时间有个朋友向我提了一个问题,他说在搭建 RocketMQ 集群过程中遇到了关于消费订阅的问题,具体问题如下: 然后他发了报错的日志给我看: the consumer's subscription not exist 我第一时间在源码里找到了报错的位置: org.apache.rocketmq.broker.processor.PullMessageProcessor#processRequest: subscriptionData = consumerGroupInfo.findSubscriptionData(requestHeader.getTopic()); if (null == subscriptionData) { log.warn("the consumer's subscription not exist, group: {}, topic:{}", requestHeader.getConsumerGroup(), requestHeader.getTop...
-
下一篇
最常见的 Git 错误都有哪些,如何解决它们?
如果您曾经与许多开发者一起开发一个大项目,那么使用 Git 作为版本控制是一个最好的选择。 不过 Git 很复杂,使用过程中经常会犯各种错误。 在本文中,我将讨论程序员在使用Git时所犯的一些常见错误以及如何解决它们。 拼写错误的最后提交消息 经过几个小时的编码后,您的提交消息可能存在很多拼写错误,好在有一个简单的解决方案: git commit --amend 该命令将打开您的编辑器,并允许您更改最后一次提交消息。 因为可能没人会注意到你写的 “Initial commment” 有三个 m。 拼写错误的分支名 假设已经是下午三点了,但是你还没有吃午饭。饥肠辘辘的你可能直接就用 feature-brunch 作为你的分支名了,哇塞,好美味啊。可是很明显你写错了,应该是 feature-branch 。所以你可以使用 mv 命令类似重命名文件的方式重命名此分支:将其移动到具有正确名称的新位置。 如: git branch -m feature-brunch feature-branch 但如果您已推送此分支,则需要执行几个额外步骤。 我们需要从远程删除旧分支并推送新分支: git pu...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Hadoop3单机部署,实现最简伪集群
- MySQL数据库在高并发下的优化方案
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- Windows10,CentOS7,CentOS8安装Nodejs环境
- SpringBoot2整合Redis,开启缓存,提高访问速度
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- SpringBoot2配置默认Tomcat设置,开启更多高级功能