TINYINT(1) 类型的字段,明明数据存的是 2,为什么查出来是 true
开心一刻
大学期间,跟初恋谈了一段刻骨铭心的恋情,因为某些原因,大学毕业后分手了。
如今大学毕业已经10年,听说她很早就出国了,而我,很早就成的哥了。
昨天,初恋坐上了我的的车,我一眼就认出了她,她亦如当初模样,而我却满脸沧桑。
我不敢打招呼,默默的听着她打电话,讲述着国外的种种。
快到目的地的时候,她放下了电话说:我已经把我这10年的经历都说给你听了,你连句你好都不说吗
我知道电话那头没有任何人,她是故意说给我听的,我哽咽着颤抖的说到:你好
她深情的看着我,问道:我们还回得去吗
我疑惑的望向她,说到:回去?回去可以啊,但得加钱......

TINYINT
关于 TINYINT,我相信大家都知道它,是数据库的一种数据类型,说的详细点,它是数据库的一种数字类型,再详细点,它是数据库的一种整数类型;需要注意的是,它并非 SQL 标准整数类型
SQL标准整数类型:INTEGER
(orINT) andSMALLINT
而是某些数据库的拓展整数类型,所以并非所有的关系型数据库都支持 TINYINT,支持的数据库类型包括 MySQL、MariaDB、SQL Server;我们基于 MySQL 来看看 TINYINT
MySQL 官方对整数类型有如下介绍

除了 TINYINT,MySQL 还拓展出了 MEDIUMINT 和 BIGINT,这些都不是 SQL 标准整数类型,在做不同库数据迁移的时候需要考虑这些点
标准 SQL,便于迁移;做表设计的时候,尽量用 SQL 标准数据类型
TINYINT 存储空间占 1 字节,有符号的值范围是 -128 到 127,无符号的值范围是 0 到 255
TINYINT 的基本介绍已经完成,下面开始实操环节,开始之前我先问你们一个问题
在实际项目中,你们一般用 TINYINT 存什么 ?
是不是用来存枚举值?例如这样
`exec_status` TINYINT DEFAULT 0 COMMENT '执行-状态,0:等待中,1:执行中,2:成功,3:失败,4:终止'
甚至在枚举值少的时候,会使用 TINYINT(1),对不对?重点来了

