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

张开发
2026/5/6 19:12:53 15 分钟阅读

分享文章

【JavaEE20-后端部分】 MyBatis 入门第四篇:多表查询、#{}与${}详解、数据库连接池
老铁们前三篇我们学会了 MyBatis 的基本操作从注解到 XML从参数传递到结果映射可以说已经能独立完成大部分单表 CRUD 了。但真实项目中数据往往分散在多张表中比如用户表、文章表、订单表……很多时候我们需要一次性把多张表的数据查出来。今天我们就来聊聊多表查询以及 MyBatis 中两个非常重要的符号#{}和${}。另外我们还会学习数据库连接池的配置以及一些企业级开发规范。1. 多表查询当数据来自多张表1.1 为什么需要多表查询在实际业务中数据往往不是孤立存在的。比如一篇文章属于一个作者我们想同时查出文章内容和作者信息。一个订单关联了用户、商品、地址等多张表。如果分别查询代码会变得冗长而且需要多次数据库交互。多表查询可以一次 SQL 把相关数据都取出来减少数据库访问次数。1.2 准备工作创建用户表和文章表的关联关系-- 创建表[用户表]droptableifexistsuser_info;createtableuser_info(idint(11)notnullprimarykeyauto_increment,usernamevarchar(127)notnull,passwordvarchar(127)notnull,agetinyint(4)notnull,gendertinyint(4)default0comment1-男 2-女 0-默认,phonevarchar(15)defaultnull,delete_flagtinyint(4)default0comment0-正常, 1-删除,create_timedatetimedefaultnow(),update_timedatetimedefaultnow()onupdatenow());-- 添加用户信息insertintouser_info(username,password,age,gender,phone)values(管理员,管理员123,18,1,18612340001);insertintouser_info(username,password,age,gender,phone)values(张三,张三123,18,1,18612340002);insertintouser_info(username,password,age,gender,phone)values(李四,李四123,18,1,18612340003);insertintouser_info(username,password,age,gender,phone)values(王五,王五123,18,1,18612340004);-- 创建文章表droptableifexistsarticle_info;createtablearticle_info(idintprimarykeyauto_increment,titlevarchar(100)notnull,contenttextnotnull,uidintnotnull,delete_flagtinyint(4)default0comment0-正常, 1-删除,create_timedatetimedefaultnow(),update_timedatetimedefaultnow());-- 插入测试数据insertintoarticle_info(title,content,uid)values(Javaee,Javaee知识介绍,1);用户表文章表1.3 sql查询结果SQL要求我在查询文章表的时候 需要将用户表中的用户名和年龄给查询出来此时涉及到我们的多表查询那么连接条件就是我们的用户id如图所示结果1.4 编写多表查询的 实体类根据我们查询出来的中间表的字段写一个对应的实体类就行了比如packagecom.zhongge.entity;importcom.fasterxml.jackson.annotation.JsonFormat;importlombok.Data;importjava.time.LocalDate;/** * ClassName ArticleInfo * Description TODO 文章实体类 * Author 笨忠 * Date 2026-03-22 10:14 * Version 1.0 */DatapublicclassArticleInfo{privateIntegerid;//文章idprivateStringtitle;//文章标题privateStringcontent;//文章内容JsonFormat(patternyyyy-MM-dd HH:mm:ss,timezoneGMT8)//日期的格式转换privateLocalDatecreateTime;//文章创建的时间JsonFormat(patternyyyy-MM-dd HH:mm:ss,timezoneGMT8)privateLocalDateupdateTime;//文章修改的时间//用户信息privateStringusername;//用户姓名privateIntegerage;//用户年龄}1.5 编写mapper接口和xml文件packagecom.zhongge.mapper;importcom.zhongge.entity.ArticleInfo;importorg.apache.ibatis.annotations.Mapper;/** * InterfaceName ArticleInfoMapper * Description TODO 文章操作接口 * Author 笨忠 * Date 2026-03-22 10:20 * Version 1.0 */MapperpublicinterfaceArticleInfoMapper{//查询文章ArticleInfogetArticleInfoByUserId(Integerid);}?xml version1.0 encodingUTF-8 ?!DOCTYPEmapperPUBLIC-//mybatis.org//DTD Mapper 3.0//ENhttp://mybatis.org/dtd/mybatis-3-mapper.dtdmappernamespacecom.zhongge.mapper.ArticleInfoMapperselectidgetArticleInfoByUserIdresultTypecom.zhongge.entity.ArticleInfoselect 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 数字类型参数Select(select * from student where id #{id})StudentfindById(Integerid);执行时MyBatis 会输出 Preparing: SELECT * FROM student WHERE id ? Parameters: 1(Integer)SQL 中是?占位符参数单独传入这叫预编译。如果换成${}Select(select * from student where id ${id})StudentfindById(Integerid);输出 Preparing: SELECT * FROM student WHERE id 1 Parameters:参数直接拼接到 SQL 中没有占位符这叫字符串替换。2.1.2 字符串类型参数Select(select * from student where name #{name})StudentfindByName(Stringname);正常输出参数被安全设置。如果换成${}Select(select * from student where name ${name})StudentfindByName(Stringname);会报错因为拼接后的 SQL 变成WHERE name 张三缺少引号。必须手动加引号Select(select * from student where name ${name})StudentfindByName(Stringname);但这样仍然有安全问题。2.2 本质区别预编译 vs 字符串拼接sql执行流程语法解析sql优化sql编译sql执行#{}MyBatis 会将其替换为?然后通过PreparedStatement的setXXX方法赋值。SQL 在数据库端先被编译解析、优化再填充参数称为预编译。${}直接将参数值拼接到 SQL 字符串中然后发送完整 SQL 给数据库称为即时编译Immediate Statement。【${}这个东西在编程中一般就是取值的意思】2.3 为什么 #{} 更安全SQL 注入攻击攻击者在输入中嵌入 SQL 关键字使拼接后的 SQL 改变原意。例如Select(select * from student where name ${name})ListStudentfindByName(Stringname);正常调用findByName(张三)→ SQL:SELECT * FROM student WHERE name 张三攻击者输入 or 11 → SQL 变成select*fromstudentwherenameor11因为11永远为真这个查询会返回所有学生造成数据泄露甚至登录绕过。如果改用#{}Select(select * from student where name #{name})ListStudentfindByName(Stringname);即使输入 or 11 也会被当作一个完整的字符串值去匹配name字段不会改变 SQL 语义。所以#{}可以防止 SQL 注入是安全的${}存在注入风险必须谨慎使用。2.4 为什么 ${} 还要存在【排序】有些场景下#{}无法胜任因为#{}会自动给字符串加引号而 SQL 的某些部分不能加引号。场景1排序字段Select(select * from student order by id #{order})ListStudentorderBy(Stringorder);执行时会变成order by id desc语法错误。必须用${}Select(select * from student order by id ${order})ListStudentorderBy(Stringorder);然而使用${}是会有风险的那么基于这个风险的话我们怎么做呢此时我们就做一个参数校验[使用Controller层校验]就行了你看 ${order}中order就只有两个值降序desc 和升序 asc 那么你就校验一下这个参数只允许是这两个就行了。场景2表名动态传入Select(select * from ${tableName})ListStudentqueryTable(StringtableName);但是还是为了解决sql注入的问题我们要做参数校验。场景3某些数据库函数参数比如limit后面不能用#{}因为会被加引号导致错误。在这些场景下只能使用${}但要严格控制输入比如用枚举限制排序字段不允许用户随意传值。2.5 模糊查询的正确姿势我们经常写like %#{key}%但这样会报错。因为#{}会被替换成?最终变成like %? %语法错误。错误写法Select(select * from student where name like %#{key}%)ListStudentsearch(Stringkey);被加了引号如果使用${}的话那么此时可以查出来但是存在SQL注入问题那么怎么解决呢答正确写法用 MySQL 的concat函数拼接确保#{}被正确预编译Select(select * from student where name like concat(%, #{key}, %))ListStudentsearch(Stringkey);这样既避免了 SQL 注入又能正常模糊查询。2.6 总结对比特性#{}${}处理方式预编译占位符?直接字符串替换SQL 注入防止存在风险自动加引号字符串自动加不加需手动加适用场景绝大多数参数排序字段、表名、动态列名性能高可缓存执行计划低每次重新编译原则能用#{}的地方绝不用${}只有在非用不可时才用${}并且要做好输入校验比如用白名单限制。3. 数据库连接池为什么需要它3.1 没有连接池会怎样每次执行 SQL 都需要建立 TCP 连接三次握手数据库认证用户名密码执行 SQL断开连接四次挥手这个过程开销极大尤其是在高并发场景下频繁创建销毁连接会成为系统瓶颈。3.2 连接池的工作原理连接池在程序启动时创建一批连接比如 10 个放在一个“池子”里。需要操作数据库时从池子借一个连接用完归还到池子。如果池子满了请求就排队等待。这样就避免了反复创建销毁连接大幅提升性能和并发能力。3.3 常见的数据库连接池HikariCPSpring Boot 2.x 以后默认的连接池性能极高号称“光速”Hikari 是日语“光”的意思。Druid阿里巴巴开源的连接池功能强大支持监控、统计、SQL 防火墙等在国内使用广泛。C3P0、DBCP老牌连接池现在使用较少。3.4 如何切换连接池Spring Boot 默认使用 HikariCP如果我们想切换到 Druid只需引入依赖即可【其他什么都不用刚干】Spring Boot 3.xdependencygroupIdcom.alibaba/groupIdartifactIddruid-spring-boot-3-starter/artifactIdversion1.2.21/version/dependencySpring Boot 2.xdependencygroupIdcom.alibaba/groupIdartifactIddruid-spring-boot-starter/artifactIdversion1.1.17/version/dependency引入后Spring Boot 会自动配置 Druid 数据源无需额外配置。启动时日志会显示DataSource: DruidDataSource说明切换成功。Druid 还提供了监控页面可以实时查看 SQL 执行情况、连接池状态等非常适合生产环境。具体配置可以参考官方文档。4. 企业开发规范与总结4.1 MySQL 开发规范表名、字段名全小写下划线因为 MySQL 在 Windows 下不区分大小写但在 Linux 下区分全小写可以避免环境差异。例如user_info、create_time。表必备三字段id主键通常用bigint unsigned auto_incrementcreate_time创建时间类型datemite default now()update_time更新时间类型datemite default now() on update now()查询避免SELECT *只查需要的字段减少网络传输。避免因表结构变化导致resultMap映射不一致。对大字段如text尤其要谨慎。4.2 #{} 和 ${} 使用总结优先使用#{}安全且高效。只有排序、表名、动态列名等无法使用#{}的场景才考虑${}并严格控制输入【做参数校验】。模糊查询使用concat(%, #{key}, %)。4.3 多表查询建议简单关联如一对一、一对多可以用 SQL 的JOIN一次查完。复杂关联多表 join大表关联建议拆成多次单表查询在业务层用代码组装减轻数据库压力。扩展实体类来接收关联字段这是 MyBatis 中最常用的方式。5. 预告动态SQL到此我们已经学习了 MyBatis 的几乎所有基础操作。下一篇我们将进入动态SQL的世界实现按条件查询、批量操作、复杂判断等高级功能让 SQL 真正“活”起来老铁们如果你觉得这篇文章对你有帮助别忘了 点赞⭐ 收藏 关注我们下期见

更多文章