【JavaEE19-后端部分】 MyBatis 入门第三篇:使用XML完成增删改查

老铁们,欢迎来到MyBatis的XML世界!不管你有没有了解过注解方式,都没关系,因为XML方式是MyBatis最原始、最强大的形式,也是我个人最喜欢的方式。为什么喜欢?因为XML把SQL语句独立出来,清晰、灵活,尤其是面对复杂查询时,那种掌控感简直不要太爽。今天我们就从零开始,彻底掌握XML方式,并且会给大家把每一个细节、每一个为什么都掰开揉碎了讲清楚。


1. 使用XML的准备条件:让MyBatis知道去哪找XML

1.1 数据库连接配置

首先,我们需要告诉MyBatis如何连接到数据库。在 application.yml 中配置:

yaml 复制代码
spring
  # 配置数据源
  datasource:
    url: jdbc:mysql://localhost:3306/javaee_test?useSSL=false&characterEncoding=utf8
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver #这个在新版本下可以不用设置

# 配置mybatis的日志(可选)
mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

这几行配置的作用是:

  • url :告诉MyBatis要连接哪个数据库(mybatis_demo),以及一些连接参数(如编码、是否使用SSL)。格式是固定的:jdbc:mysql://主机:端口/数据库名?参数
  • username/password:验证你的身份,只有提供正确的用户名密码才能登录数据库。
  • driver-class-name :指定使用哪个JDBC驱动。MySQL 8.x使用 com.mysql.cj.jdbc.Driver,5.x使用 com.mysql.jdbc.Driver

1.2 配置MyBatis的XML文件路径(关键)

这是XML方式最关键的一步 :你必须告诉MyBatis,你的XML映射文件放在哪里。在 application.yml 中添加:

