【JavaEE20-后端部分】 MyBatis 入门第四篇:多表查询、#{}与${}详解、数据库连接池

老铁们,前三篇我们学会了 MyBatis 的基本操作,从注解到 XML,从参数传递到结果映射,可以说已经能独立完成大部分单表 CRUD 了。但真实项目中,数据往往分散在多张表中,比如用户表、文章表、订单表......很多时候我们需要一次性把多张表的数据查出来。

今天我们就来聊聊 多表查询 ,以及 MyBatis 中两个非常重要的符号:#{}${}。另外,我们还会学习数据库连接池的配置,以及一些企业级开发规范。


1. 多表查询:当数据来自多张表

1.1 为什么需要多表查询?

在实际业务中,数据往往不是孤立存在的。比如:

  • 一篇文章属于一个作者,我们想同时查出文章内容和作者信息。
  • 一个订单关联了用户、商品、地址等多张表。

如果分别查询,代码会变得冗长,而且需要多次数据库交互。多表查询可以一次 SQL 把相关数据都取出来,减少数据库访问次数。

1.2 准备工作:创建用户表和文章表的关联关系

sql 复制代码
-- 创建表[用户表] 
drop table if exists user_info;
create table `user_info` (
 id int ( 11 ) not null primary key auto_increment,
 username varchar ( 127 ) not null,
 password varchar ( 127 ) not null,
 age tinyint ( 4 ) not null,
 gender tinyint ( 4 ) default '0' comment '1-男 2-女 0-默认',
 phone varchar ( 15 ) default null,
 delete_flag tinyint ( 4 ) default 0 comment '0-正常, 1-删除',
 create_time datetime default now(),
 update_time datetime default now() on update now()
); 

-- 添加用户信息 
insert into user_info( username, `password`, age, gender, phone )
values ( '管理员', '管理员123', 18, 1, '18612340001' );
insert into user_info( username, `password`, age, gender, phone )
values ( '张三', '张三123', 18, 1, '18612340002' );
insert into user_info( username, `password`, age, gender, phone )
values ( '李四', '李四123', 18, 1, '18612340003' );
insert into user_info( username, `password`, age, gender, phone )
values ( '王五', '王五123', 18, 1, '18612340004' );


-- 创建文章表 
drop table if exists article_info;
create table article_info (
 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() 
);

-- 插入测试数据 
insert into article_info ( title, content, uid ) values ( 'Javaee', 'Javaee知识介绍', 1 );

用户表

文章表

1.3 sql查询结果


SQL 要求:我在查询文章表的时候 需要将用户表中的用户名和年龄给查询出来,此时涉及到我们的多表查询,那么连接条件就是我们的用户id,如图所示:

结果:

1.4 编写多表查询的 实体类

根据我们查询出来的中间表的字段写一个对应的实体类就行了,比如:

java 复制代码
package com.zhongge.entity;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;

import java.time.LocalDate;


/**
 * @ClassName ArticleInfo
 * @Description TODO 文章实体类
 * @Author 笨忠
 * @Date 2026-03-22 10:14
 * @Version 1.0
 */
@Data
public class ArticleInfo {
    private Integer id;//文章id
    private String title;//文章标题
    private String content;//文章内容
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")//日期的格式转换
    private LocalDate createTime;//文章创建的时间
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDate updateTime;//文章修改的时间

    //用户信息
    private String username;//用户姓名
    private Integer age;//用户年龄
}

1.5 编写mapper接口和xml文件

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

import com.zhongge.entity.ArticleInfo;
import org.apache.ibatis.annotations.Mapper;

/**
 * @InterfaceName ArticleInfoMapper
 * @Description TODO 文章操作接口
 * @Author 笨忠
 * @Date 2026-03-22 10:20
 * @Version 1.0
 */
@Mapper
public interface ArticleInfoMapper {
    //查询文章
    ArticleInfo getArticleInfoByUserId(Integer id);
}
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.ArticleInfoMapper">

    <select id="getArticleInfoByUserId" resultType="com.zhongge.entity.ArticleInfo">
        select u.username, u.age, a.id, a.title, a.content, a.create_time, a.update_time
        from user_info as u
        left join article_info as a
        on u.id = a.uid
        where u.id = #{id}
    </select>
</mapper>

测试:

结果:

1.6 小结:多表查询的本质

MyBatis 处理多表查询和单表查询在底层没有任何区别------它只关心 SQL 执行后返回的列名,然后根据列名去映射 Java 对象的属性。因此,我们只需要把查询结果需要的列都列出来,然后在实体类中定义对应的属性,就能轻松接收多表数据


2. #{} 和 ${} 的区别:安全与性能

我们一直在用 #{} 传参,比如 #{name}#{id}。其实 MyBatis 还有一种传参方式:${}。这两者到底有什么区别?什么时候该用哪个?