TINYINT(1) 中的 1 表示什么?很多小伙伴会理所当然的回答道:数值范围,TINYINT(1) 表示的是数值范围是 -9 到 9,或者 0 ~ 9
对不对呢?我们验证下就知道了。基于 MySQL 8.0.31,我们创建表
CREATE TABLE `tbl_qsl_job` (
`id` int NOT NULL COMMENT '主键',
`exec_status` tinyint(1) DEFAULT 0 COMMENT '执行-状态,0:等待中,1:执行中,2:成功,3:失败,4:终止',
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
插入一条记录
INSERT INTO tbl_qsl_job(id, exec_status) VALUES(1, 12);
结果会怎么样,超出范围报错?
实际是插入成功

道心是不是碎了一地?

给你们 10 秒钟,收拾下破碎的道心;收拾好了之后我们一起看看官方说明:Numeric Type Attributes
MySQL supports an extension for optionally specifying the display width of integer data types in parentheses following the base keyword for the type. For example,
INT(4)specifies anINTwith a display width of four digits. This optional display width may be used by applications to display integer values having a width less than the width specified for the column by left-padding them with spaces. (That is, this width is present in the metadata returned with result sets. Whether it is used is up to the application.)The display width does not constrain the range of values that can be stored in the column. Nor does it prevent values wider than the column display width from being displayed correctly. For example, a column specified as
SMALLINT(3)has the usualSMALLINTrange of-32768to32767, and values outside the range permitted by three digits are displayed in full using more than three digits.
相信你们都能看懂,INT(n) 中的 n 表示的是显示宽度,INT(4) 表示的是显示宽度为四位数的 INT。应用程序可以采用左填充空格的方式来填充宽度不够列指定宽度的整数值(也就是说,此宽度会作为结果集的元数据返回,是否使用取决于应用程序)
显示宽度不会限制列存储的数值范围,也不会截断比列显示宽度更宽的值。例如,SMALLINT(3) 类型的列的存数范围与 SMALLINT 一样,也是 32768 到 32767,超出三位数的值会完整显示
关于整数类型显示宽度,我们可以进行如下总结
类型后面的 n,是显示宽度,表示显示时最少占 n 个字符宽度,既不会限制列存储的数值范围,也不会截断比列显示宽度更宽的值,
显示宽度会作为结果集的元数据返回,是否使用取决于应用程序(这个伏笔,后面会呼应,值得我们留意)
2 变 true
我们把 tbl_qsl_job 中 id = 1 的记录的状态改成 2(因为没有枚举值 12)
UPDATE tbl_qsl_job SET exec_status = 2 WHERE id = 1;
基于 SpringBoot 2.7.18、spring-jdbc 5.3.31 、HikariCP 4.0.3、mysql-connector-java 8.0.25 构建查询
package com.qsl;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.rowset.SqlRowSet;
import org.springframework.jdbc.support.rowset.SqlRowSetMetaData;
import javax.annotation.Resource;
/**
* @author youzb
*/
@SpringBootTest(classes = Application.class)
public class QslJobTest {
@Resource
private JdbcTemplate jdbcTemplate;
@Test
public void getByJdbcTemplate() {
SqlRowSet sqlRowSet = jdbcTemplate.queryForRowSet("SELECT * FROM tbl_qsl_job WHERE id = 1");
SqlRowSetMetaData metaData = sqlRowSet.getMetaData();
while (sqlRowSet.next()) {
for (int i = 1; i <= metaData.getColumnCount(); i++) {
System.out.print(sqlRowSet.getObject(i) + " ");
}
System.out.println();
}
}
}
你们觉得执行结果是怎样的,是不是 1 2 呢

可以看到,结果并不是 1 2,而是 1 true,是不是有点懵?
明明数据存的是 2,为什么查出来结果是 true?

首先我们可以确定的是,数据库存储的值是没问题的,因为通过 navicat 查到的结果是 2(也说明 navicat 没有对这个值做特殊转换)

回到我们的程序,数据库中的 2 到程序控制台的 true,经过了那些环节,我们是不是能整理出来?

数据库我们已经确定没问题了,至于是 mysql-connector-java 8.0.25、HikariCP 4.0.3、spring-jdbc 5.3.31 谁做了特殊处理,需要我们进一步排查了;最直接的方式就是 Debug 嘛,断点我已经替你们打好

此时到了 MySQL JDBC 驱动里面,我们来看一下 this.results 的信息,有几个点值得我们重点关注下
-
连接元信息

我们配置的连接 url 是
spring: datasource: url: jdbc:mysql://localhost:3306/fnj_test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC并未设置
tinyint1isBit值,所以其值 true 是mysql-connector-java 8.0.25给的默认值 -
列元信息

可以看到,字段 exec_status 的 length = 1,这个 length 表示什么呢,因为列 exec_status 的类型是整数类型,所以这个 length 是不是就是
显示宽度?不然还能代表什么?最重要的来了,
mysqlType的name是BIT
也就是说,mysql-connector-java 8.0.25 把 TINYINT(1) 解析成了 MysqlType.BIT,对应的 JAVA 类型就是
Boolean
既然 exec_status 的 JAVA 类型被解析成了 Boolean,其值 2 也就被解析成 true 了,所以标题的答案是不是就清楚了
mysql-connector-java 8.0.25 默认情况下,把 TINYINT(1) 解析成 JAVA 的 Boolean
问题又来了,mysql-connector-java 8.0.25 对 TINYINT 的解析逻辑是怎样的呢?我们跟下源码就知道了

我们先定位到 com.mysql.cj.protocol.a.ColumnDefinitionReader 的 180 行,然后往上找到 mysqlType 赋值的地方(ColumnDefinitionReade 134 行)
MysqlType mysqlType = NativeProtocol.findMysqlType(this.protocol.getPropertySet(), colType, colFlag, colLength, tableName, originalTableName,
collationIndex, encoding);
跟进 NativeProtocol.findMysqlType,我们能看到这样一段代码

第一个 if,我们分几部分解析下
-
isUnsigned
表示是否无符号,建表的时候,exec_status 并未明确指定
UNSIGNED,所以是有符号的,那么 isUnsigned 值是 false,取反则是 true -
length
表字段的显示宽度,也就是 TINYINT(1) 中的 1,所以 length == 1 的结果是 true
-
tinyInt1isBit
mysql-connector-java 8.0.25 的
PropertyDefinitions会静态初始化很多属性默认值,其中就包括 tinyInt1isBit
tinyInt1isBit 的默认值是 true
如果我们在数据库连接 url 中增加
tinyInt1isBit=falsespring: datasource: url: jdbc:mysql://localhost:3306/fnj_test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC&tinyInt1isBit=false那么配置值会覆盖默认值

setValueInternal(extractedValue, exceptionInterceptor); this.initialValue = this.value;这两行会分别将
value和initialValue设置成false回到 url 未配置 tinyInt1isBit 的情况,tinyInt1isBit 的值是默认值 true
-
内嵌 if 的 transformedBitIsBoolean
在分析 tinyInt1isBit 默认值的时候,也红框框住过 transformedBitIsBoolean 的默认值,是 false;没注意的小伙伴,可以往上翻一翻
所以 NativeProtocol.findMysqlType 的返回值是 MysqlType.BIT。如果 length > 1,那么返回的则是 MysqlType.TINYINT_UNSIGNED 或 MysqlType.TINYINT
自此,来龙去脉是不是清楚呢?
问题处理
既然已经找到问题原因,那么处理方式也就清晰了,有如下几种
-
url 中配置 tinyInt1isBit=false
这个会全局生效,有些需要将 TINYINT(1) 映射成 Boolean 的情况,会出问题
老项目不推荐增加该配置,除非确定整个项目中没有 TINYINT(1) 映射成 Boolean 的情况
新项目的话,可以增加该配置
-
字段类型调整成无符号
增加
UNSIGNED修饰,例如`exec_status` tinyint(1) unsigned DEFAULT 0 COMMENT '执行-状态,0:等待中,1:执行中,2:成功,3:失败,4:终止',但有个前提,枚举值不能出现负数
-
调大字段类型显示宽度
直接使用 TINYINT(4) 或 TINYINT
举一反三
既然是 mysql-connector-java 8.0.25 做了默认值的处理,那么 MyBatis-Plus 查的结果是不是也是将 2 处理成 true 呢?我们试一下就知道了,引入 MyBatis-Plus 3.5.7,测试代码很简单
@TableName("tbl_qsl_job")
public class QslJob {
private Integer id;
private Integer execStatus;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getExecStatus() {
return execStatus;
}
public void setExecStatus(Integer execStatus) {
this.execStatus = execStatus;
}
}
@Resource
private QslJobDao qslJobDao;
@Test
public void testMybatisPlus() {
QslJob qslJob = qslJobDao.selectById(1);
System.out.println(qslJob.getId() + " " + qslJob.getExecStatus());
}
你们觉得执行结果是怎样的,报错?输出 1 true?还是输出 1 2?执行下就知道了

查询结果正常!
按前面的分析,mysql-connector-java 8.0.25 返回 exec_status 的值是 true,Boolean 强转 Integer 会报错,即使不报错,结果也应该是 1(0 = false,1 = true)嘛,怎么会是 2 呢?

Debug 下你们就明白了

Mybatis 需要将 mysql-connector-java 8.0.25 查到的 ResultSet 映射成 QslJob,就需要用到类型处理器;QslJob 实体的 execStatus 是 Integer 类型的,那肯定用 IntegerTypeHandler 进行处理嘛,自然而然就用到 rs.getInt ,查到的结果自然就是 2 了。
我们再回过头去看 SqlRowSet,如果我们用 sqlRowSet.getInt 替换 sqlRowSet.getObject,是不是就行了?
@Test
public void getByJdbcTemplate() {
SqlRowSet sqlRowSet = jdbcTemplate.queryForRowSet("SELECT * FROM tbl_qsl_job WHERE id = 1");
SqlRowSetMetaData metaData = sqlRowSet.getMetaData();
while (sqlRowSet.next()) {
for (int i = 1; i <= metaData.getColumnCount(); i++) {
System.out.print(sqlRowSet.getInt(i) + " ");
}
System.out.println();
}
}
运行下,我们会发现报错了

这又是为什么?
原因在 JdbcTemplate 的数据提取

我们对 rs 进行一下 Evaluate,结果如下

此时 rs.getInt 能正常获取到 2,这说明什么?
mysql-connector-java 8.0.25 虽然把 TINYINT(1) 映射成了 JAVA 的 Boolean,但其查到的 ResultSet 中的数据仍是与数据库中数据一致的原始数据
HikariCP 4.0.3 也并未对原始数据做转换处理
我们继续跟进 rse.extractData(rs),跟进两层会来到关键点

rowSet.populate(rs) 会把 ResultSet rs 中的数据填充到 CachedRowSetImpl 的 rvh 中

我们接着跟进 sqlRowSet.getInt(i),答案即将揭晓

红框框住是不是就会抛异常了?我们继续跟进 this.getCurrentRow,会看到我们刚刚讲到的 rvh
protected BaseRow getCurrentRow() {
return (BaseRow)(this.onInsertRow ? this.insertRow : (BaseRow)((BaseRow)this.rvh.get(this.cursorPos - 1)));
}
是不是就呼应上了?这里有两个呼应
-
显示宽度
前面已经讲到,显示宽度会作为结果集的元数据返回,是否使用取决于应用程序
spring-jdbc 的 SqlRowSetResultSetExtractor#createSqlRowSet 方法用到了 CachedRowSetImpl#populate,在进行数据填充的过程中,会调用

mysql-connector-java 8.0.25 的 ResultSetImpl#getObject(int) 方法

是不是相当于用到了显示宽度?
-
rvh
CachedRowSetImpl#populate 方法,将数据填充到了 Vector

浙公网安备 33010602011771号