yaml 复制代码
mybatis:
  mapper-locations: classpath:mapper/**Mapper.xml

这个配置是什么意思呢?

  • classpath: 表示从编译后的classes目录下找(也就是 resources 目录下的文件编译后会放到classes里)。你可以把它理解为"项目的根目录"。
  • mapper/**Mapper.xml 是一个路径通配符:
    • ** 表示任意多级目录(包括子文件夹)这里面使用一个* 或者两个 *都是可以的。
    • *Mapper.xml 表示文件名以 Mapper.xml 结尾。

举个例子:如果你的XML文件放在 resources/mapper/student/StudentMapper.xml,它也能被扫描到。如果放在 resources/mybatis/xml/StudentMapper.xml,那就要把配置改成 classpath:mybatis/xml/*.xml

为什么需要这个配置?

因为MyBatis启动时,需要去加载所有XML文件,解析里面的SQL语句。如果不告诉它路径,它就会像无头苍蝇一样找不到文件,你调用Mapper接口时就会报错(说找不到对应的SQL)。所以这一步是必须的!

疑问:如果我不配置这个会怎样?

那你就会得到一个经典的错误:Invalid bound statement (not found)。意思是MyBatis找到了你的Mapper接口,但是找不到对应的SQL语句(因为没加载XML)。


2. 写持久层代码:接口和XML文件

MyBatis的XML方式需要两部分:

  1. Mapper接口:定义方法(告诉MyBatis你要做什么操作)。
  2. XML映射文件:写具体的SQL语句(告诉MyBatis怎么操作)。

2.1 创建Mapper接口(加@Mapper注解)

我们创建一个 StudentMapper 接口:

java 复制代码
package com.example.mapper;

import com.example.model.Student;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;

@Mapper  // 这个注解非常重要!
public interface StudentMapper {
    List<Student> findAll();  // 查询所有学生
}

为什么需要加 @Mapper 注解?

这个注解是告诉Spring:这是一个MyBatis的Mapper接口,需要在容器中为它创建代理对象 。如果不加这个注解,Spring就不会扫描到这个接口,也就不会生成代理对象,你后面就无法通过 @Autowired 注入它。

加了 @Mapper 后,MyBatis帮我们做了什么?

  • 在Spring启动时,MyBatis会扫描所有带 @Mapper 的接口。
  • 为每个接口创建一个动态代理对象(实现了该接口的类)。
  • 将这个代理对象注册到Spring容器中,成为一个Bean。
  • 这样,你才能在Service层通过 @Autowired 注入这个接口。

2.2 创建XML映射文件

resources/mapper 目录下创建一个XML文件,命名为 StudentMapper.xml(文件名可以任意,但建议和接口名一致)。文件内容如下:

xml 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zhongge.mapper.StudentMapper">

    <select id="findAll" resultType="com.zhongge.entity.Student">
        select id, name, age, gender, create_time, update_time
        from student
    </select>

</mapper>

逐行解释:

  • 第一行:XML声明,指定版本和编码,没什么特别的。
  • 第二行<!DOCTYPE ...> 是文档类型定义,告诉XML解析器这个文件遵循MyBatis的DTD规范。你可以把它理解为"这个XML的语法规则说明书"。没有它,一些标签可能无法被正确识别。
  • <mapper> 标签 :根标签,有两个重要属性:
    • namespace:命名空间。它的值必须是接口的全限定名 (包名+接口名),比如这里的 com.zhongge.mapper.StudentMapper。这个属性是用来绑定接口的,告诉MyBatis这个XML文件是为哪个接口服务的。
  • <select> 标签 :代表一个查询操作。
    • id:这条SQL语句的唯一标识。它的值必须等于接口中的方法名 ,比如这里的 findAll
    • resultType:返回结果的类型。这里写的是实体类的全限定名 com.zhongge.entity.Student。MyBatis会自动把查询结果的每一行映射成 Student 对象。
    • 标签中间就是SQL语句。

2.3 单元测试验证

写一个测试类来验证是否成功:

java 复制代码
@SpringBootTest
class StudentMapperTest {
    @Autowired
    private StudentMapper studentMapper;

    @Test
    void findAll() {
        List<Student> all = studentMapper.findAll();
        System.out.println(all);
    }
}

如果一切正常,你会看到查询出的学生列表。但注意:你会发现 createTimeupdateTime 属性是 null(因为列名和属性名不一致)。这个我们后面会解决。


3. 四大约定:为什么MyBatis能找到对应的SQL?

现在我们来解答你心中的四大疑问。这些问题搞清楚了,你就能彻底理解XML方式的工作原理。

约定一:XML文件必须放在和接口同一个包下吗?

答案:不一定!

我们可以在 mapper-locations 中任意指定路径。比如:

yaml 复制代码
mybatis:
  mapper-locations: classpath:mybatis/xml/*.xml

这样,只要XML文件放在 resources/mybatis/xml 下,不管接口在哪个包,MyBatis都能找到。

但是,很多人习惯把XML和接口放在同一个包下(比如接口在 com.example.mapper,XML放在 resources/com/example/mapper)。这样做的好处是:一眼就能看出哪个XML对应哪个接口,方便维护。不过需要你在 resources 下建立同样的包结构,稍微麻烦一点。

结论 :位置可以灵活配置,但为了方便,建议统一放在一个文件夹下(比如 mapper),然后用配置指定。

约定二:XML文件名必须和接口名一致吗?

答案:不一定!

MyBatis并不会自动根据文件名去匹配接口,它完全依赖 namespace 来定位。所以理论上你可以把XML文件命名为 abc.xml,只要 namespace 写对了,一样能工作。

但是,强烈建议文件名和接口名一致 ,比如 StudentMapper.java 对应 StudentMapper.xml。这样团队协作时,别人一看就知道哪个文件对应哪个接口,省去很多麻烦。

结论:文件名不是必须一致,但为了可维护性,建议一致。

约定三:为什么 namespace 必须等于接口的全限定名?

这是硬性要求 ,必须满足!
namespace 的作用是告诉MyBatis:这个XML文件是为哪个接口服务的。当调用 studentMapper.findAll() 时,MyBatis会做以下事情:

  1. 拿到接口的全限定名:com.zhongge.mapper.StudentMapper
  2. 在内部维护的一个Map中,以这个全限定名为key,找到对应的 namespace 的XML文件。
  3. 然后在那个 namespace 下,根据方法名 findAll 找到对应的SQL标签。

如果 namespace 写错了,MyBatis就找不到这个XML,就会报错。

打个比方

  • namespace 就像公司的部门名称。你要找某个部门的员工(SQL语句),必须先知道部门名称(接口全限定名)。
  • 如果部门名称写错了,你就找不对人。

结论namespace 必须等于接口全限定名,这是MyBatis的硬性规定。

约定四:为什么SQL标签的 id 必须等于接口的方法名?

这也是硬性要求。

在同一个 namespace 下,每个SQL标签的 id 必须与接口中的方法名一致。这样,当调用 studentMapper.findById(1) 时,MyBatis就知道要执行 com.zhongge.mapper.StudentMapperid=findById 的那个SQL。

如果 id 不一致,MyBatis就找不到对应的SQL,报错 Invalid bound statement

打个比方

  • id 就像部门里的工号。你要找一个具体的人(SQL语句),必须知道他的工号(方法名)。
  • 如果工号写错了,你就找不到这个人。

结论id 必须等于方法名,这也是硬性规定。

完成约定后,MyBatis帮我们做了什么?

当所有约定都满足(特别是 namespaceid),MyBatis在启动时会:

  1. 扫描接口 :根据 @Mapper 注解找到所有Mapper接口。
  2. 生成代理对象:为每个接口创建一个动态代理对象(实现了该接口),并放入Spring容器。
  3. 解析XML :根据 mapper-locations 找到所有XML文件,解析出每个 <select><insert> 等标签,创建 MappedStatement 对象(包含了SQL语句、参数映射、返回值类型等信息),并以 namespace.id 为key缓存起来。
  4. 建立关联 :将接口方法和对应的 MappedStatement 关联起来。

当你调用接口方法时:

  1. 代理对象拦截调用。
  2. 根据接口全限定名和方法名,从缓存中找到对应的 MappedStatement
  3. 执行SQL,并处理结果。

4. 参数传递原理

无论你用哪种方式写SQL,MyBatis处理参数的底层机制都是一样的。下面我们通过几种常见情况,一步步拆解参数传递的完整流程。

4.1 参数传递的核心:参数容器

MyBatis会把方法传入的参数统一管理起来,这个"管理者"我们可以抽象为参数容器/参数源。它可以是:

  • 单个简单值(如 Integer、String)
  • 一个 Java 对象(如 Student)
  • 一个 Map(称为 ParamMap

整个流程如下:

下面我们结合具体案例,画出每一步的细节。

4.2 情况一:单个简单参数

场景:根据ID查询学生。

java 复制代码
Student findById(Integer id);

XML

xml 复制代码
    <select id="findById" resultType="com.zhongge.entity.Student">
        select * from student where id = #{id}
    </select>

调用:findById(1)

结果:

流程图

关键点 :单个简单参数时,无论 #{} 里写什么名字,都取这个唯一的参数值。

4.3 情况二:多个参数(无@Param)

场景:根据姓名和年龄查询学生。

java 复制代码
Student findByNameAndAge(String name, Integer age);

XML

xml 复制代码
<select id="findByNameAndAge" resultType="com.zhongge.entity.Student">
    select * from student where name = #{name} and age = #{age}
</select>

调用:findByNameAndAge("张三", 18)

这个写法能工作吗?------取决于编译参数

情况 A:编译时保留了参数名(如 Spring Boot 默认)

如果项目编译时开启了 -parameters(Spring Boot 默认开启),那么 MyBatis 能获取到参数的真实名称 nameage。它会构建一个 ParamMap,其中包含 key nameage 以及 param1param2 等通用 key。此时 XML 中写 #{name}#{age} 可以正确取值。

情况 B:编译时未保留参数名

如果编译器没有保留参数名,MyBatis 只能拿到默认的 arg0arg1 等。此时 XML 中写 #{name} 会报错(因为 Map 中没有 name),必须写成 #{arg0}#{arg1}#{param1}#{param2} 才能工作。

实际运行结果

下面是一个错误示例,如果在 Spring Boot 项目中 XML 里写成了 #{arg0}

xml 复制代码
<select id="findByNameAndAge" resultType="com.zhongge.entity.Student">
    select * from student where name = #{arg0} and age = #{arg1}
</select>

运行测试会报错:

复制代码
org.apache.ibatis.binding.BindingException: Parameter 'arg0' not found. 
Available parameters are [name, param1, age, param2]

错误信息明确告诉你:可用的参数名是 nameageparam1param2,没有 arg0。所以正确的写法应该是使用 #{name}#{age}

结论

  • 在 Spring Boot 项目中(默认保留参数名) ,无 @Param 的多参数方法,XML 中应直接使用参数的真实名称(如 #{name}),或使用通用名称 #{param1}#{param2}
  • 如果希望代码在任何编译环境下都能工作强烈建议使用 @Param 注解,这样无论是否保留参数名,都能用你自己定义的名称。

MyBatis 内部处理步骤(以保留参数名为例)

  1. 方法调用 :传入 "张三"18
  2. 参数解析ParamNameResolver 发现有多个参数,且没有 @Param,但因为开启了 -parameters,它能获取到真实参数名 nameage。于是它创建 ParamMap,放入:
    • "name" → "张三"
    • "age" → 18
    • 同时为了兼容性,还会放入 "param1" → "张三""param2" → 18
  3. SQL 解析 :解析 #{name} 时,从 Map 中取 key "name" 得到 "张三"#{age}"age" 得到 18
  4. 设置参数:MyBatis 设置到 PreparedStatement。

图示

关键点

  • @Param 的多参数,是否能用 #{name} 取决于编译时是否保留了参数名。
  • Spring Boot 默认保留了参数名,所以可以直接使用真实参数名。
  • 为了代码的健壮性和可移植性,强烈推荐始终使用 @Param 注解,这样你可以自定义参数名,不再依赖编译选项。

4.4 情况三:多个参数(使用 @Param)

场景 :使用 @Param 明确指定参数名。

java 复制代码
Student findByNameAndAge(@Param("name") String name, @Param("age") Integer age);

XML

xml 复制代码
<select id="findByNameAndAge" resultType="com.example.model.Student">
    select * from student where name = #{name} and age = #{age}
</select>

调用:findByNameAndAge("李四", 20)

实际运行结果

如果在 XML 中误用了 #{arg0}#{arg1},会得到如下错误:

复制代码
org.apache.ibatis.binding.BindingException: Parameter 'arg0' not found. 
Available parameters are [name, param1, age, param2]

错误信息告诉我们:可用的参数名是 nameageparam1param2,并没有 arg0arg1。这说明在使用了 @Param 的情况下,MyBatis 构建的 ParamMap只包含

  • @Param 指定的 key(如 nameage
  • 通用顺序 key:param1param2(始终存在,用于兼容)

为什么没有 arg0arg1

因为当方法参数上使用了 @Param 注解,MyBatis 认为开发者已经明确指定了参数名称,就不再生成基于索引的默认名称(arg0arg1),避免 Map 中 key 过多造成混乱。仅保留 param1param2 作为通用备用名称,方便在动态 SQL 中引用。

MyBatis 内部处理步骤

  1. 方法调用 :传入 "李四"20
  2. 参数解析ParamNameResolver 发现有多个参数,且都带有 @Param 注解。它创建一个 ParamMap ,放入以下键值对:
    • "name" → "李四"(来自 @Param("name")
    • "age" → 20(来自 @Param("age")
    • "param1" → "李四"(自动添加的通用名)
    • "param2" → 20(自动添加的通用名)
  3. SQL 解析 :解析 #{name} 时,从 Map 中取 key "name" 得到 "李四"#{age}"age" 得到 20
  4. 设置参数:MyBatis 设置到 PreparedStatement。

图示

关键点

  • @Param 明确指定了参数在 Map 中的 key ,所以 SQL 中应该使用这些 key(如 #{name}#{age})。
  • param1param2 始终存在,作为备用名称,但一般不建议使用,因为可读性差。
  • 不会生成 arg0arg1 ,避免 Map 膨胀。如果需要使用索引名称,应使用 param1param2
  • 使用 @Param 是最佳实践,它让你的代码清晰、不依赖编译参数,且在任何环境下都能稳定运行。

4.5 情况四:单个对象参数(无@Param)

场景:插入一个学生对象。

java 复制代码
int insert(Student student);

XML

xml 复制代码
<insert id="insert">
    insert into student(name, age, gender) values(#{name}, #{age}, #{gender})
</insert>

调用:

java 复制代码
Student stu = new Student("王五", 22, 1);
insert(stu);

流程图

关键点 :无@Param时,对象本身作为参数源,#{} 里直接写属性名,MyBatis通过OGNL自动调用对应的getter方法。

4.6 情况五:单个对象参数(有 @Param)

场景:插入一个学生对象,但加了 @Param。

java 复制代码
int insert(@Param("stu") Student student);

XML

xml 复制代码
<insert id="insert">
    insert into student(name, age, gender) values(#{stu.name}, #{stu.age}, #{stu.gender})
</insert>

调用:

java 复制代码
Student stu = new Student("赵六", 25, 2);
insert(stu);

实际运行结果

如果在 XML 中误用了 #{arg0.name} 这样的写法:

xml 复制代码
<insert id="insert">
    insert into student(name, age, gender) values(#{arg0.name}, #{arg0.age}, #{arg0.gender})
</insert>

会得到如下错误:

复制代码
org.apache.ibatis.binding.BindingException: Parameter 'arg0' not found. 
Available parameters are [stu, param1]

错误信息告诉我们:可用的参数名是 stuparam1,并没有 arg0。这说明在使用了 @Param 的情况下,MyBatis 构建的 ParamMap只包含

  • @Param 指定的 key(如 stu
  • 通用顺序 key:param1(始终存在,用于兼容)

为什么没有 arg0

因为当方法参数上使用了 @Param 注解,MyBatis 认为开发者已经明确指定了参数名称,就不再生成基于索引的默认名称(arg0),避免 Map 中 key 过多造成混乱。仅保留 param1 作为通用备用名称,方便在动态 SQL 中引用。

MyBatis 内部处理步骤

  1. 方法调用 :传入一个 Student 对象,里面有 name="赵六", age=25, gender=2
  2. 参数解析ParamNameResolver 发现只有一个参数,但带有 @Param 注解。它创建一个 ParamMap ,放入以下键值对:
    • "stu" → student对象(来自 @Param("stu")
    • "param1" → student对象(自动添加的通用名)
  3. SQL 解析 :解析 #{stu.name} 时,先从 Map 中取 key "stu" 得到 student 对象,然后通过 OGNL 从该对象中取 name 属性(即 student.getName())。类似地,#{stu.age}#{stu.gender} 依次取值。
  4. 设置参数:MyBatis 将取出的值设置到 PreparedStatement。

图示

关键点

  • 加了 @Param 后,对象被放入 Map ,SQL 中必须用 #{key.属性名} 的形式取值。
  • 可用 key 只有 @Param 指定的值(如 stu)和 param1 ,没有 arg0arg1 等基于索引的 key。
  • 如果直接用 #{name}#{arg0.name} 都会报错 ,因为 Map 中没有 namearg0 这些 key。

对比

  • 无 @Param:直接写 #{属性名},对象本身作为参数源。
  • 有 @Param:必须写 #{key.属性名},其中 key@Param 指定的值(或 param1)。

5. 结果映射原理:从数据库行到Java对象

5.1 默认映射过程

当我们执行一条查询SQL后,数据库会返回一个 ResultSet (结果集),可以想象成一个表格,有行有列。MyBatis需要把每一行数据转换成一个Java对象,这个过程叫结果映射

默认的映射规则非常简单:

  1. 遍历结果集的每一列,获取列名(如 idnamecreate_time)。
  2. 在目标Java类中查找同名属性(忽略大小写)。
  3. 如果找到了,就通过反射调用该属性的 setter 方法,将这一列的值赋进去。
  4. 如果没找到,这一列就被忽略,属性保持默认值(如 null)。

举个例子

假设SQL返回的列名是 idnameagecreate_timeupdate_time,而 Student 类有属性 idnameagecreateTimeupdateTime

  • id → 找到 id 属性,调用 setId()
  • name → 找到 name 属性,调用 setName()
  • age → 找到 age 属性,调用 setAge()
  • create_time → 在类中找 create_time 属性,没有;找 createTime 属性,有,但名字不一样(下划线 vs 驼峰),默认匹配失败,所以 createTime 保持 null
  • update_time 同理。

结果:

这就是为什么我们之前看到 createTimeupdateTimenull

流程图(默认映射):

5.2 解决方法一:SQL别名

在SQL中给列起别名,让别名等于Java属性名。

xml 复制代码
<select id="findAll" resultType="com.zhongge.entity.Student">
    select
        id,
        name,
        age,
        gender,
        create_time as createTime,
        update_time as updateTime
    from student
</select>

原理 :数据库返回的列名变成了 createTimeupdateTime,MyBatis就能直接匹配了。

流程图

5.3 解决方法二:使用 <resultMap> 手动映射

xml 复制代码
<resultMap id="BaseResultMap" type="com.zhongge.entity.Student">
    <id column="id" property="id"/>
    <result column="name" property="name"/>
    <result column="age" property="age"/>
    <result column="gender" property="gender"/>
    <result column="create_time" property="createTime"/>
    <result column="update_time" property="updateTime"/>
</resultMap>

<select id="findAll" resultMap="BaseResultMap">
    select id, name, age, gender, create_time, update_time
    from student
</select>

原理 :MyBatis不再自动匹配,而是按照 resultMap 中定义的规则,将指定列的值赋给对应的属性。

流程图

5.4 解决方法三:开启驼峰命名自动转换(推荐)

application.yml 中配置:

yaml 复制代码
mybatis:
  configuration:
    map-underscore-to-camel-case: true

原理 :开启后,MyBatis在自动匹配时,会将列名中的下划线去掉,并将下划线后的字母大写,然后再去匹配属性。例如 create_timecreateTimeupdate_timeupdateTime

流程图

强烈推荐这种方法,一次配置,全局生效,代码最简洁。


6. 增删改查操作

现在我们来完整实现增删改查

6.1 插入数据(Insert)

6.1.1 基本插入

接口

java 复制代码
int insert(Student student);

XML

xml 复制代码
<insert id="insert">
    insert into student(name, age, gender) values(#{name}, #{age}, #{gender})
</insert>

测试

java 复制代码
Student stu = new Student();
stu.setName("赵云");
stu.setAge(22);
stu.setGender(1);
int count = studentMapper.insert(stu);
System.out.println("插入行数:" + count);

结果

6.1.2 获取自增主键(为什么要获取?怎么获取?)

为什么需要获取自增主键?

在很多业务场景中,插入一条数据后,我们需要知道这条数据在数据库中生成的唯一ID,以便进行后续操作。例如:

  • 订单系统:下单后需要拿着订单ID去通知库存、物流、支付等系统。
  • 社交系统:发布文章后需要返回文章ID给前端,用于跳转到详情页。
  • 关联操作:插入主表后,需要在子表中引用新生成的主键(如用户注册后,需要创建关联的账户信息)。

如果插入后拿不到ID,我们就得再用查询语句去查一遍(比如根据唯一业务字段查询),既浪费资源又麻烦,而且还可能因为并发导致查错数据。所以MyBatis提供了直接获取自增主键的功能,让你在插入后能立刻拿到ID。

怎么获取?

使用 useGeneratedKeyskeyProperty 属性:

xml 复制代码
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
    insert into student(name, age, gender) values(#{name}, #{age}, #{gender})
</insert>
  • useGeneratedKeys="true":useGeneratedKeys译为使用自动生成的键值,告诉MyBatis,我们想要数据库自动生成的主键。这个属性的名字翻译过来就是"使用生成的主键"。
  • keyProperty="id" :keyProperty译为关键属性,指定将生成的主键赋值给传入对象的哪个属性(这里是 id 属性)。MyBatis会通过反射调用 setId() 方法把主键值设置进去。

测试代码

java 复制代码
Student stu = new Student();
stu.setName("赵六");
stu.setAge(25);
stu.setGender(2);
int count = studentMapper.insert(stu);
System.out.println("插入行数:" + count);
System.out.println("生成的主键:" + stu.getId());  // 这里就能拿到数据库生成的id了

底层原理(通俗解释)

  1. JDBC层面 :Java的JDBC API中,PreparedStatement 有一个特殊的方法,可以在执行插入时告诉数据库:"我要插入数据,并且插入后请把自动生成的主键返回给我。"这个是通过在创建 PreparedStatement 时指定一个标志 Statement.RETURN_GENERATED_KEYS 实现的。执行插入后,可以通过 getGeneratedKeys() 方法获取一个包含主键的结果集。
  2. MyBatis层面 :当我们在XML中设置了 useGeneratedKeys="true",MyBatis在内部创建 PreparedStatement 时就会自动加上 RETURN_GENERATED_KEYS 标志。插入执行完毕后,MyBatis会立即调用 getGeneratedKeys() 拿到数据库返回的主键值,然后通过反射找到你传入的 student 对象的 setId() 方法,把主键值设置进去。这样,你原来的 stu 对象里的 id 就不再是 null 了。

注意 :即使获取了主键,int insert( Student student);这个方法的返回值依然是 int 类型,代表受影响的行数。主键是通过传入的对象属性带回来的,不是通过返回值。

6.2 删除数据(Delete)

接口

java 复制代码
int deleteById(Integer id);

XML

xml 复制代码
<delete id="deleteById">
    delete from student where id = #{id}
</delete>

测试

java 复制代码
int count = studentMapper.deleteById(5);
System.out.println("删除行数:" + count);

开发中,删除参数一般传递什么?

  • 最常见:根据主键ID删除。因为ID是唯一标识,能精准定位要删除的记录,避免误删。
  • 有时:也会根据其他唯一字段删除,比如根据用户账号删除,或者根据业务ID删除。
  • 极少 :根据组合条件删除(如 where name = ? and age = ?),但这种情况风险较高,可能删除多条数据,通常需要谨慎处理,或配合事务使用。

删除操作的返回值int 类型,代表实际删除的行数。如果返回0,说明没有符合条件的记录;返回1表示删除了一条;返回大于1表示删除了多条(如果是根据非唯一条件删除)。在业务中,常常根据返回值判断删除是否成功。

6.3 更新数据

接口

java 复制代码
int update(Student student);

XML

xml 复制代码
<update id="update">
    update student
    set name = #{name}, age = #{age}
    where id = #{id}
</update>

测试

java 复制代码
Student stu = new Student();
stu.setId(1);
stu.setName("张三丰");
stu.setAge(108);
int count = studentMapper.update(stu);
System.out.println("更新行数:" + count);

开发中,更新参数一般怎么传递?

  • 传递对象:最常用。将需要更新的数据封装成一个对象(通常包含ID和要修改的字段),然后根据对象中的ID去更新其他字段。这样代码简洁,语义清晰。
  • 传递多个参数 :有时也会用 @Param 传递多个参数,例如只更新某个字段,可以写 updateNameById(@Param("id") Integer id, @Param("name") String name)。这种方式适合简单更新,不需要创建完整的对象。
  • 传递Map :动态更新时,可能会把字段名和值放入Map,然后在SQL中用 <foreach> 动态拼接,但这种情况较少见。

更新操作的返回值int 类型,代表实际更新的行数。如果返回0,说明ID不存在或没有数据被更新;返回1表示更新了一条。在业务中,可以根据返回值判断更新是否生效,例如如果更新了0条,可能需要提示用户"数据不存在"或"没有变化"。


7. 注解和XML的对比(简单了解)

虽然老铁们可能只学XML方式,但以后工作中可能会遇到其他项目用注解,简单了解一下两者的区别:

对比项 XML方式 注解方式
SQL位置 写在独立的XML文件中 写在Java接口的方法上
可读性 复杂SQL可以格式化,清晰 复杂SQL挤在一起,难看
动态SQL 支持 <if><foreach> 等,功能强大 需要用 <script> 包裹,体验一般
维护性 所有SQL集中管理,方便DBA审查 SQL分散在各处,不易管理
修改后 修改XML无需重新编译(需热部署) 修改注解需重新编译
学习曲线 需要学习XML语法和标签 简单直观,上手快

我的建议

  • 简单的单表CRUD,用注解就够了。
  • 涉及复杂查询、动态SQL、多表关联,强烈推荐XML方式。
  • 一个项目里可以同时使用两种方式,不冲突。

我个人偏爱XML,因为它把SQL和代码彻底分离,看着舒服,维护起来也方便。而且当你需要复制SQL到数据库客户端测试时,直接从XML里复制出来就能用,不需要去掉注解里的引号,爽得很!


老铁们,如果你觉得这篇文章的整理对你有帮助,别忘了👍点赞、⭐收藏、👀 关注,你的支持是我持续输出的最大动力!我们下期见!

相关推荐
小江的记录本7 小时前
【VO、DTO、Entity】VO、DTO、Entity三大核心数据对象全解析(附核心对比表 + 代码示例)
java·数据库·spring boot·spring·架构·mybatis·数据库架构
计算机学姐7 小时前
基于SpringBoot的流浪动物救助收养系统
vue.js·spring boot·后端·mysql·java-ee·intellij-idea·mybatis
spencer_tseng7 小时前
Tomcat server.xml <Connector> address=“0.0.0.0“
xml·tomcat
计算机学姐9 小时前
基于SpringBoot的蛋糕烘焙销售服务系统
java·spring boot·后端·spring·tomcat·intellij-idea·mybatis
zdl6869 小时前
Mybatis控制台打印SQL执行信息(执行方法、执行SQL、执行时间)
数据库·sql·mybatis
敲代码的嘎仔20 小时前
Java后端面试——SSM框架面试题
java·面试·职场和发展·mybatis·ssm·springboot·八股
ruanyongjing21 小时前
SpringBoot3 整合 Mybatis 完整版
mybatis
小江的记录本1 天前
【MyBatis-Plus】Spring Boot + MyBatis-Plus 进行各种数据库操作(附完整 CRUD 项目代码示例)
java·前端·数据库·spring boot·后端·sql·mybatis
小江的记录本1 天前
【MyBatis-Plus】MyBatis-Plus的核心特性、条件构造器、分页插件、乐观锁插件
java·前端·spring boot·后端·sql·tomcat·mybatis