在第一篇中,我们搭建了 MyBatis 环境,并完成了第一个查询。但老铁们可能对 MyBatis 如何接收参数、如何将结果映射成对象感到好奇。
今天我们就来搞懂这些问题,通过注解完成增删改查,并深入 MyBatis 的"后台",看看它到底是怎么处理参数、怎么自动赋值的。
1. 配置日志:让 MyBatis "开口说话"
1.1 为什么需要日志?
目前我们执行查询,控制台只输出结果,却看不到 MyBatis 到底执行了什么 SQL、传递了什么参数。如果 SQL 写错了,我们根本不知道错误出在哪。配置日志后,MyBatis 会把执行的 SQL 语句、参数以及返回的行数打印出来,就像给 MyBatis 装了一个"监控器",让我们能清楚地看到它的一举一动。
1.2 怎么配置?
在 application.yml 中添加:
yaml
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
如果使用 application.properties,则是:
properties
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
配置后重新运行测试,你会看到类似这样的输出:

Preparing后面是 MyBatis 预编译后的 SQL(占位符?代替了具体值)。Parameters是实际传递的参数值。Columns是返回的列名。Row是具体的数据行。Total是返回的记录数。
1.3 原理是什么?
简单来说就是我们的MyBatis 内部集成了多种日志框架(如 SLF4J、Log4j、Logback 等),并通过日志适配器 统一输出。StdOutImpl 是 MyBatis 提供的一个简单实现,它直接把日志打印到控制台上,所以你才会看到上述的日志信息。
一旦我们配置日志之后,MyBatis 在执行 SQL 的各个阶段(如参数设置、执行、结果处理)都会调用日志输出方法,从而打印出这些信息。
2. 参数传递:从方法到数据库的完整旅程
参数传递是 MyBatis 最核心的功能之一。我们需要把 Java 方法的参数传递给 SQL 语句中的占位符,MyBatis 在背后做了很多工作。下面我们分五种常见情况,一步步介绍 MyBatis 的处理流程,那么老铁们继续往下看:
2.1 总体流程概览
无论哪种参数情况,MyBatis 处理参数的整体流程都是类似的,如下图所示:

注意:为了便于理解,我们将 MyBatis 处理参数的 "数据源" 抽象为 "参数容器",
"参数容器"是关键,它可以是:
- 单个简单值(如 Integer、String)
- 一个 Java 对象(如 Student)
- 一个 Map 集合
下面我们分别来看。
2.2 情况一:单个简单参数
场景:根据 ID 查询学生。
java
//根据id查询学生
@Select("select id, name, age from student where id = #{id}")
Student findById(Integer id);
测试:

结果:

MyBatis 内部处理步骤:
- 方法调用 :传入一个 Integer 值,比如
1。 - 参数解析 :
ParamNameResolver发现只有一个参数,且没有@Param注解。它决定不构建 Map,而是直接保留这个参数值作为"参数源"。 - SQL 解析 :当解析到
#{id}时,MyBatis 知道要从参数源中取值。由于只有一个参数,无论#{}里写什么名字,都取这个唯一的参数值。这一步相当于参数值 = 1。 - 设置参数 :MyBatis 调用
PreparedStatement.setInt(1, 1)将参数设置到第一个占位符。 - 执行 SQL。
图示:

关键点:单个简单参数时,MyBatis 不会创建 Map,而是直接持有参数值。无论 #{} 里写什么名字(如 #{id}/#{xxx}/#{arg0}),都会取这个唯一的参数值;但为了代码可读性,建议 #{} 内的名称与方法参数名保持一致。
2.3 多个参数(无 @Param)
多个参数(无 @Param)的真相:取决于编译参数
你可能会在不同项目中发现奇怪的现象:有时不加 @Param 也能用 #{name} 正常工作,有时却报错。这是为什么呢?我们先来看一个具体场景。
场景:根据姓名和年龄查询学生。
java
// 传递多个参数,没有 @Param
@Select("SELECT * FROM student WHERE name = #{name} AND age = #{age}")
Student findByNameAndAge(String name, Integer age);
调用:findByNameAndAge("张三", 18)
这个代码一定能工作吗?
答案:不一定 。它能否工作取决于你的项目编译时是否保留了方法参数的真实名称 (即 name 和 age)。
情况 1:编译时未保留参数名(传统默认行为)
如果你使用的编译器没有开启保留参数名的选项,那么编译后的 .class 文件中,参数名会变成 arg0、arg1 之类的占位符。此时 MyBatis 通过反射获取到的参数名就是 arg0 和 arg1。它会像下面这样处理:
- 方法调用 :传入
"张三"和18。 - 参数解析 :
ParamNameResolver发现有多个参数,且没有@Param,它会创建一个 ParamMap (一个特殊的 Map),并放入两套键值对:- 按索引:
arg0 → "张三",arg1 → 18 - 按顺序:
param1 → "张三",param2 → 18
最终 Map 中包含 4 个条目。
- 按索引:
- SQL 解析 :当解析
#{name}时,MyBatis 去 Map 中找 key 为"name"的项,但 Map 中没有,所以报错(如Parameter 'name' not found)。如果改为#{arg0}或#{param1},就能找到对应的值。 - 设置参数 :如果使用
#{arg0}和#{arg1},MyBatis 从 Map 中取出值,设置到 PreparedStatement。
图示(未保留参数名时):

