JavaEE 进阶第十七期:MyBatis,查询请求的生命周期全景图(二)

专栏:JavaEE 进阶跃迁营

个人主页:手握风云

目录

[一、MyBatis 的基础操作](#一、MyBatis 的基础操作)

[1.1. 增(insert)](#1.1. 增(insert))

[1.2. 删(delete)](#1.2. 删(delete))

[1.3. 改(update)](#1.3. 改(update))

[二、MyBatis XML 配置文件](#二、MyBatis XML 配置文件)

[2.1. 添加 mapper 接口](#2.1. 添加 mapper 接口)

[2.2. 添加对应的 XXX.xml 映射文件](#2.2. 添加对应的 XXX.xml 映射文件)

[2.3. 增删查改操作](#2.3. 增删查改操作)

[1. 查(Select)](#1. 查(Select))

[2. 增(Insert)](#2. 增(Insert))

[3. 删(Delete)](#3. 删(Delete))

[4. 改(Update)](#4. 改(Update))

三、其他查询操作

[3.1. 多表查询](#3.1. 多表查询)

[3.2. #{} 和 {}](#{} 和 {})

[1. 使用方式与底层实现差异](#1. 使用方式与底层实现差异)

[2. 核心区别](#2. 核心区别)

[3. {} 的不可替代使用场景](#3. {} 的不可替代使用场景)

[4. 使用原则](#4. 使用原则)


一、MyBatis 的基础操作

1.1. 增(insert)

Mapper 接口通过 @Insert 注解编写插入 SQL,使用#{实体类属性名}绑定动态参数,方法接收对应实体类对象,返回值为 Integer 类型,代表受影响的行数。

java 复制代码
@Insert("INSERT INTO user_info (username, `password`, age) VALUE (#{username}, #{password}, #{age})")
Integer insertUser(UserInfo userInfo);
java 复制代码
@Test
void insertUser() {
    UserInfo userInfo = new UserInfo("James", "java", 24);
    Integer rows = userInfoMapper2.insertUser(userInfo);
    System.out.println("影响的行数:" + rows);
}

若方法参数通过 @Param 设置别名,SQL 中需通过#{别名.实体类属性名}获取参数。

java 复制代码
@Insert("insert into user_info (username, `password`, age) VALUE(#{userInfo.username}, #{userInfo.password}, #{userInfo.age} )")
Integer insertUser2(@Param("userInfo") UserInfo userInfo);
java 复制代码
@Test
void insertUser2() {
    UserInfo userInfo = new UserInfo("Linux", "C", 25);
    Integer rows = userInfoMapper2.insertUser2(userInfo);
    System.out.println("影响的行数:" + rows);
}

在 @Insert 注解上添加@Options(useGeneratedKeys = true, keyProperty = "id"),开启自增主键获取功能;useGeneratedKeys 表示启用 JDBC 的自增主键获取,keyProperty指定将自增主键赋值到实体类的 id 属性;方法返回值仍为受影响行数,自增的主键 ID 会自动赋值到实体类对象的对应属性中。

java 复制代码
@Options(useGeneratedKeys = true, keyProperty = "id")
@Insert("insert into user_info (username, `password`, age) VALUE(#{username}, #{password}, #{age} )")
Integer insertUser3(UserInfo userInfo);
java 复制代码
@Test
void insertUser3() {
    UserInfo userInfo = new UserInfo("Tom", "C++", 26);
    Integer rows = userInfoMapper2.insertUser3(userInfo);
    System.out.println("影响行数:"+ rows+", 自增id:" + userInfo.getId());
}

1.2. 删(delete)

通过@Delete注解编写删除 SQL,使用#{参数名}绑定动态的 id 参数,Mapper 接口方法接收Integer类型的 id 参数,实现根据主键 id 删除数据库中指定数据。

java 复制代码
@Delete("delete from user_info where id = #{id}")
Integer deleteUser(Integer id);
java 复制代码
@Test
void deleteUser() {
    Integer rows = userInfoMapper2.deleteUser(7);
    System.out.println("影响行数:"+ rows);
}

1.3. 改(update)

通过@Update注解编写更新 SQL,使用#{实体类属性名}绑定动态参数(如修改的字段值、主键 id),Mapper 接口方法接收对应实体类对象,实现根据主键 id 修改数据库中指定字段的值。

java 复制代码
@Update("update user_info set gender = #{gender}, delete_flag = #{deleteFlag}  where id = #{id}")
Integer updateUser(UserInfo userInfo);
java 复制代码
@Test
void updateUser() {
    UserInfo userInfo = new UserInfo();
    userInfo.setId(6);
    userInfo.setGender(1);
    userInfo.setDeleteFlag(1);
    Integer rows = userInfoMapper2.updateUser(userInfo);
    System.out.println("影响行数:"+ rows);
}

二、MyBatis XML 配置文件

2.1. 添加 mapper 接口

创建持久层接口,添加 @Mapper 注解,让 MyBatis 识别该接口并交由 Spring IOC 容器管理,与注解方式的接口注解使用一致;接口中定义需要的数据库操作抽象方法(如查询所有用户的queryAllUser()),方法的返回值、参数与业务需求匹配即可。

2.2. 添加对应的 XXX.xml 映射文件

我们需要先在配置文件 application.yml 中,mybatis.mapper-locations 指定的路径下,与配置匹配。

XML 复制代码
mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true  #自动驼峰转换
  mapper-locations: classpath:mapper/*Mapper.xml

接着我们需要固定 XML 格式,里面必须包含 XML 声明、MyBatis 的 DOCTYPE 约束,以及根标签 <mapper>,是 MyBatis XML 的基础规范。

java 复制代码
package com.yang.test2_9_1.mapper;

import com.yang.test2_9_1.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface UserInfoMapperXmlMapper {
    List<UserInfo> selectList();
}
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.yang.test2_3_1.mapper.UserInfoMapperXML">
	<select id="selectList" resultType="com.yang.test2_3_1.model.UserInfo">
		SELECT * FROM `user_info`
	</select>
</mapper>

<mapper> 标签:通过 namespace 属性指定对应 mapper 接口的全限定名,建立 XML 与接口的唯一关联;SQL 实现标签(如 <select> ):标签的 id 属性必须与接口中的方法名完全一致,resultType 属性指定方法返回数据对应的实体类全限定名,标签内部编写具体的 SQL 语句;

2.3. 增删查改操作

1. 查(Select)

使用 <select> 标签,id 属性与接口方法名一致,resultType 指定查询结果的封装实体类,基础查询 XML 代码:

java 复制代码
@Test
void selectList() {
    System.out.println(userInfoMapperXml.selectList());
}

@Test
void selectList2() {
    System.out.println(userInfoMapperXml.selectList2());
}

@Test
void selectList3() {
    System.out.println(userInfoMapperXml.selectList3());
}
XML 复制代码
<select id="selectList" resultType="com.yang.test2_9_1.model.UserInfo">
	select * from user_info
</select>
<select id="selectList2" resultType="com.yang.test2_9_1.model.UserInfo">
	SELECT id,  username,  `password`,  age,  gender,  phone,  delete_flag AS deleteFlag,
	       create_time AS createTime, update_time AS updateTime FROM `user_info`
</select>
<select id="selectList3" resultMap="BaseMap1">
	select * from user_info
</select>

<resultMap id="BaseMap1" type="com.yang.test2_9_1.model.UserInfo">
	<id column="id" property="id"></id>
	<result column="delete_flag" property="deleteFlag"></result>
	<result column="create_time" property="createTime"></result>
	<result column="update_time" property="updateTime"></result>
</resultMap>

对于上面的 3 个查询我们都使用到了同一条 SQL 语句,我们可以借助下面的语法来实现复用

SQL 片段。

java 复制代码
<?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.yang.test2_9_1.mapper.UserInfoMapperXml">
	<sql id="sql">
		select * from user_info
	</sql>

	<select id="selectList" resultType="com.yang.test2_9_1.model.UserInfo">
-- 		select * from user_info
		<include refid="sql"></include>
	</select>

	<select id="selectList2" resultType="com.yang.test2_9_1.model.UserInfo">
		/*SELECT id,  username,  `password`,  age,  gender,  phone,  delete_flag AS deleteFlag,
		       create_time AS createTime, update_time AS updateTime FROM `user_info`*/
		<include refid="sql"></include>
	</select>

	<select id="selectList3" resultMap="BaseMap1">
-- 		select * from user_info
		<include refid="sql"></include>
	</select>

	<resultMap id="BaseMap1" type="com.yang.test2_9_1.model.UserInfo">
		<id column="id" property="id"></id>
		<result column="delete_flag" property="deleteFlag"></result>
		<result column="create_time" property="createTime"></result>
		<result column="update_time" property="updateTime"></result>
	</resultMap>
	
</mapper>

2. 增(Insert)

在 UserInfoMapperXml 接口中定义新增方法,接收用户实体类对象作为参数,返回受影响行数。

java 复制代码
Integer insertUser1(UserInfo userInfo);
XML 复制代码
<insert id="insertUser1">
		insert into user_info (username, `password`, age) VALUE(#{username}, #{password}, #{age} )
</insert>

若需给参数设置别名,接口定义为:

java 复制代码
Integer insertUser(@Param("userInfo") UserInfo userInfo);

若需要返回新插入数据的自增 id,无需修改接口,仅在 <insert> 标签中添加两个属性即可,自增 id 会自动赋值到实体类的对应属性中:

XML 复制代码
<insert id="insertUser2" useGeneratedKeys="true" keyProperty="id">
	insert into user_info (username, `password`, age)
		VALUE(#{userInfo.username}, #{userInfo.password}, #{userInfo.age} )
</insert>

useGeneratedKeys=true:启用 MyBatis 获取数据库自动生成的主键功能;keyProperty="id":指定将自增主键赋值到实体类的 id 属性。

3. 删(Delete)

定义删除方法,接收 id 作为参数,返回受影响行数:

java 复制代码
Integer deleteUserById(Integer id);

使用 <delete> 标签,id 属性与接口方法名一致,直接通过 #{id} 获取传入的主键参数:

XML 复制代码
<delete id="deleteUserBuId">
	delete from user_info where id = #{id}
</delete>

4. 改(Update)

定义修改方法,接收用户实体类对象作为参数,返回受影响行数:

java 复制代码
Integer updateUser(UserInfo userInfo);

使用 <update> 标签,id 属性与接口方法名一致,通过 #{属性名} 获取实体类中的更新字段和条件字段:

XML 复制代码
<update id="updateUser">
	update user_info set gender = #{gender}, delete_flag = #{deleteFlag}  where id = #{id}
</update>

三、其他查询操作

3.1. 多表查询

  • 数据准备
sql 复制代码
-- 创建文章表
DROP TABLE IF EXISTS articleinfo;
CREATE TABLE articleinfo ( 
    id INT PRIMARY KEY auto_increment,
    title VARCHAR ( 100 ) NOT NULL,
    content TEXT NOT NULL,
    uid INT NOT NULL,
    delete_flag TINYINT ( 4 ) DEFAULT 0 COMMENT '0-正常, 1-删除',
    create_time DATETIME DEFAULT now(),
    update_time DATETIME DEFAULT now() 
) DEFAULT charset 'utf8mb4';

-- 插入测试数据
INSERT INTO articleinfo ( title, content, uid ) VALUES ( 'Java', 'Java正文', 1 );
java 复制代码
package com.yang.test2_10_1.model;

import lombok.Data;

import java.util.Date;

@Data
public class ArticleInfo {
    private Integer id;
    private String title;
    private String content;
    private Integer uid;
    private Integer deleteFlag;
    private Date createTime;
    private Date updateTime;
    private String username;
    private Integer age;
}
  • 查询工作
sql 复制代码
SELECT ta.*, tb.username, tb.age 
FROM articleinfo ta 
LEFT JOIN user_info tb 
ON ta.uid = tb.id WHERE ta.id;
java 复制代码
package com.yang.test2_10_1.mapper;

import com.yang.test2_10_1.model.ArticleInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface ArticleInfoMapper {
    @Select("select ta.*, tb.username, tb.age from articleinfo ta " +
            "left join user_info tb on ta.uid = tb.id " +
            "where ta.id= #{id}")
    ArticleInfo queryArticleInfo(Integer id);
}
java 复制代码
package com.yang.test2_10_1.mapper;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class ArticleInfoMapperTest {
    @Autowired
    private ArticleInfoMapper articleInfoMapper;

    @Test
    void queryArticleInfo() {
        System.out.println(articleInfoMapper.queryArticleInfo(1));
    }
}

在实际工作中,多表查询(尤其是多表关联查询)的使用场景被严格限制。首先,多表查询会带来显著的性能与资源开销。复杂的多表关联(如 JOIN 操作)会大幅提升数据库的 CPU、内存和磁盘 IO 负载,容易生成执行效率低下的慢 SQL,不仅拖慢单条查询的响应时间,还会挤占数据库集群的整体资源,影响其他业务的正常运行。同时,多表查询的执行时间通常更长,会持续占用数据库连接资源,像图中 MySQL 集群提供的 1000 个链接是多个项目共享的,若大量多表查询长时间占用连接,会导致连接池耗尽,新的业务请求无法及时处理,严重降低系统的并发能力。

其次,多表查询不利于系统的可维护性与扩展性。多表关联的 SQL 语句往往逻辑复杂、可读性差,当业务迭代需要调整表结构或关联逻辑时,修改成本高且容易引入 bug;在分库分表等架构演进场景中,跨库、跨表的多表查询兼容性问题会更加突出,限制了系统的扩展能力。此外,数据库字段的修改代价极大,DBA 在慢 SQL 治理中会严格约束多表查询的使用,避免因复杂查询导致数据库性能瓶颈,保障核心业务的稳定性。

最后,多表查询的适用场景本身就很有限。它更适合离线大数据分析、对性能要求不高的 B 端场景,而在线业务(尤其是 C 端)对响应时间和并发能力要求极高,因此开发中通常会优先采用 "单表查询 + 应用层关联" 的方式,将计算压力转移到应用服务,既提升了查询性能,又降低了数据库的负载,保障了系统的高效稳定运行。

3.2. #{} 和 ${}

1. 使用方式与底层实现差异

  • #{} :采用预编译 SQL 实现,会将 SQL 中的 #{} 替换为?占位符,先对 SQL 进行编译并缓存,后续仅将参数填充到占位符中;会根据参数类型自动拼接引号(字符串类型加单引号,数值类型不加),无需手动处理。
  • ${} :采用即时 SQL 实现,会直接将参数进行字符替换 ,与 SQL 语句拼接后一起编译执行;不会自动添加引号,字符串类型参数需要手动在 SQL 中加单引号,数值类型加引号可能导致数据库索引失效、性能下降。
java 复制代码
@Select("SELECT * FROM `user_info` where id = #{id}")
UserInfo selectById(Integer id);

@Select("SELECT * FROM `user_info` where id = ${id}")
UserInfo selectById2(Integer id);

2. 核心区别

二者的本质区别是预编译 SQL 和即时 SQL 的区别,直接导致性能和安全性上的显著差异:

  1. 性能层面:#{} 预编译后会缓存编译结果,相同 SQL 多次执行时无需重复解析、优化、编译,效率更高;${} 每次执行都会重新编译拼接后的 SQL,性能较低。
  2. 安全层面 :#{} 能有效防止 SQL 注入,是推荐的使用方式;${} 存在严重的 SQL 注入风险,恶意参数会被直接拼接到 SQL 中改变执行逻辑。

3. ${} 的不可替代使用场景

#{} 虽安全高效,但因会自动加引号,在部分 SQL 语法中会导致语法错误,此时需使用 ${},核心场景包括:

  1. 动态排序 :排序规则(asc/desc)动态传入时,SQL 中order by 字段 ${sort},若用 #{} 会拼接为order by 字段 'asc',加引号后违反 SQL 语法,导致执行报错。
  2. 动态表名 / 字段名 :当表名、查询字段名需要动态传入时,只能使用{},如`select {column} from ${tableName}`,#{} 的自动加引号会导致表名 / 字段名被识别为字符串,触发语法错误。

4. 使用原则

  • 优先使用 #{}:绝大多数业务场景(如增删改查的普通参数传递)均使用 #{},兼顾性能与安全。
  • 谨慎使用 ${} :仅在动态排序、动态表名 / 字段名 等 #{} 无法满足的场景使用,且使用时必须对传入的参数做严格的校验(如限制排序参数仅能为asc/desc,表名仅能为指定白名单),避免 SQL 注入。
相关推荐
寻寻觅觅☆8 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
l1t8 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
青云计划8 小时前
知光项目知文发布模块
java·后端·spring·mybatis
赶路人儿9 小时前
Jsoniter(java版本)使用介绍
java·开发语言
ceclar1239 小时前
C++使用format
开发语言·c++·算法
探路者继续奋斗9 小时前
IDD意图驱动开发之意图规格说明书
java·规格说明书·开发规范·意图驱动开发·idd
码说AI9 小时前
python快速绘制走势图对比曲线
开发语言·python
Gofarlic_OMS10 小时前
科学计算领域MATLAB许可证管理工具对比推荐
运维·开发语言·算法·matlab·自动化
星空下的月光影子10 小时前
易语言开发从入门到精通:补充篇·网络爬虫与自动化采集分析系统深度实战·HTTP/HTTPS请求·HTML/JSON解析·反爬策略·电商价格监控·新闻资讯采集
开发语言
老约家的可汗10 小时前
初识C++
开发语言·c++