踩完 10 个坑后,我把多表查询 + MyBatis 动态 SQL 写成了干货

各位后端搭子们,是不是也遇到过这种情况:产品说 "要查用户带订单带商品信息",你对着三张表发呆半小时;写动态查询时,多余的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:处理一对多关系(比如查用户带多个订单)

实战栗子:查用户带多个订单(一对多)

  1. 先定义实体类(User 里有 List orders)
arduino 复制代码
public class User {
    private Integer id;
    private String name;
    private List<Order> orders; // 一对多关联
    // getter/setter
}
  1. 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只对 "紧跟的第一个查询" 生效!如果后面还有查询,不会分页哦~

最后:这些坑你肯定踩过(避坑总结)

  1. 多表查询忘写ON条件,导致笛卡尔积;
  1. MyBatis 关联查询时,字段名重复没写别名,映射失败;
  1. 动态 SQL 用if时没加where,导致多余AND;
  1. PageHelper 没放在查询前面,分页失效;
  1. 子查询套太多层,SQL 性能爆炸。

如果这篇文章帮你避开了这些坑,欢迎点赞收藏~ 你在多表查询或 MyBatis 里还遇到过哪些奇葩问题?评论区聊聊,咱一起解决!

相关推荐
没有bug.的程序员11 分钟前
AOT 编译与 GraalVM 实战:Java 云原生的终极进化
java·python·云原生·graalvm·aot
找不到、了26 分钟前
常用的分布式ID设计方案
java·分布式
野区捕龙为宠34 分钟前
Unity Netcode for GameObjects(多人联机小Demo)
java·unity·游戏引擎
陈随易1 小时前
10年老前端,分享20+严选技术栈
前端·后端·程序员
携欢1 小时前
Portswigger靶场之 Blind SQL injection with time delays通关秘籍
数据库·sql
汪子熙1 小时前
计算机世界里的 blob:从数据库 BLOB 到 Git、Web API 与云存储的二进制宇宙
后端
十八旬1 小时前
苍穹外卖项目实战(日记十)-记录实战教程及问题的解决方法-(day3-2)新增菜品功能完整版
java·开发语言·spring boot·mysql·idea·苍穹外卖
鞋尖的灰尘1 小时前
springboot-事务
java·后端
元元的飞1 小时前
6、Spring AI Alibaba MCP结合Nacos自动注册与发现
后端·ai编程
Cisyam1 小时前
Go环境搭建实战:告别Java环境配置的复杂
后端