再举一个例子:如果你的项目不会保留参数名(编译后参数名变成 arg0/arg1);
那么对于接口:

而我们的sql:

此时运行之后,我们就会出现一下的报错:

如果我们加上注解@param 那么此时就不会出现这个错误

此时我们在控制台上就不会出现错误日志了而是出现正确的结果:

情况 2:编译时保留了参数名(如 Spring Boot 默认行为)
可能在以前的版本会说,不加 @Param 就必须用 #{arg0}、#{param1} 之类的默认名称,否则会报错。但在现代开发环境(尤其是 Spring Boot)中,你可能会发现下面这段代码竟然能正常工作:
java
// 传递多个参数,没有 @Param
@Select("select * from student where name = #{name} and age = #{age}")
Student findByNameAndAge(String name, Integer age);
测试:

结果:

那么调用 findByNameAndAge("张三", 18) 后,数据正确返回,并没有报错。这是为什么呢?
原因:编译时保留了参数真实名称
从 Java 8 开始,可以通过编译参数 -parameters 让编译器在 .class 文件中保留方法参数的真实名称 (例如 name、age)。而我们的Spring Boot 的 Maven/Gradle 插件默认开启了这一选项,因此我们在 Spring Boot 项目中写上述代码,MyBatis 能够通过反射获取到参数的真实名字,并将它们也作为 key 放入参数容器中。那么具体的步骤是什么呢?
MyBatis 内部处理步骤
- 方法调用 :传入
"张三"和18。 - 参数解析 :
ParamNameResolver参数名称解析器 发现有多个参数,且没有@Param。它会自动创建一个ParamMap(一个特殊的 Map),并放入多套键值对:- 如果编译时保留了参数名,就会包含真实参数名:
name → "张三",age → 18 - 同时,为了兼容性,还会放入按索引的默认名:
arg0 → "张三",arg1 → 18 - 以及按顺序的通用名:
param1 → "张三",param2 → 18
最终 Map 中包含多个条目。
- 如果编译时保留了参数名,就会包含真实参数名:
- SQL 解析 :当解析
#{name}时,MyBatis 从 Map 中找 key 为"name"的项,此时 Map 中已经存在name,所以能取出"张三"。同理#{age}取出 18。 - 设置参数:MyBatis 将取出的值设置到 PreparedStatement。
可能老铁们读起来会有点抽象,所以我们使用一个图来描述上述的流程:
图示

关键点
-
为什么现在能直接使用
#{name}?因为 Spring Boot 默认开启了编译参数
-parameters,保留了参数的真实名称,MyBatis 在构建ParamMap时会把真实参数名也作为 key。 -
是否意味着以后都可以不加
@Param?虽然不加
@Param在 Spring Boot 环境中能工作,但强烈建议仍然使用@Param注解。原因:- 显式意图 :看到
@Param("name"),代码阅读者立刻知道 SQL 中的#{name}对应这个参数。 - 环境无关 :如果你的代码被移植到未开启
-parameters的项目中,加了@Param依然能正确运行。 - 避免混淆:防止后续维护者误以为参数名可以随意使用而引发错误。
- 显式意图 :看到
所以,虽然现在我们虽然可以不加 @Param 也能跑通,但养成加 @Param 的习惯,能让你的代码更健壮、更清晰。
2.4 情况三:多个参数(使用 @Param)
场景 :使用 @Param 明确指定参数名。
java
@Select("select * from student where name = #{name} and age = #{age}")
Student findByNameAndAge(@Param("name") String name, @Param("age") Integer age);
测试:

结果:

MyBatis 内部处理步骤:
-
方法调用 :传入
"李四"和20。 -
参数解析 :
ParamNameResolver发现有多个参数,且都带有@Param注解。它创建一个ParamMap,以注解的值为 key,参数值为 value:"name" → "李四""age" → 20
同时,为了兼容性,也会保留默认的arg0/arg1和param1/param2。
-
SQL 解析(BoundSql) :读取原始 SQL,扫描所有
#{}占位符,将其替换为?,得到预编译 SQL:sqlselect * from student where name = ? and age = ?同时,记录每个占位符对应的属性名 :第一个占位符属性名为
"name",第二个为"age"。 -
从参数容器中取值 :遍历每个占位符,根据属性名从
ParamMap中取出实际的值:- 属性名
"name"→map.get("name")→"李四" - 属性名
"age"→map.get("age")→20
- 属性名
-
设置 PreparedStatement :将取出的值按顺序设置到
?上:- 第一个
?设置为"李四"(setString) - 第二个
?设置为20(setInt)
- 第一个
-
执行 SQL。
那么这个流程对应的图示如下:

关键点:
- 使用
@Param可以自定义 Map 的 key,让 SQL 中的#{}能直接引用这些名字,清晰易懂。 - 虽然默认的
arg0、param1等 key 仍然存在,但推荐只使用自定义 key,避免混乱。 - 整个流程严格遵循:先解析 SQL 生成占位符并记录属性名,再从参数容器中取值,最后设置参数。
2.5 情况四:单个对象参数
场景:插入一个学生对象。
java
@Insert("INSERT INTO student(name, age, gender) VALUES(#{name}, #{age}, #{gender})")
int insert(Student student);
测试:

结果:

MyBatis 内部处理步骤:
-
方法调用 :传入一个 Student 对象,里面有
name="王五",age=22,gender=1。 -
参数解析 :
ParamNameResolver发现只有一个参数,且是对象类型。它不会创建 Map,而是直接把这个对象作为"参数源"。 -
SQL 解析(BoundSql) :读取原始 SQL,扫描所有
#{}占位符,将其替换为?,得到预编译 SQL:sqlinsert into student(name, age, gender) values(?, ?, ?)同时,记录每个占位符对应的属性名 :第一个占位符属性名为
"name",第二个为"age",第三个为"gender"。 -
从参数容器中取值 :遍历每个占位符,根据属性名从参数源(即 student 对象)中取出实际的值。MyBatis 使用 OGNL(Object-Graph Navigation Language,对象图导航语言, 该知识在后面有补充第7个知识点) 表达式通过反射调用对应的 getter 方法:
- 属性名
"name"→ 调用student.getName()→"王五" - 属性名
"age"→ 调用student.getAge()→22 - 属性名
"gender"→ 调用student.getGender()→1
- 属性名
-
设置 PreparedStatement :将取出的值按顺序设置到
?上:- 第一个
?设置为"王五"(setString) - 第二个
?设置为22(setInt) - 第三个
?设置为1(setInt)
- 第一个
-
执行 SQL。
图示:

关键点:
- 单个对象参数时,MyBatis 不会创建 Map,而是直接使用对象本身作为参数源。
#{}中的名字必须是对象的属性名,MyBatis 会通过 OGNL 调用对应的 getter 方法取值。- 整个流程遵循:先解析 SQL 生成占位符并记录属性名,再通过 OGNL 从对象取值,最后设置参数。
2.6 情况五:混合参数(对象 + 简单参数)
场景:根据学生对象的部分属性和额外参数查询。
java
@Select("select * from student where name = #{stu.name} and age > #{minAge}")
List<Student> findByNameAndMinAge(@Param("stu") Student student, @Param("minAge") Integer minAge);
测试

结果:

如果不使用stu.就会报错:


那么底层到底做了什么?为什么需要有stu.呢?
MyBatis 内部处理步骤:
-
方法调用 :传入一个 Student 对象(name="赵六", age=25)和一个 Integer
18。 -
参数解析 :
ParamNameResolver发现有两个参数,且都有@Param注解。它创建一个ParamMap:"stu" → student对象"minAge" → 18
同时,为了兼容性,也会保留默认的arg0/arg1和param1/param2。
-
SQL 解析(BoundSql) :读取原始 SQL,扫描所有
#{}占位符,将其替换为?,得到预编译 SQL:sqlselect * from student where name = ? and age > ?同时,记录每个占位符对应的属性名 :第一个占位符属性名为
"stu.name",第二个为"minAge"。 -
从参数容器中取值 :遍历每个占位符,根据属性名从
ParamMap中取值:- 对于
#{stu.name},MyBatis 先解析出两部分:对象引用stu和属性name。它先从 Map 中取 key 为"stu",得到 student 对象,然后通过 OGNL 从该对象中取name属性(即student.getName())。 - 对于
#{minAge},直接从 Map 中取 key 为"minAge"的值,得到18。
- 对于
-
设置 PreparedStatement :将取出的值按顺序设置到
?上:- 第一个
?设置为"赵六"(setString) - 第二个
?设置为18(setInt)
- 第一个
-
执行 SQL。
图示:

