【DB系列】Mybatis之传参类型如何确定

文章目录
  1. I. 环境配置
    1. 1. 项目配置
    2. 2. 数据库表
  2. II. 传参类型确定
    1. 1. 参数类型为整形
    2. 2. 指定jdbcType
    3. 3. 传参类型为String
    4. 4. TypeHandler实现参数替换强制添加引号
    5. 5. 小结
  3. III. 不能错过的源码和相关知识点
    1. 0. 项目
    2. 1. 微信公众号: 一灰灰Blog

最近有小伙伴在讨论#{}${}的区别时,有提到#{}是用字符串进行替换,就我个人的理解,它的主要作用是占位,最终替换的结果并不一定是字符串方式,比如我们传参类型是整形时,最终拼接的sql,传参讲道理也应该是整形,而不是字符串的方式

接下来我们来看一下,mapper接口中不同的参数类型,最终拼接sql中是如何进行替换的

I. 环境配置

我们使用SpringBoot + Mybatis + MySql来搭建实例demo

  • springboot: 2.2.0.RELEASE
  • mysql: 5.7.22

1. 项目配置

1
2
3
4
5
6
7
8
9
10
11
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>

核心的依赖mybatis-spring-boot-starter,至于版本选择,到mvn仓库中,找最新的

另外一个不可获取的就是db配置信息,appliaction.yml

1
2
3
4
5
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password:

2. 数据库表

用于测试的数据库

1
2
3
4
5
6
7
8
9
10
CREATE TABLE `money` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
`money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0',
`create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=551 DEFAULT CHARSET=utf8mb4;

测试数据,主要是name字段,值为一个数字的字符串

1
2
3
INSERT INTO `money` (`id`, `name`, `money`, `is_deleted`, `create_at`, `update_at`)
VALUES
(120, '120', 200, 0, '2021-05-24 20:04:39', '2021-09-27 19:21:40');

II. 传参类型确定

本文忽略掉mybatis中的po、mapper接口、xml文件的详情,有兴趣的小伙伴可以直接查看最下面的源码(或者查看之前的博文也可以)

1. 参数类型为整形

针对上面的case,定义一个根据name查询数据的接口,但是这个name参数类型为整数

mapper接口:

1
2
3
4
5
6
/**
* int类型,最终的sql中参数替换的也是int
* @param name
* @return
*/
List<MoneyPo> queryByName(@Param("name") Integer name);

对应的xml文件如下

1
2
3
<select id="queryByName" resultMap="BaseResultMap">
select * from money where `name` = #{name}
</select>

上面这个写法非常常见了,我们现在的问题就是,传参为整数,那么最终的sql是 name = 120 还是 name = '120'呢?

那么怎么确定最终生成的sql是啥样的呢?这里介绍一个直接输出mysql执行sql日志的方式

在mysql服务器上执行下面两个命令,开启sql执行日志

1
2
set global general_log = "ON";
show variables like 'general_log%';

当我们访问上面的接口之后,会发现最终发送给mysql的sql语句中,参数替换之后依然是整数

1
select * from money where `name` = 120

2. 指定jdbcType

在使用#{}, ${}时,有时也会看到除了参数之外,还会指定jdbcType,那么我们在xml中指定这个对最终的sql生成会有影响么?

1
2
3
<select id="queryByNameV2" resultMap="BaseResultMap">
select * from money where `name` = #{name, jdbcType=VARCHAR} and 0=0
</select>

生成的sql如下

1
select * from money where `name` = 120 and 0=0

从实际的sql来看,这个jdbcType并没有影响最终的sql参数拼接,那它主要是干嘛用呢?(它主要适用于传入null时,类型转换可能出现的异常)

3. 传参类型为String

当我们传参类型为string时,最终的sql讲道理应该会带上引号

1
2
3
4
5
6
/**
* 如果传入的参数类型为string,会自动带上''
* @param name
* @return
*/
List<MoneyPo> queryByNameV3(@Param("name") String name);