2.1 先看现象

2.1.1 数字类型参数
java 复制代码
@Select("select * from student where id = #{id}")
Student findById(Integer id);

执行时,MyBatis 会输出:

复制代码
==> Preparing: SELECT * FROM student WHERE id = ?
==> Parameters: 1(Integer)

SQL 中是 ? 占位符,参数单独传入,这叫 预编译 。如果换成 ${}

java 复制代码
@Select("select * from student where id = ${id}")
Student findById(Integer id);

输出:

复制代码
==> Preparing: SELECT * FROM student WHERE id = 1
==> Parameters: 

参数直接拼接到 SQL 中,没有占位符,这叫 字符串替换

2.1.2 字符串类型参数
java 复制代码
@Select("select * from student where name = #{name}")
Student findByName(String name);

正常输出,参数被安全设置。

如果换成 ${}

java 复制代码
@Select("select * from student where name = ${name}")
Student findByName(String name);

会报错,因为拼接后的 SQL 变成 WHERE name = 张三,缺少引号。必须手动加引号:

java 复制代码
@Select("select * from student where name = '${name}'")
Student findByName(String name);

但这样仍然有安全问题。

2.2 本质区别:预编译 vs 字符串拼接

sql执行流程:

  • 语法解析
  • sql优化
  • sql编译
  • sql执行

  • #{} :MyBatis 会将其替换为 ?,然后通过 PreparedStatementsetXXX 方法赋值。SQL 在数据库端先被编译(解析、优化),再填充参数,称为 预编译
  • ${} :直接将参数值拼接到 SQL 字符串中,然后发送完整 SQL 给数据库,称为 即时编译(Immediate Statement)。【${}这个东西在编程中一般就是取值的意思】

2.3 为什么 #{} 更安全?

SQL 注入攻击:攻击者在输入中嵌入 SQL 关键字,使拼接后的 SQL 改变原意。例如:

java 复制代码
@Select("select * from student where name = '${name}'")
List<Student> findByName(String name);

正常调用:findByName("张三") → SQL: SELECT * FROM student WHERE name = '张三'

攻击者输入:" ' or '1'='1 " → SQL 变成:

sql 复制代码
select * from student where name = '' or '1'='1'

因为 '1'='1' 永远为真,这个查询会返回所有学生,造成数据泄露甚至登录绕过。

如果改用 #{}

java 复制代码
@Select("select * from student where name = #{name}")
List<Student> findByName(String name);

即使输入 " ' or '1'='1 ",也会被当作一个完整的字符串值去匹配 name 字段,不会改变 SQL 语义。

所以,#{} 可以防止 SQL 注入,是安全的;${} 存在注入风险,必须谨慎使用。

2.4 为什么 ${} 还要存在?【排序】

有些场景下,#{} 无法胜任,因为 #{} 会自动给字符串加引号,而 SQL 的某些部分不能加引号。

场景1:排序字段

java 复制代码
@Select("select * from student order by id #{order}")
List<Student> orderBy(String order);

执行时会变成 order by id 'desc',语法错误。必须用 ${}

java 复制代码
@Select("select * from student order by id ${order}")
List<Student> orderBy(String order);

然而使用{}是会有风险的,那么基于这个风险的话,我们怎么做呢?此时我们就做一个参数校验\[使用Controller层校验\]就行了,你看 {order}中order就只有两个值:降序desc 和升序 asc 那么你就校验一下这个参数只允许是这两个就行了。


场景2:表名动态传入

java 复制代码
@Select("select * from ${tableName}")
List<Student> queryTable(String tableName);

但是还是为了解决sql注入的问题,我们要做参数校验。

场景3:某些数据库函数参数

比如 limit 后面不能用 #{},因为会被加引号,导致错误。

在这些场景下,只能使用 ${},但要严格控制输入,比如用枚举限制排序字段,不允许用户随意传值。

2.5 模糊查询的正确姿势

我们经常写 like '%#{key}%',但这样会报错。因为 #{} 会被替换成 ?,最终变成 like '%'? '%',语法错误。

错误写法

java 复制代码
@Select("select * from student where name like '%#{key}%'")
List<Student> search(String key);

被加了引号


如果使用${}的话那么此时可以查出来,但是存在SQL注入问题,那么怎么解决呢?

答:正确写法 :用 MySQL 的 concat 函数拼接,确保 #{} 被正确预编译:

java 复制代码
@Select("select * from student where name like concat('%', #{key}, '%')")
List<Student> search(String key);

这样既避免了 SQL 注入,又能正常模糊查询。

2.6 总结对比

特性 #{} ${}
处理方式 预编译占位符 ? 直接字符串替换
SQL 注入 防止 存在风险
自动加引号 字符串自动加 不加,需手动加
适用场景 绝大多数参数 排序字段、表名、动态列名
性能 高(可缓存执行计划) 低(每次重新编译)

