鄂州市网站建设_网站建设公司_Angular_seo优化
2026/1/19 9:03:29 网站建设 项目流程

开心一刻

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

TINYINT

关于 TINYINT,我相信大家都知道它,是数据库的一种数据类型,说的详细点,它是数据库的一种数字类型,再详细点,它是数据库的一种整数类型;需要注意的是,它并非 SQL 标准整数类型

SQL标准整数类型:INTEGER(orINT) and SMALLINT

而是某些数据库的拓展整数类型,所以并非所有的关系型数据库都支持 TINYINT,支持的数据库类型包括 MySQLMariaDBSQL Server;我们基于 MySQL 来看看 TINYINT

MySQL 官方对整数类型有如下介绍

整数类型

除了 TINYINT,MySQL 还拓展出了 MEDIUMINTBIGINT,这些都不是 SQL 标准整数类型,在做不同库数据迁移的时候需要考虑这些点

标准 SQL,便于迁移;做表设计的时候,尽量用 SQL 标准数据类型

TINYINT 存储空间占 1 字节,有符号的值范围是 -128 到 127,无符号的值范围是 0 到 255

TINYINT 的基本介绍已经完成,下面开始实操环节,开始之前我先问你们一个问题

在实际项目中,你们一般用 TINYINT 存什么 ?

是不是用来存枚举值?例如这样

`exec_status` TINYINT DEFAULT 0 COMMENT '执行-状态,0:等待中,1:执行中,2:成功,3:失败,4:终止'

甚至在枚举值少的时候,会使用 TINYINT(1),对不对?重点来了

640 (8)

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

结果会怎么样,超出范围报错?

实际是插入成功

tinyint1_插入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 an INT with 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 usual SMALLINT range of -32768 to 32767, 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.18spring-jdbc 5.3.31 HikariCP 4.0.3mysql-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 {@Resourceprivate JdbcTemplate jdbcTemplate;@Testpublic 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

2变true

可以看到,结果并不是 1 2,而是 1 true,是不是有点懵?

明明数据存的是 2,为什么查出来结果是 true?

懵

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

数据库中值是2

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

数据流转

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

断点debug

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

  1. 连接元信息

    连接元信息

    我们配置的连接 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 给的默认值

  2. 列元信息

    列元信息

    可以看到,字段 exec_status 的 length = 1,这个 length 表示什么呢,因为列 exec_status 的类型是整数类型,所以这个 length 是不是就是 显示宽度 ?不然还能代表什么?

    最重要的来了,mysqlTypenameBIT

    mysqltype_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 的解析逻辑是怎样的呢?我们跟下源码就知道了

mysqlType赋值逻辑

我们先定位到 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,我们能看到这样一段代码

tinyint赋值逻辑

第一个 if,我们分几部分解析下

  1. isUnsigned

    表示是否无符号,建表的时候,exec_status 并未明确指定 UNSIGNED,所以是有符号的,那么 isUnsigned 值是 false,取反则是 true

  2. length

    表字段的显示宽度,也就是 TINYINT(1) 中的 1,所以 length == 1 的结果是 true

  3. tinyInt1isBit

    mysql-connector-java 8.0.25 的 PropertyDefinitions 会静态初始化很多属性默认值,其中就包括 tinyInt1isBit

    tinyInt1isBit默认值

    tinyInt1isBit 的默认值是 true

    如果我们在数据库连接 url 中增加 tinyInt1isBit=false

    spring:datasource:url: jdbc:mysql://localhost:3306/fnj_test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC&tinyInt1isBit=false
    

    那么配置值会覆盖默认值

    tinyInt1isBit值覆盖
    setValueInternal(extractedValue, exceptionInterceptor);
    this.initialValue = this.value;
    

    这两行会分别将 valueinitialValue 设置成 false

    回到 url 未配置 tinyInt1isBit 的情况,tinyInt1isBit 的值是默认值 true

  4. 内嵌 if 的 transformedBitIsBoolean

    在分析 tinyInt1isBit 默认值的时候,也红框框住过 transformedBitIsBoolean 的默认值,是 false;没注意的小伙伴,可以往上翻一翻

所以 NativeProtocol.findMysqlType 的返回值是 MysqlType.BIT。如果 length > 1,那么返回的则是 MysqlType.TINYINT_UNSIGNEDMysqlType.TINYINT

自此,来龙去脉是不是清楚呢?

问题处理

既然已经找到问题原因,那么处理方式也就清晰了,有如下几种

  1. url 中配置 tinyInt1isBit=false

    这个会全局生效,有些需要将 TINYINT(1) 映射成 Boolean 的情况,会出问题

    老项目不推荐增加该配置,除非确定整个项目中没有 TINYINT(1) 映射成 Boolean 的情况

    新项目的话,可以增加该配置

  2. 字段类型调整成无符号

    增加 UNSIGNED 修饰,例如

    `exec_status` tinyint(1) unsigned DEFAULT 0 COMMENT '执行-状态,0:等待中,1:执行中,2:成功,3:失败,4:终止',
    

    但有个前提,枚举值不能出现负数

  3. 调大字段类型显示宽度

    直接使用 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;}
}@Resourceprivate QslJobDao qslJobDao;@Testpublic void testMybatisPlus() {QslJob qslJob = qslJobDao.selectById(1);System.out.println(qslJob.getId() + " " + qslJob.getExecStatus());}