对应的xml

1
2
3
<select id="queryByNameV3" resultMap="BaseResultMap">
select * from money where `name` = #{name, jdbcType=VARCHAR} and 1=1
</select>

上面这个最终生成的sql如下

1
select * from money where `name` = '120' and 1=1

4. TypeHandler实现参数替换强制添加引号

看完上面几节,基本上可以有一个得出一个简单的推论(当然对不对则需要从源码上分析了)

  • sql参数替换,最终并不是简单使用字符串来替换,实际上是由参数java的参数类型决定,若java参数类型为字符串,拼接的sql为字符串格式;传参为整型,拼接的sql也是整数

那么问题来了,为什么要了解这个?

  • 关键点在于索引失效的问题

比如本文实例中的name上添加了索引,当我们的sql是 select * from money where name = 120 会走不了索引,如果想走索引,要求传入的参数必须是字符串,不能出现隐式的类型转换

基于此,我们就有一个应用场景了,为了避免由于传参类型问题,导致走不了索引,我们希望name的传参,不管实际传入参数类型是什么,最终拼接的sql,都是字符串的格式;

我们借助自定义的TypeHandler来实现这个场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@MappedTypes(value = {Long.class, Integer.class})
@MappedJdbcTypes(value = {JdbcType.CHAR, JdbcType.VARCHAR, JdbcType.LONGVARCHAR})
public class StrTypeHandler extends BaseTypeHandler<Object> {

/**
* java 类型转 jdbc类型
*
* @param ps
* @param i
* @param parameter
* @param jdbcType
* @throws SQLException
*/
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, String.valueOf(parameter));
}

/**
* jdbc类型转java类型
*
* @param rs
* @param columnName
* @return
* @throws SQLException
*/
@Override
public Object getNullableResult(ResultSet rs, String columnName) throws SQLException {
return rs.getString(columnName);
}

@Override
public Object getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return rs.getString(columnIndex);
}

@Override
public Object getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return cs.getString(columnIndex);
}
}

然后在xml中,指定TypeHandler

1
2
3
4
5
6
/**
* 通过自定义的 TypeHandler 来实现 java <-> jdbc 类型的互转,从而实现即时传入的是int/long,也会转成String
* @param name
* @return
*/
List<MoneyPo> queryByNameV4(@Param("name") Integer name);
1
2
3
<select id="queryByNameV4" resultMap="BaseResultMap">
select * from money where `name` = #{name, jdbcType=VARCHAR, typeHandler=com.git.hui.boot.mybatis.handler.StrTypeHandler} and 2=2
</select>

上面这种写法输出的sql就会携带上单引号,这样就可以从源头上解决传参类型不对,导致最终走不了索引的问题

1
select * from money where `name` = '120' and 2=2

5. 小结

本文通过一个简单的实例,来测试Mapper接口中,不同的参数类型,对最终的sql生成的影响

  • 参数类型为整数时,最终的sql的参数替换也是整数(#{}并不是简单的字符串替换哦)
  • 参数类型为字符串时,最终的sql参数替换,会自动携带''${}注意它不会自动带上单引号,需要自己手动添加)

当我们希望不管传参什么类型,最终生成的sql,都是字符串替换时,可以借助自定义的TypeHandler来实现,这样可以从源头上避免因为隐式类型转换导致走不了索引问题

最后疑问来了,上面的结论靠谱么?mybatis中最终的sql是在什么地方拼接的?这个sql拼接的流程是怎样的呢?

关于sql的拼接全流程,后续博文即将上线,我是一灰灰,走过路过的各位大佬帮忙点个赞、价格收藏、给个评价呗

III. 不能错过的源码和相关知识点

0. 项目

系列博文:

1. 微信公众号: 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

一灰灰blog


打赏 如果觉得我的文章对您有帮助,请随意打赏。
分享到