原则 :能用 #{} 的地方绝不用 ${},只有在非用不可时才用 ${},并且要做好输入校验(比如用白名单限制)。


3. 数据库连接池:为什么需要它?

3.1 没有连接池会怎样?


每次执行 SQL 都需要:

  1. 建立 TCP 连接(三次握手)
  2. 数据库认证用户名密码
  3. 执行 SQL
  4. 断开连接(四次挥手)

这个过程开销极大,尤其是在高并发场景下,频繁创建销毁连接会成为系统瓶颈。

3.2 连接池的工作原理

连接池在程序启动时创建一批连接(比如 10 个),放在一个"池子"里。需要操作数据库时,从池子 一个连接,用完归还到池子。如果池子满了,请求就排队等待。这样就避免了反复创建销毁连接,大幅提升性能和并发能力。

3.3 常见的数据库连接池

  • HikariCP:Spring Boot 2.x 以后默认的连接池,性能极高,号称"光速"(Hikari 是日语"光"的意思)。
  • Druid:阿里巴巴开源的连接池,功能强大,支持监控、统计、SQL 防火墙等,在国内使用广泛。
  • C3P0、DBCP:老牌连接池,现在使用较少。

3.4 如何切换连接池?

Spring Boot 默认使用 HikariCP,如果我们想切换到 Druid,只需引入依赖即可【其他什么都不用刚干】:

Spring Boot 3.x

xml 复制代码
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-3-starter</artifactId>
    <version>1.2.21</version>
</dependency>

Spring Boot 2.x

xml 复制代码
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.17</version>
</dependency>

引入后,Spring Boot 会自动配置 Druid 数据源,无需额外配置。启动时日志会显示 DataSource: DruidDataSource,说明切换成功。

Druid 还提供了监控页面,可以实时查看 SQL 执行情况、连接池状态等,非常适合生产环境。具体配置可以参考官方文档。


4. 企业开发规范与总结

4.1 MySQL 开发规范

  1. 表名、字段名全小写+下划线

    因为 MySQL 在 Windows 下不区分大小写,但在 Linux 下区分,全小写可以避免环境差异。例如:user_infocreate_time

  2. 表必备三字段

    • id:主键,通常用 bigint unsigned auto_increment
    • create_time:创建时间,类型 datemite default now()
    • update_time:更新时间,类型 datemite default now() on update now()
  3. 查询避免 SELECT *

    • 只查需要的字段,减少网络传输。
    • 避免因表结构变化导致 resultMap 映射不一致。
    • 对大字段(如 text)尤其要谨慎。

4.2 #{} 和 ${} 使用总结

  • 优先使用 #{},安全且高效。
  • 只有排序、表名、动态列名等无法使用 #{} 的场景,才考虑 ${},并严格控制输入【做参数校验】。
  • 模糊查询使用 concat('%', #{key}, '%')

4.3 多表查询建议

  • 简单关联(如一对一、一对多)可以用 SQL 的 JOIN 一次查完。
  • 复杂关联(多表 join,大表关联)建议拆成多次单表查询,在业务层用代码组装,减轻数据库压力。
  • 扩展实体类来接收关联字段,这是 MyBatis 中最常用的方式。

5. 预告:动态SQL

到此,我们已经学习了 MyBatis 的几乎所有基础操作。下一篇我们将进入 动态SQL 的世界,实现按条件查询、批量操作、复杂判断等高级功能,让 SQL 真正"活"起来!

老铁们,如果你觉得这篇文章对你有帮助,别忘了
👍 点赞
⭐ 收藏
👀 关注

我们下期见!

相关推荐
2501_945423544 小时前
用Matplotlib绘制专业图表:从基础到高级
jvm·数据库·python
2301_793804694 小时前
使用Python处理计算机图形学(PIL/Pillow)
jvm·数据库·python
哆啦A梦158810 小时前
Springboot整合MyBatis实现数据库操作
数据库·spring boot·mybatis
Zzzzmo_10 小时前
【MySQL】JDBC(含settings.xml文件配置/配置国内镜像以及pom.xml文件修改)
数据库·mysql
FirstFrost --sy11 小时前
MySQL内置函数
数据库·mysql
2401_8796938712 小时前
将Python Web应用部署到服务器(Docker + Nginx)
jvm·数据库·python
reembarkation12 小时前
光标在a-select,鼠标已经移出,下拉框跟随页面滚动
java·数据库·sql
eggwyw12 小时前
MySQL-练习-数据汇总-CASE WHEN
数据库·mysql
星轨zb12 小时前
通过实际demo掌握SpringSecurity+MP中的基本框架搭建
数据库·spring boot·spring security·mp