各位后端搭子们,是不是也遇到过这种情况:产品说 "要查用户带订单带商品信息",你对着三张表发呆半小时;写动态查询时,多余的AND把 SQL 搞崩;分页功能明明写了limit,结果返回全表数据......
今天咱就把 "多表操作 + MyBatis 进阶" 这堆 "乱麻" 捋顺,从 "表的社交关系" 讲到 "插件偷懒技巧",保证看完能笑着写出优雅代码!
一、先搞懂:表和表之间的 "暧昧关系"
其实数据库里的表跟职场一样,不是孤立存在的,主要就三种 "社交模式":
1. 一对一:专属搭档(比如用户表 & 用户详情表)
就像每个员工都有唯一的工牌,一个用户只有一份详细资料(身高、爱好这些),反过来一份资料也只属于一个用户。
识别技巧:外键加unique,或者用主键关联(详情表主键跟用户表主键一样)。
2. 一对多:部门老大 & 下属(比如用户表 & 订单表)
一个用户能下 N 个订单,但每个订单只属于一个用户。这是最常见的关系,比如你在淘宝买 10 件东西,就生成 10 个订单,都绑着你的账号。
识别技巧:在 "多" 的一方(订单表)加外键,指向 "一" 的一方(用户表)的主键。
3. 多对多:项目组 & 成员(比如订单表 & 商品表)
一个订单能买多个商品,一个商品也能出现在多个订单里(比如你买了薯片,别人也买了薯片)。这种关系必须靠 "中间表" 牵线(比如订单商品关联表,存订单 ID 和商品 ID)。
识别技巧:中间表有两个外键,分别指向两个主表的主键,联合起来当主键。
二、多表查询:别再写 "面条 SQL" 了!
搞懂关系后,查询就像 "拉群聊",把需要的表拉进来对话。核心就 3 种玩法:
1. 内连接(INNER JOIN):只留 "双向奔赴" 的数据
相当于 "两个表的交集",只有两边都有匹配的数据才会显示。比如查 "有订单的用户",没下过单的用户直接过滤。
举个栗子:查用户姓名和对应的订单号
sql
SELECT u.name, o.order_no
FROM user u
INNER JOIN order o ON u.id = o.user_id; -- 关键:ON后面写关联条件
坑点提醒:别忘写ON!不然会变成 "笛卡尔积",数据量直接爆炸(比如 100 个用户 + 100 个订单,查出 10000 条垃圾数据)。
2. 外连接:就算你 "单恋",我也记下来
内连接太 "现实",外连接更 "念旧",分左外和右外:
- 左外连接(LEFT JOIN) :左边表的所有数据都保留,右边表没匹配的补NULL。比如查 "所有用户,包括没下过单的",没订单的用户订单号显示NULL。
- 右外连接(RIGHT JOIN) :反过来,右边表数据全保留,左边没匹配的补NULL(实际用得少,左外能搞定大部分场景)。
左外栗子:查所有用户及他们的订单
sql
SELECT u.name, o.order_no
FROM user u
LEFT JOIN order o ON u.id = o.user_id;
3. 子查询:套娃式查数据(别套太多层!)
就是 "查询里套查询",适合逻辑比较复杂的场景。比如查 "买过单价超过 500 元商品的用户",可以先查符合条件的订单 ID,再查对应的用户。
举个栗子:
sql
SELECT name FROM user
WHERE id IN (
SELECT user_id FROM order
WHERE id IN (
SELECT order_id FROM order_goods
WHERE goods_price > 500 -- 先筛高价商品的订单
)
);
温馨提示:子查询别套超过 3 层!不然 SQL 跑得比蜗牛还慢,还难调试(实在复杂就用JOIN)。
三、MyBatis 多表查询:核心就靠 "这两张牌"
用 MyBatis 查多表,别再手动拼 SQL 了!掌握resultMap和关联标签,轻松搞定一对一 / 一对多。
1. 关键 1:resultMap------ 给数据 "搭积木"
MyBatis 默认用resultType映射字段,但多表查询时字段名可能重复(比如两个表都有id),这时候必须用resultMap手动指定映射关系。
2. 关键 2:关联标签 ------ 处理表关系
- association:处理一对一关系(比如查用户带用户详情)
- collection:处理一对多关系(比如查用户带多个订单)
实战栗子:查用户带多个订单(一对多)
- 先定义实体类(User 里有 List orders)
arduino
public class User {
private Integer id;
private String name;
private List<Order> orders; // 一对多关联
// getter/setter
}
- MyBatis 映射文件(UserMapper.xml)
xml
<!-- 定义resultMap:用户+订单 -->
<resultMap id="UserWithOrdersMap" type="com.xxx.User">
<!-- 映射用户表字段 -->
<id column="user_id" property="id"/>
<result column="user_name" property="name"/>
<!-- 映射订单列表(一对多用collection) -->
<collection property="orders" ofType="com.xxx.Order">
<id column="order_id" property="id"/>
<result column="order_no" property="orderNo"/>
<result column="create_time" property="createTime"/>
</collection>
</resultMap>
<!-- 多表查询SQL -->
<select id="getUserWithOrders" resultMap="UserWithOrdersMap">
SELECT
u.id user_id, u.name user_name,
o.id order_id, o.order_no, o.create_time
FROM user u
LEFT JOIN `order` o ON u.id = o.user_id
WHERE u.id = #{userId}
</select>
划重点:ofType指定集合里的元素类型(Order),字段名用别名(user_id、order_id)避免冲突!
四、MyBatis 动态 SQL:写 SQL 像 "搭乐高",灵活又省心
产品经常改需求:"今天要按姓名查,明天要按手机号 + 时间查",总不能写 N 个 SQL 吧?动态 SQL 帮你 "一键适配",核心标签就 5 个:
1. if:看条件 "加不加"
最常用的标签,满足条件就拼接 SQL 片段。比如 "传了姓名就按姓名查,没传就不查"。
xml
<select id="getUserList" resultType="com.xxx.User">
SELECT * FROM user
WHERE 1=1 <!-- 避免后面多AND,也可以用where标签 -->
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="age != null">
AND age = #{age}
</if>
</select>
2. where:自动 "删多余的 AND/OR"
上面的1=1有点傻?用where标签,它会自动去掉开头多余的AND或OR。
bash
<select id="getUserList" resultType="com.xxx.User">
SELECT * FROM user
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="age != null">
AND age = #{age}
</if>
</where>
</select>
3. choose/when/otherwise:"多选一",类似 Java 的 switch
比如 "传了手机号就按手机号查,传了姓名就按姓名查,都没传就查默认状态"。
xml
<select id="getUserList" resultType="com.xxx.User">
SELECT * FROM user
<where>
<choose>
<when test="phone != null and phone != ''">
AND phone = #{phone}
</when>
<when test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</when>
<otherwise>
AND status = 1 <!-- 默认查正常用户 -->
</otherwise>
</choose>
</where>
</select>
4. set:更新时 "删多余的逗号"
更新用户信息时,可能只改姓名,也可能只改年龄,set标签会自动去掉末尾多余的逗号。
bash
<update id="updateUser">
UPDATE user
<set>
<if test="name != null and name != ''">
name = #{name},
</if>
<if test="age != null">
age = #{age},
</if>
</set>
WHERE id = #{id}
</update>
5. foreach:批量操作 "救星"(批量查询 / 删除)
比如 "批量删除多个用户""查询 ID 在 [1,2,3] 里的用户",foreach帮你循环拼接IN条件。
xml
<!-- 批量查询用户 -->
<select id="getUserByIds" resultType="com.xxx.User">
SELECT * FROM user
WHERE id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</select>
参数说明:
- collection:集合参数名(如果是 List 就写 list,数组写 array,或者用 @Param 指定)
- item:循环里的每个元素名(这里是 id)
- open:开头拼接的字符(比如 ()
- close:结尾拼接的字符(比如))
- separator:元素之间的分隔符(比如,)
五、PageHelper:分页不用自己算 limit!懒人福音
手动写limit #{pageNum}, #{pageSize}太麻烦了,还得自己算总条数?PageHelper 插件帮你 "一键分页",三步搞定:
1. 第一步:加依赖(Maven)
xml
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version> <!-- 用最新版就行 -->
</dependency>
2. 第二步:配置分页参数(application.yml)
yaml
pagehelper:
helper-dialect: mysql # 数据库方言(mysql/oracle等)
reasonable: true # 页码不合理时自动处理(比如页码<=0查第一页,超总页数查最后一页)
support-methods-arguments: true # 支持通过Mapper接口参数传递分页参数
3. 第三步:代码里用(就加一行!)
在查询方法前调用PageHelper.startPage(pageNum, pageSize),后面的查询会自动分页。
scss
public PageInfo<User> getUserList(Integer pageNum, Integer pageSize) {
// 这行代码要放在查询方法前面!
PageHelper.startPage(pageNum, pageSize);
// 正常查询,不用加limit
List<User> userList = userMapper.getUserList();
// 包装成PageInfo,里面有总条数、总页数等信息
return new PageInfo<>(userList);
}
踩坑提醒:startPage只对 "紧跟的第一个查询" 生效!如果后面还有查询,不会分页哦~
最后:这些坑你肯定踩过(避坑总结)
- 多表查询忘写ON条件,导致笛卡尔积;
- MyBatis 关联查询时,字段名重复没写别名,映射失败;
- 动态 SQL 用if时没加where,导致多余AND;
- PageHelper 没放在查询前面,分页失效;
- 子查询套太多层,SQL 性能爆炸。
如果这篇文章帮你避开了这些坑,欢迎点赞收藏~ 你在多表查询或 MyBatis 里还遇到过哪些奇葩问题?评论区聊聊,咱一起解决!