你们觉得执行结果是怎样的,报错?输出 1 true?还是输出 1 2?执行下就知道了

tinyint(1)_mybatis-plus

查询结果正常!

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

好难 不该学编程的

Debug 下你们就明白了

mybatis_getInt

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

运行下,我们会发现报错了

getInt报

这又是为什么?

原因在 JdbcTemplate 的数据提取

extractData

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

rs_evaluate

此时 rs.getInt 能正常获取到 2,这说明什么?

mysql-connector-java 8.0.25 虽然把 TINYINT(1) 映射成了 JAVA 的 Boolean,但其查到的 ResultSet 中的数据仍是与数据库中数据一致的原始数据

HikariCP 4.0.3 也并未对原始数据做转换处理

我们继续跟进 rse.extractData(rs),跟进两层会来到关键点

cachedRowSet

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

rhv

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

getInt_error

红框框住是不是就会抛异常了?我们继续跟进 this.getCurrentRow,会看到我们刚刚讲到的 rvh

protected BaseRow getCurrentRow() {return (BaseRow)(this.onInsertRow ? this.insertRow : (BaseRow)((BaseRow)this.rvh.get(this.cursorPos - 1)));
}

是不是就呼应上了?这里有两个呼应

  1. 显示宽度

    前面已经讲到,显示宽度会作为结果集的元数据返回,是否使用取决于应用程序

    spring-jdbc 的 SqlRowSetResultSetExtractor#createSqlRowSet 方法用到了 CachedRowSetImpl#populate,在进行数据填充的过程中,会调用

    调用myql驱动的getObject

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

    BIT处理

    是不是相当于用到了显示宽度?

  2. rvh

    CachedRowSetImpl#populate 方法,将数据填充到了 Vector rvh 中

    而我们写的代码 sqlRowSet.getInt(i) 是从这个 rvh 中获取的

    所以,归根结底是 spring-jdbc 将 ResultSet 处理成 CachedRowSetImpl 的机制,我们没有遵循,才会出现 2 变 true 这种意料之外的结果

    SQL Server 的 tinyint 会不会有类似问题呢?我可以肯定的告诉你们,目前不会有,因为 SQL Server 的 tinyint 没有显示宽度一说,就是固定的 tinyint,不支持 tinyint(n),至于未来是否支持,未来再说嘛

    总结

    1. MySQL 整数类型后面的 n 是显示宽度,表示显示时最少占 n 个字符宽度,既不会限制列存储的数值范围,也不会截断比列显示宽度更宽的值

    2. MySQL 会把显示宽度作为结果集的元数据返回,所以 mysql-connector-java 8.0.25 能获取到

    3. mysql-connector-java 8.0.25 虽然有显示宽度,也提供了间接用到显示宽度的getObject方法,但其 ResultSet 中的数据与数据库中的数据是一致的

      至于上层应用如何使用 mysql-connector-java 8.0.25 中的元数据,由上层应用决定;spring-jdbc 就是上层应用之一、MyBatis 也是

    4. spring-jdbc 的 JdbcTemplate.queryForRowSet 会间接使用显示宽度,而 Mybatis 不会,MyBatis 有类型处理器

    5. 如何避免标题中的问题,文中的 问题处理 章节已经说明,推荐第三种方式

      不使用 TINYINT(1),直接使用 TINYINT(4) 或 TINYINT

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询