关键点:
- 混合参数时,所有参数(包括对象)都被放入
ParamMap中。 - 对于
#{stu.name}这种点号表达式,MyBatis 会先从 Map 中取出对象,再通过 OGNL 取对象属性。 - 整个流程依然遵循:先解析 SQL 生成占位符并记录属性名,再从参数容器中取值,最后设置参数。
对比2.5和2.6的总结:对于参数是对象的,如果不加@Param的话,那么可以使用属性【因为对象就是一个容器】,如果加了@Param注解的话,那么属性就得需要使用对象来点。

3. 参数传递总结
| 参数情况 | MyBatis 内部构建 | SQL 中如何引用 | 底层原理 |
|---|---|---|---|
| 单个简单参数 | 直接保存参数值 | #{任意名} |
直接取值,不涉及 Map |
| 单个对象参数 | 直接保存对象 | #{属性名} |
通过 OGNL 从对象取属性值 |
| 多个参数(无@Param) | 创建 ParamMap,key 为 arg0/arg1, param1/param2 | #{arg0} 或 #{param1} |
从 Map 取默认 key |
| 多个参数(有@Param) | 创建 ParamMap,key 为 @Param 指定的值 | #{指定名} |
从 Map 取指定 key |
| 混合参数 | 创建 ParamMap,包含对象和简单值 | 对象属性用 #{key.属性名},简单值直接用 #{key} |
先从 Map 取对象,再 OGNL 取属性 |
核心思想 :MyBatis 通过一个"参数容器"来统一管理传入的参数。这个容器要么是单个值/对象,要么是 Map。SQL 中的 #{} 占位符根据名称从容器中取值。理解了这个容器模型,你就掌握了 MyBatis 参数传递的精髓。
4. 增删改操作
增删改操作是数据库最基本的操作,它们背后同样遵循我们刚刚学过的参数传递规则。下面我们逐一拆解,看看 MyBatis 是如何处理这些操作的,以及为什么这么设计。
4.1 插入数据(Insert)
4.1.1 插入一个对象:为什么 #{} 里是属性名?
我们先看一个标准的插入方法:
java
@Insert("INSERT INTO student(name, age, gender) VALUES(#{name}, #{age}, #{gender})")
int insert(Student student);
你可能会问:为什么 #{name} 能直接取到 Student 对象的 name 属性?MyBatis 是怎么知道 #{name} 对应 student.getName() 的?
底层原理:
- 参数容器:因为只有一个参数且是对象类型,MyBatis 不会创建 Map,而是直接把这个 Student 对象作为"参数源"。
- OGNL 表达式 :当解析 SQL 中的
#{name}时,MyBatis 使用 OGNL(对象图导航语言) 来从参数源中取值。OGNL 本质上是一个表达式引擎,它能够根据字符串表达式(比如"name")去调用对象的 getter 方法。 - 反射调用 :OGNL 会通过反射找到
student.getName()方法并执行,从而拿到属性值。
所以,#{} 里的名字必须和对象的属性名一致,因为 MyBatis 会把这个名字当作 OGNL 表达式去对象里找对应的 getter 方法。如果名字不对,比如写成 #{username},而 Student 类里没有 getUsername() 方法,就会抛出异常。
4.1.2 插入后为什么需要获取主键?怎么获取?
为什么需要主键?
在很多业务场景中,插入一条数据后,我们需要知道这条数据的 ID,以便进行后续操作。比如:
- 电商系统中,下单后需要拿着订单 ID 去通知库存、物流系统。
- 社交系统中,发布文章后需要返回文章 ID 供前端跳转。
- 关联操作中,需要在其他表中引用新插入数据的 ID。
如果插入后拿不到 ID,我们就得再用查询语句去查一遍,既浪费资源又麻烦。所以 MyBatis 提供了直接获取自增主键的功能。
怎么获取?
使用 @Options 注解:
java
@Options(useGeneratedKeys = true, keyProperty = "id")
@Insert("INSERT INTO student(name, age, gender) VALUES(#{name}, #{age}, #{gender})")
int insert(Student student);
useGeneratedKeys = true:useGeneratedKeys 译为使用自动生成的键值,告诉 MyBatis,我们想要数据库自动生成的主键。keyProperty = "id":告诉 MyBatis,把生成的主键值赋值给传入对象的哪个属性(这里是id属性)。
底层原理是什么?
这个功能的底层依赖于 JDBC 的 getGeneratedKeys() 方法。让我们看看完整的流程:
-
JDBC 层面 :当我们通过
PreparedStatement执行插入时,如果数据库支持自增主键,JDBC 允许我们在创建PreparedStatement时指定一个标志,表示想要返回生成的主键。例如:javaps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);执行插入后,可以通过
ps.getGeneratedKeys()获取一个ResultSet,里面就是生成的主键。 -
MyBatis 层面 :当我们在
@Options中设置了useGeneratedKeys=true,MyBatis 在内部创建PreparedStatement时就会加上Statement.RETURN_GENERATED_KEYS标志。插入执行完毕后,MyBatis 会自动调用getGeneratedKeys()拿到主键值,然后通过反射找到传入对象的setId()方法,把主键值设置进去。
取出来存在哪里?
主键值被存回了你传入的那个对象中。也就是说,执行完插入方法后,你之前创建的 student 对象的 id 属性就不再是 null 了,而是变成了数据库生成的值。这样你就可以直接从原对象中拿到主键,非常方便。
4.1.3 插入成功返回的结果是什么?
插入方法的返回值类型通常是 int,表示受影响的行数。如果插入成功一条,返回值就是 1;如果插入多条(批量插入),返回值就是成功插入的条数。
即使我们通过 @Options 获取了主键,返回值依然是受影响的行数,而不是主键。主键是通过对象属性带回来的。
4.2 删除数据(Delete)
4.2.1 删除为什么通常根据 id 删除?
删除操作的核心是精准定位要删除的记录。在关系型数据库中,主键(通常是 id)是唯一标识一条记录的字段。通过主键删除可以确保只删除目标记录,不会误删其他数据。
如果我们用其他字段(比如 name)删除,可能会删除多条同名记录,造成数据丢失。所以,标准的删除操作都是根据主键进行的。
4.2.2 删除方法的参数传递
删除方法通常接收一个 id 参数,这是一个单个简单参数:
java
@Delete("DELETE FROM student WHERE id = #{id}")
int deleteById(Integer id);
- 调用时传入一个 Integer 值,比如
deleteById(5)。 - 根据我们之前学的参数传递规则,单个简单参数不会被包装成 Map,而是直接作为参数源。
- MyBatis 解析
#{id}时,直接从参数源中取值(5),设置到 PreparedStatement 中。
4.2.3 删除成功返回的结果是什么?
删除方法的返回值也是 int,表示受影响的行数。如果删除成功一条,返回 1;如果删除的记录不存在,返回 0。我们可以根据返回值判断是否真的删除了数据。
4.3 更新数据(Update)
4.3.1 更新为什么通常根据 id 修改?
和删除一样,更新也需要精确定位到要修改的记录。主键 id 是唯一标识,所以更新条件中几乎总是包含 id,确保只修改目标记录,不会改到其他数据。
4.3.2 更新方法的两种常见形式
形式一:接收多个参数(id + 要修改的字段)
java
@Update("UPDATE student SET name = #{name}, age = #{age} WHERE id = #{id}")
int updateById(@Param("id") Integer id, @Param("name") String name, @Param("age") Integer age);
- 这里有三个参数,我们用了
@Param指定名字。 - MyBatis 会构建一个 ParamMap,key 为
"id"、"name"、"age"。 - 解析 SQL 时,从 Map 中取出对应的值设置到 PreparedStatement。
形式二:传递一个对象,根据对象里的 id 修改
java
@Update("UPDATE student SET name = #{name}, age = #{age} WHERE id = #{id}")
int update(Student student);
- 这是一个单个对象参数。
- MyBatis 直接把 student 对象作为参数源,通过 OGNL 从对象中取出
name、age、id的值。 - 注意:SQL 中的
#{id}就是从 student 对象的getId()方法取值。这就要求传入的 student 对象必须已经设置了 id(即你要修改哪条记录)。
为什么可以用对象?
因为更新时,我们通常会把要修改的数据封装成一个对象,包括 id。这样代码更简洁,而且和插入操作风格一致。
4.3.3 更新成功返回的结果是什么?
更新方法同样返回 int,表示受影响的行数。如果更新成功一条,返回 1;如果更新的记录不存在,返回 0。我们可以通过返回值判断更新是否生效。
5. 增删改操作中的参数传递总结
| 操作 | 常见参数形式 | 参数类型 | MyBatis 处理方式 | SQL 中引用 | 返回值含义 |
|---|---|---|---|---|---|
| 插入 | 单个对象 | 对象参数 | 直接使用对象作为参数源,通过 OGNL 取属性值 | #{属性名} |
受影响行数(通常为 1) |
| 插入(带主键返回) | 单个对象 | 对象参数 | 同上,但执行后会通过反射将生成的主键设置回对象 | #{属性名} |
受影响行数,主键在对象中 |
| 删除 | 单个简单参数(id) | 简单参数 | 直接持有参数值 | #{任意名} |
受影响行数 |
| 删除 | 多个参数(如根据复合条件) | 多参数(有@Param) | 构建 ParamMap | #{指定key} |
受影响行数 |
| 更新 | 多个参数(id+字段) | 多参数(有@Param) | 构建 ParamMap | #{指定key} |
受影响行数 |
| 更新 | 单个对象(含 id) | 对象参数 | 直接使用对象作为参数源,通过 OGNL 取属性值 | #{属性名} |
受影响行数 |
核心思想:增删改操作的本质就是 SQL 执行,参数传递规则与查询完全一致。理解了参数容器模型,你就掌握了所有操作的参数传递。
5. 查询结果映射:解决字段名不一致的问题
5.1 查询结果是如何变成 Java 对象的?
当我们执行一条查询 SQL 后,数据库会返回一个 ResultSet 结果集,你可以把它想象成一个表格------有行有列。MyBatis 需要把表格中的 每一行数据 转换成一个 Java 对象 ( 比如 Student 对象),这个转换过程就叫 结果映射(Result Mapping)。
MyBatis 的默认映射规则非常直接:它会遍历结果集中的每一列,获取该列的列名,然后在目标 Java 类中查找 同名属性 (忽略大小写)。如果找到了对应的属性,MyBatis 就会通过反射调用该属性的 setter 方法,将这一列的值赋进去。
举个例子,假设 SQL 返回的列名是 id、name、age,MyBatis 就会去 Student 类中找 id、name、age 这三个属性,并分别调用 setId、setName、setAge 方法把值填进去。这个过程对开发者完全透明,我们只需要定义好 Java 类,MyBatis 就自动帮我们把数据库的行变成了对象。
这种默认规则在列名和属性名完全一致 (或仅有大小写区别)时工作得很好。但现实开发中,数据库的命名习惯往往是下划线命名 (如 create_time),而 Java 属性习惯驼峰命名 (如 createTime)。这就导致默认匹配失效------MyBatis 拿着 create_time 这个列名去类里找,找不到叫 create_time 的属性,于是这一列就被忽略,对应的 Java 属性就保持默认值 null。
5.2 现象回顾:为什么 createTime 是 null?
我们之前查询所有学生:
java
@Select("select id, name, age, gender, create_time, update_time from student")
List<Student> findAll();
结果:

运行后,你会发现 createTime 和 updateTime 这两个属性是 null。原因就是上面说的:数据库返回的列名是 create_time 和 update_time,而 Student 类的属性名是 createTime 和 updateTime,两者并不相等(即使忽略大小写,下划线和驼峰也不同)。MyBatis 找不到对应的属性,自然不会赋值。
为了解决这个问题,MyBatis 提供了三种方式,让我们可以告诉它"这个列应该对应哪个属性"。
5.3 解决方法一:SQL 别名
最直接的方法就是在 SQL 中给列起别名,让别名直接等于 Java 属性名。这样 MyBatis 看到的列名就和属性名一致了,自动映射就能成功。
java
@Select("select id, name, age, gender, " +
"create_time as createTime, update_time as updateTime " +
"from student")
List<Student> findAll();
结果:

- 原理 :数据库返回的列名被改成了
createTime和updateTime,MyBatis 在自动映射时就能直接找到createTime和updateTime属性。 - 优点:简单直观,哪里不匹配改哪里。
- 缺点:每个查询都要手动写别名,如果查询很多或字段很多,SQL 会变得冗长且难以维护。
5.4 解决方法二:使用 @Results 手动映射
MyBatis 提供了 @Results 和 @Result 注解,允许我们显式指定列名与属性名的对应关系。这相当于给 MyBatis 一张"对照表",明确告诉它哪一列对应哪个属性。
java
@Results(id = "studentMap", value = {
@Result(column = "create_time", property = "createTime"),
@Result(column = "update_time", property = "updateTime")
})
@Select("SELECT id, name, age, gender, create_time, update_time FROM student")
List<Student> findAll();
@Results:定义一组映射规则,可以给一个id方便复用。@Result:指定单个映射,column是数据库列名,property是 Java 属性名。- 对于没有在
@Results中指定的列(如id、name等),MyBatis 仍然会尝试自动匹配(前提是列名和属性名一致)。
如果其他查询也想复用这组映射,可以使用 @ResultMap 注解引用:
java
@ResultMap("studentMap")
@Select("SELECT * FROM student WHERE id = #{id}")
Student findById(Integer id);
- 原理 :MyBatis 内部维护了一个映射注册表,当遇到
@ResultMap时,它会根据id找到预先定义的映射规则,然后按照规则将列值赋给对应属性。 - 优点:映射规则可以复用,适合复杂映射或跨多个方法。
- 缺点 :每个需要映射的字段都要写
@Result,如果字段很多,代码依然比较繁琐。
5.5 解决方法三:开启驼峰命名自动转换(推荐使用)
MyBatis 提供了一个全局配置开关:mapUnderscoreToCamelCase 。开启后,MyBatis 会在自动映射时自动将数据库列名的下划线命名转换为驼峰命名,然后再去匹配 Java 属性。
在 application.yml 中配置:
yaml
mybatis:
configuration:
map-underscore-to-camel-case: true
配置后,代码无需任何修改:
java
@Select("SELECT id, name, age, gender, create_time, update_time FROM student")
List<Student> findAll(); // 现在 createTime 和 updateTime 都能正确赋值了
- 原理 :开启该配置后,MyBatis 在创建自动映射时,会对列名进行转换:将下划线后的字母大写,并去掉下划线。例如
create_time→createTime,update_time→updateTime,然后拿着转换后的名字去查找 Java 对象的属性。这样即使列名和属性名风格不同,也能成功匹配。 - 优点:全局生效,一劳永逸,代码最简洁。
- 缺点 :如果某些字段命名不符合驼峰规则(例如列名就是
createtime全小写),转换后可能还是对不上,但这种情况极少见。绝大多数场景下这是最佳实践。
5.6 三种方法对比
| 方法 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| SQL 别名 | 改变列名,直接匹配 | 简单直接 | 每个查询都要写,冗余 | 临时解决,少量字段 |
| @Results | 手动建立映射表 | 规则可复用,精确控制 | 每个字段都要写,仍显繁琐 | 映射规则复杂或需复用 |
| 驼峰配置 | 自动转换列名 | 全局生效,代码最简 | 需配置,少数特殊命名无效 | 推荐,绝大多数项目 |
在实际开发中,推荐优先使用驼峰命名自动转换 ,因为它只需要一次配置,就能解决所有字段名不一致的问题,让代码更简洁、更易维护。只有在某些特殊场景下(例如需要处理复杂的嵌套映射),再考虑使用 @Results 或别名。
现在我们不仅知道如何解决字段名不一致的问题,还明白了 MyBatis 是如何把数据库的行变成 Java 对象的------这一切都离不开 ResultSet 的遍历、列名匹配、反射调用 setter 方法 这一系列幕后工作。理解了这一点,以后遇到映射问题,我们就能快速定位是哪个环节出了偏差。
6. 深入理解 @Param 注解
@Param 的作用我们已经知道:给方法参数起名字,让 MyBatis 在封装 Map 时使用这个名字作为 key。
但你可能好奇:它是对形参命名,还是对底层参数命名?
答案是:@Param 是对底层参数命名 。它在编译阶段会被保留,并通过反射在运行时获取。MyBatis 在处理参数时,会检查方法参数上是否有 @Param 注解,如果有,就用注解的 value 作为 Map 的 key;如果没有,则使用默认的 arg0/arg1 或 param1/param2。
举个例子:
java
Student findByNameAndAge(@Param("name") String name, @Param("age") Integer age);
- MyBatis 构建一个 Map,放入两个键值对:
{"name": name的值, "age": age的值}。 - 当解析 SQL 中的
#{name}时,从 Map 中取 key 为 "name" 的值。
如果没有 @Param:
java
Student findByNameAndAge(String name, Integer age);
- MyBatis 构建的 Map 中会有:
{"arg0": name的值, "arg1": age的值, "param1": name的值, "param2": age的值}。 - 此时 SQL 中可以用
#{arg0}或#{param1},但不能用#{name}【不过这个取决于编译时是否保留了方法参数的真实名称,我们前面提到过】,因为 Map 中没有这个 key。
所以,@Param 的本质是为参数在 Map 中指定一个易于识别的 key,避免使用默认的索引名。
7.补充 【OGNL 是什么?为什么在 MyBatis 里会用到它?】
声明:以下内容查阅资料所得
OGNL 的全称是 Object-Graph Navigation Language ,翻译过来叫"对象图导航语言"。名字听起来很唬人,但它的作用其实很简单:帮我们从 Java 对象里取出属性值。
你可以把 OGNL 想象成一个智能的"取货员":
- 你给他一张纸条,上面写着要取什么货(比如
name)。 - 他知道去哪里找(当前的对象是谁)。
- 他知道怎么取(通过调用 getter 方法)。
- 最后把取到的货(属性值)交给你。
1. 为什么需要 OGNL?
在 MyBatis 里,我们经常这样写 SQL:
java
@Insert("INSERT INTO student(name, age) VALUES(#{name}, #{age})")
int insert(Student student);
这里 #{name} 和 #{age} 看起来只是简单的占位符,但 MyBatis 怎么知道要从 student 对象里取 name 和 age 的值呢?
答案就是 OGNL。当 MyBatis 解析到 #{name} 时,它会用 OGNL 表达式去计算:"从当前参数对象中取出名为 name 的属性值"。
2. OGNL 怎么工作?
假设我们有一个 Student 类:
java
public class Student {
private String name;
private int age;
// getter 和 setter 省略
}
当我们执行 insert(student) 时,MyBatis 内部大致做了这样几步:
- 确定参数源 :因为只有一个参数且是对象,所以参数源就是
student对象本身。 - 解析
#{name}:OGNL 拿到表达式"name",然后在student对象上执行"获取 name 属性"的操作。这背后其实是通过反射调用student.getName()方法。 - 拿到值 :OGNL 把得到的值(比如
"张三")返回给 MyBatis。 - 设置到 SQL 占位符 :MyBatis 用这个值去设置
PreparedStatement的参数。
3. OGNL 表达式的威力
OGNL 不只是取一层属性,它还能"导航"到更深的对象。比如如果 Student 里有一个 Address 对象:
java
public class Student {
private String name;
private Address address;
// ...
}
public class Address {
private String city;
// ...
}
那么在 SQL 里可以写 #{address.city},OGNL 就会自动先取 address 对象,再从这个对象里取 city 属性,相当于执行了 student.getAddress().getCity()。
4. OGNL 和 Map 的关系
前面我们讲过,当参数是多个时,MyBatis 会把它们封装成一个 Map。这时候 OGNL 还会发挥作用吗?
答案是:如果参数是 Map,OGNL 就会直接从这个 Map 里根据 key 取值。比如:
java
@Select("SELECT * FROM student WHERE name = #{name} AND age = #{age}")
Student findByNameAndAge(@Param("name") String name, @Param("age") Integer age);
MyBatis 内部构建了一个 Map:
{
"name" : "张三",
"age" : 18
}
当解析 #{name} 时,OGNL 会把这个 Map 当作对象,然后执行 map.get("name") 来取值。这和使用对象时的原理是一样的------都是通过 OGNL 表达式从参数源中取值,只是参数源的类型不同(一个是普通 Java 对象,一个是 Map 对象)。
5. 用生活中的例子理解 OGNL
想象你去一个巨大的仓库(Java 对象)取货:
- 仓库里有很多货架(属性),每个货架上贴着标签(属性名)。
- 你拿着一张提货单(SQL 中的
#{}),上面写着要取的货名(比如name)。 - 仓库管理员(OGNL)接过提货单,走到仓库里,根据标签找到对应的货架,把货取出来交给你。
如果货架上放的又是一个小仓库(嵌套对象),比如 address 货架上放的是另一个仓库,那么提货单上就可以写 address.city,管理员会先打开 address 仓库,再在里面找 city 货架。
6. 总结
- OGNL 就是 MyBatis 用来从参数中取值的工具。
- 当参数是对象时,OGNL 通过反射调用 getter 方法取值。
- 当参数是 Map 时,OGNL 直接根据 key 从 Map 中取值。
- 我们写
#{属性名}就是在写一个 OGNL 表达式,告诉 MyBatis 要从参数里取什么。
老铁们,到这里我们的注解实现增删改查就告一段落了。相信通过这篇文章,你对 MyBatis 的参数传递、结果映射以及背后的原理有了更深刻的理解。
下一期,我们将继续深入,学习如何使用 XML 文件来完成增删改查,看看 XML 方式和注解方式有哪些异同,以及如何应对更复杂的 SQL 场景。
如果觉得这篇文章对你有帮助,别忘了
👍 点赞
⭐ 收藏
👀 关注
你的支持是我持续输出的最大动力!我们下期见!