【Day47】MyBatis 进阶:动态 SQL、关联查询(一对一 / 一对多)

前言

MyBatis 的核心优势不仅在于简化 JDBC 操作,更在于其强大的动态 SQL 和灵活的关联查询能力。动态 SQL 解决了传统 JDBC 中 SQL 拼接的痛点,关联查询则完美适配数据库表之间的一对一、一对多等关系。本文将从实战角度出发,详细讲解动态 SQL 的常用标签、一对一 / 一对多关联查询的实现方式,结合完整案例让你掌握 MyBatis 进阶核心技能。

一、环境准备

1. 核心依赖(pom.xml)

沿用 Day46 的依赖,无需新增:

xml

XML 复制代码
<!-- MyBatis核心依赖 -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.10</version>
</dependency>

<!-- MySQL驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.28</version>
    <scope>runtime</scope>
</dependency>

<!-- JUnit单元测试 -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
    <scope>test</scope>
</dependency>

<!-- 日志依赖 -->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

2. 数据库准备(新增关联表)

mybatis_demo数据库中新增订单表(order)和用户表(user)的关联关系,以及订单详情表(order_item):

sql

sql 复制代码
-- 1. 用户表(基础表,沿用Day46)
CREATE TABLE IF NOT EXISTS user (
    id INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID',
    username VARCHAR(50) NOT NULL COMMENT '用户名',
    password VARCHAR(50) NOT NULL COMMENT '密码',
    age INT COMMENT '年龄',
    email VARCHAR(100) COMMENT '邮箱',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) COMMENT '用户表';

-- 2. 订单表(一对一/一对多关联用户表)
CREATE TABLE IF NOT EXISTS `order` (
    id INT PRIMARY KEY AUTO_INCREMENT COMMENT '订单ID',
    order_no VARCHAR(50) NOT NULL COMMENT '订单编号',
    total_amount DECIMAL(10,2) NOT NULL COMMENT '订单总金额',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    user_id INT NOT NULL COMMENT '关联用户ID',
    FOREIGN KEY (user_id) REFERENCES user(id)
) COMMENT '订单表';

-- 3. 订单详情表(一对多关联订单表)
CREATE TABLE IF NOT EXISTS order_item (
    id INT PRIMARY KEY AUTO_INCREMENT COMMENT '详情ID',
    order_id INT NOT NULL COMMENT '关联订单ID',
    product_name VARCHAR(100) NOT NULL COMMENT '商品名称',
    price DECIMAL(10,2) NOT NULL COMMENT '商品单价',
    quantity INT NOT NULL COMMENT '购买数量',
    FOREIGN KEY (order_id) REFERENCES `order`(id)
) COMMENT '订单详情表';

-- 插入测试数据
INSERT INTO user (username, password, age, email) VALUES
('admin', '123456', 25, 'admin@test.com'),
('zhangsan', '654321', 22, 'zhangsan@test.com');

INSERT INTO `order` (order_no, total_amount, user_id) VALUES
('ORDER_20260121_001', 199.98, 1),
('ORDER_20260121_002', 299.97, 2);

INSERT INTO order_item (order_id, product_name, price, quantity) VALUES
(1, 'Java编程思想', 99.99, 2),
(2, 'Spring实战', 99.99, 3);

3. 核心配置文件(mybatis-config.xml)

新增下划线转驼峰自动映射配置,简化结果集映射:

xml

XML 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- 全局设置:开启下划线转驼峰自动映射 -->
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <!-- 开启日志打印 -->
        <setting name="logImpl" value="LOG4J"/>
    </settings>

    <!-- 环境配置 -->
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis_demo?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>

    <!-- 映射器配置 -->
    <mappers>
        <mapper resource="mapper/UserMapper.xml"/>
        <mapper resource="mapper/OrderMapper.xml"/>
        <mapper resource="mapper/OrderItemMapper.xml"/>
    </mappers>
</configuration>

二、动态 SQL:灵活拼接 SQL 的核心

动态 SQL 是 MyBatis 提供的一套标签,用于根据不同条件动态拼接 SQL 语句,避免手动拼接导致的语法错误和 SQL 注入风险。

1. 常用动态 SQL 标签

标签 作用
<if> 单条件判断,满足则拼接 SQL 片段
<where> 自动处理 WHERE 关键字,智能去除多余的 AND/OR
<choose> 多条件分支判断(类似 Java 的 switch-case)
<set> 用于 UPDATE 语句,自动处理 SET 关键字,去除多余的逗号
<foreach> 遍历集合(List/Array/Map),常用于 IN 查询、批量插入
<trim> 自定义字符串截取,替代 where/set 的功能

2. 动态 SQL 实战案例(UserMapper)

步骤 1:编写 User 实体类(沿用 Day46)

java

运行

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

import java.util.Date;
import java.util.List;

public class User {
    private Integer id;
    private String username;
    private String password;
    private Integer age;
    private String email;
    private Date createTime;
    // 一对多关联:一个用户对应多个订单
    private List<Order> orderList;

    // 无参构造、有参构造、getter/setter、toString(省略,需手动添加)
}

步骤 2:UserMapper 接口

java

运行

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

import com.example.entity.User;
import org.apache.ibatis.annotations.Param;

import java.util.List;

public interface UserMapper {
    /**
     * 动态条件查询用户
     */
    List<User> findByCondition(@Param("username") String username, @Param("age") Integer age);

    /**
     * 动态更新用户信息
     */
    int updateUserDynamic(User user);

    /**
     * 批量删除用户(foreach)
     */
    int batchDelete(@Param("ids") List<Integer> ids);

    /**
     * 批量插入用户(foreach)
     */
    int batchInsert(@Param("userList") List<User> userList);
}

步骤 3:UserMapper.xml(动态 SQL 核心)

xml

XML 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.UserMapper">
    <resultMap id="UserResultMap" type="com.example.entity.User">
        <id column="id" property="id"/>
        <result column="username" property="username"/>
        <result column="password" property="password"/>
        <result column="age" property="age"/>
        <result column="email" property="email"/>
        <result column="create_time" property="createTime"/>
    </resultMap>

    <!-- 1. if + where:动态条件查询 -->
    <select id="findByCondition" resultMap="UserResultMap">
        SELECT * FROM user
        <where>
            <if test="username != null and username != ''">
                AND username LIKE CONCAT('%', #{username}, '%')
            </if>
            <if test="age != null">
                AND age > #{age}
            </if>
        </where>
    </select>

    <!-- 2. if + set:动态更新 -->
    <update id="updateUserDynamic">
        UPDATE user
        <set>
            <if test="username != null and username != ''">
                username = #{username},
            </if>
            <if test="password != null and password != ''">
                password = #{password},
            </if>
            <if test="age != null">
                age = #{age},
            </if>
            <if test="email != null and email != ''">
                email = #{email},
            </if>
        </set>
        WHERE id = #{id}
    </update>

    <!-- 3. foreach:批量删除(IN查询) -->
    <delete id="batchDelete">
        DELETE FROM user WHERE id IN
        <foreach collection="ids" item="id" open="(" separator="," close=")">
            #{id}
        </foreach>
    </delete>

    <!-- 4. foreach:批量插入 -->
    <insert id="batchInsert">
        INSERT INTO user (username, password, age, email)
        VALUES
        <foreach collection="userList" item="user" separator=",">
            (#{user.username}, #{user.password}, #{user.age}, #{user.email})
        </foreach>
    </insert>
</mapper>

步骤 4:动态 SQL 测试

java

运行

java 复制代码
package com.example.test;

import com.example.entity.User;
import com.example.mapper.UserMapper;
import com.example.util.MyBatisUtil;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;

import java.util.ArrayList;
import java.util.List;

public class DynamicSqlTest {

    /**
     * 测试动态条件查询
     */
    @Test
    public void testFindByCondition() {
        try (SqlSession session = MyBatisUtil.getSqlSession()) {
            UserMapper mapper = session.getMapper(UserMapper.class);
            // 条件:用户名包含"admin",年龄大于20
            List<User> userList = mapper.findByCondition("admin", 20);
            System.out.println("动态查询结果:");
            userList.forEach(System.out::println);
        }
    }

    /**
     * 测试动态更新
     */
    @Test
    public void testUpdateUserDynamic() {
        try (SqlSession session = MyBatisUtil.getSqlSession(true)) {
            UserMapper mapper = session.getMapper(UserMapper.class);
            User user = new User();
            user.setId(1);
            user.setAge(26); // 仅更新年龄
            int rows = mapper.updateUserDynamic(user);
            System.out.println("动态更新受影响行数:" + rows);
        }
    }

    /**
     * 测试批量删除
     */
    @Test
    public void testBatchDelete() {
        try (SqlSession session = MyBatisUtil.getSqlSession(true)) {
            UserMapper mapper = session.getMapper(UserMapper.class);
            List<Integer> ids = new ArrayList<>();
            ids.add(3);
            ids.add(4);
            int rows = mapper.batchDelete(ids);
            System.out.println("批量删除受影响行数:" + rows);
        }
    }

    /**
     * 测试批量插入
     */
    @Test
    public void testBatchInsert() {
        try (SqlSession session = MyBatisUtil.getSqlSession(true)) {
            UserMapper mapper = session.getMapper(UserMapper.class);
            List<User> userList = new ArrayList<>();
            userList.add(new User(null, "lisi", "111111", 28, "lisi@test.com", null));
            userList.add(new User(null, "wangwu", "222222", 30, "wangwu@test.com", null));
            int rows = mapper.batchInsert(userList);
            System.out.println("批量插入受影响行数:" + rows);
        }
    }
}

三、关联查询:一对一(订单→用户)

1. 实体类准备

Order 实体类(包含一对一关联的 User)

java

运行

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

import java.math.BigDecimal;
import java.util.Date;
import java.util.List;

public class Order {
    private Integer id;
    private String orderNo;
    private BigDecimal totalAmount;
    private Date createTime;
    private Integer userId;
    // 一对一关联:一个订单对应一个用户
    private User user;
    // 一对多关联:一个订单对应多个订单详情
    private List<OrderItem> orderItemList;

    // 无参构造、有参构造、getter/setter、toString(省略)
}

2. OrderMapper 接口

java

运行

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

import com.example.entity.Order;
import org.apache.ibatis.annotations.Param;

import java.util.List;

public interface OrderMapper {
    /**
     * 一对一查询:查询订单并关联用户信息(级联属性)
     */
    Order findOrderWithUser(@Param("id") Integer id);

    /**
     * 一对一查询:使用association标签(推荐)
     */
    Order findOrderWithUserByAssociation(@Param("id") Integer id);
}

3. OrderMapper.xml(一对一关联查询)

MyBatis 实现一对一关联查询有两种方式:级联属性association 标签(推荐)。

xml

XML 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.OrderMapper">
    <!-- 方式1:级联属性映射(简单场景) -->
    <resultMap id="OrderUserResultMap1" type="com.example.entity.Order">
        <id column="id" property="id"/>
        <result column="order_no" property="orderNo"/>
        <result column="total_amount" property="totalAmount"/>
        <result column="create_time" property="createTime"/>
        <result column="user_id" property="userId"/>
        <!-- 级联映射用户属性 -->
        <result column="user_id" property="user.id"/>
        <result column="username" property="user.username"/>
        <result column="age" property="user.age"/>
        <result column="email" property="user.email"/>
    </resultMap>

    <!-- 方式2:association标签(推荐,清晰) -->
    <resultMap id="OrderUserResultMap2" type="com.example.entity.Order">
        <id column="id" property="id"/>
        <result column="order_no" property="orderNo"/>
        <result column="total_amount" property="totalAmount"/>
        <result column="create_time" property="createTime"/>
        <result column="user_id" property="userId"/>
        <!-- association:一对一关联 -->
        <association property="user" javaType="com.example.entity.User">
            <id column="user_id" property="id"/>
            <result column="username" property="username"/>
            <result column="age" property="age"/>
            <result column="email" property="email"/>
        </association>
    </resultMap>

    <!-- 一对一查询:级联属性 -->
    <select id="findOrderWithUser" resultMap="OrderUserResultMap1">
        SELECT o.*, u.username, u.age, u.email
        FROM `order` o
        LEFT JOIN user u ON o.user_id = u.id
        WHERE o.id = #{id}
    </select>

    <!-- 一对一查询:association标签 -->
    <select id="findOrderWithUserByAssociation" resultMap="OrderUserResultMap2">
        SELECT o.*, u.username, u.age, u.email
        FROM `order` o
        LEFT JOIN user u ON o.user_id = u.id
        WHERE o.id = #{id}
    </select>
</mapper>

4. 一对一查询测试

java

运行

java 复制代码
@Test
public void testFindOrderWithUser() {
    try (SqlSession session = MyBatisUtil.getSqlSession()) {
        OrderMapper mapper = session.getMapper(OrderMapper.class);
        Order order = mapper.findOrderWithUserByAssociation(1);
        System.out.println("订单信息:" + order);
        System.out.println("关联用户信息:" + order.getUser());
    }
}

四、关联查询:一对多(用户→订单、订单→订单详情)

1. 实体类准备

OrderItem 实体类

java

运行

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

import java.math.BigDecimal;

public class OrderItem {
    private Integer id;
    private Integer orderId;
    private String productName;
    private BigDecimal price;
    private Integer quantity;

    // 无参构造、有参构造、getter/setter、toString(省略)
}

2. OrderMapper 接口扩展

java

运行

java 复制代码
/**
 * 一对多查询:查询用户及关联的所有订单
 */
User findUserWithOrders(@Param("userId") Integer userId);

/**
 * 一对多查询:查询订单及关联的所有订单详情
 */
Order findOrderWithItems(@Param("orderId") Integer orderId);

3. OrderMapper.xml(一对多关联查询)

使用<collection>标签实现一对多关联映射:

xml

XML 复制代码
<!-- 一对多:用户→订单 -->
<resultMap id="UserWithOrdersResultMap" type="com.example.entity.User">
    <id column="user_id" property="id"/>
    <result column="username" property="username"/>
    <result column="age" property="age"/>
    <result column="email" property="email"/>
    <!-- collection:一对多关联 -->
    <collection property="orderList" ofType="com.example.entity.Order">
        <id column="order_id" property="id"/>
        <result column="order_no" property="orderNo"/>
        <result column="total_amount" property="totalAmount"/>
        <result column="create_time" property="createTime"/>
    </collection>
</resultMap>

<!-- 一对多:订单→订单详情 -->
<resultMap id="OrderWithItemsResultMap" type="com.example.entity.Order">
    <id column="order_id" property="id"/>
    <result column="order_no" property="orderNo"/>
    <result column="total_amount" property="totalAmount"/>
    <!-- 一对一关联用户 -->
    <association property="user" javaType="com.example.entity.User">
        <id column="user_id" property="id"/>
        <result column="username" property="username"/>
    </association>
    <!-- 一对多关联订单详情 -->
    <collection property="orderItemList" ofType="com.example.entity.OrderItem">
        <id column="item_id" property="id"/>
        <result column="product_name" property="productName"/>
        <result column="price" property="price"/>
        <result column="quantity" property="quantity"/>
    </collection>
</resultMap>

<!-- 查询用户及关联订单 -->
<select id="findUserWithOrders" resultMap="UserWithOrdersResultMap">
    SELECT u.id AS user_id, u.username, u.age, u.email,
           o.id AS order_id, o.order_no, o.total_amount, o.create_time
    FROM user u
    LEFT JOIN `order` o ON u.id = o.user_id
    WHERE u.id = #{userId}
</select>

<!-- 查询订单及关联详情 -->
<select id="findOrderWithItems" resultMap="OrderWithItemsResultMap">
    SELECT o.id AS order_id, o.order_no, o.total_amount,
           u.id AS user_id, u.username,
           oi.id AS item_id, oi.product_name, oi.price, oi.quantity
    FROM `order` o
    LEFT JOIN user u ON o.user_id = u.id
    LEFT JOIN order_item oi ON o.id = oi.order_id
    WHERE o.id = #{orderId}
</select>

4. 一对多查询测试

java

运行

java 复制代码
/**
 * 测试一对多:用户→订单
 */
@Test
public void testFindUserWithOrders() {
    try (SqlSession session = MyBatisUtil.getSqlSession()) {
        OrderMapper mapper = session.getMapper(OrderMapper.class);
        User user = mapper.findUserWithOrders(1);
        System.out.println("用户信息:" + user);
        System.out.println("关联订单列表:");
        user.getOrderList().forEach(System.out::println);
    }
}

/**
 * 测试一对多:订单→订单详情
 */
@Test
public void testFindOrderWithItems() {
    try (SqlSession session = MyBatisUtil.getSqlSession()) {
        OrderMapper mapper = session.getMapper(OrderMapper.class);
        Order order = mapper.findOrderWithItems(1);
        System.out.println("订单信息:" + order);
        System.out.println("关联订单详情:");
        order.getOrderItemList().forEach(System.out::println);
    }
}

五、关联查询进阶:分步查询(延迟加载)

上述关联查询属于嵌套结果查询 (一次 SQL 查询所有数据),MyBatis 还支持分步查询(多次 SQL,按需加载),配合延迟加载可提升性能。

1. 分步查询实现(订单→用户)

OrderMapper.xml 新增分步查询

xml

XML 复制代码
<!-- 分步查询1:查询订单基本信息 -->
<select id="findOrderById" resultMap="OrderStepResultMap">
    SELECT * FROM `order` WHERE id = #{id}
</select>

<!-- 分步查询2:通过user_id查询用户信息 -->
<resultMap id="OrderStepResultMap" type="com.example.entity.Order">
    <id column="id" property="id"/>
    <result column="order_no" property="orderNo"/>
    <result column="total_amount" property="totalAmount"/>
    <result column="user_id" property="userId"/>
    <!-- association分步查询 -->
    <association property="user" 
                 select="com.example.mapper.UserMapper.findUserById"
                 column="user_id"/>
</resultMap>

UserMapper.xml 新增查询方法

xml

XML 复制代码
<select id="findUserById" resultMap="UserResultMap">
    SELECT * FROM user WHERE id = #{id}
</select>

2. 开启延迟加载(核心配置文件)

xml

XML 复制代码
<settings>
    <!-- 开启下划线转驼峰 -->
    <setting name="mapUnderscoreToCamelCase" value="true"/>
    <!-- 开启延迟加载 -->
    <setting name="lazyLoadingEnabled" value="true"/>
    <!-- 按需加载(默认false,加载所有属性) -->
    <setting name="aggressiveLazyLoading" value="false"/>
</settings>

3. 延迟加载测试

java

运行

java 复制代码
@Test
public void testLazyLoad() {
    try (SqlSession session = MyBatisUtil.getSqlSession()) {
        OrderMapper mapper = session.getMapper(OrderMapper.class);
        // 第一步:仅查询订单信息(未查询用户)
        Order order = mapper.findOrderById(1);
        System.out.println("订单编号:" + order.getOrderNo());
        
        // 第二步:调用user属性时,才执行第二步SQL查询用户
        System.out.println("用户名称:" + order.getUser().getUsername());
    }
}

六、核心总结

1. 动态 SQL 核心

  • <if>+<where>:解决动态条件查询,自动处理 AND/OR;
  • <if>+<set>:解决动态更新,自动处理逗号;
  • <foreach>:解决批量操作(IN 查询、批量插入 / 删除);
  • 动态 SQL 避免了手动拼接 SQL 的繁琐和风险,是 MyBatis 的核心优势之一。

2. 关联查询核心

关联类型 实现标签 关键属性
一对一 <association> javaType:指定关联对象类型
一对多 <collection> ofType:指定集合元素类型

3. 关联查询方式

  • 嵌套结果查询:一次 SQL,关联查询所有数据,适合数据量小的场景;
  • 分步查询:多次 SQL,配合延迟加载(按需加载),适合数据量大、关联层级深的场景。

掌握动态 SQL 和关联查询,你已经能应对绝大多数 MyBatis 开发场景。后续可深入学习 MyBatis 的缓存、插件、分页插件(PageHelper)等内容,进一步提升 MyBatis 的使用效率。

相关推荐
biter00881 小时前
Ubuntu 上搜狗输入法突然“消失 / 只能英文”的排查与修复教程
linux·数据库·ubuntu
何以不说话1 小时前
MyCat实现 MySQL 读写分离
数据库·mysql
齐 飞2 小时前
SQL server使用MybatisPlus查询SQL加上WITH (NOLOCK)
数据库·mysql·sqlserver
_F_y2 小时前
MySQL表的增删查改
android·数据库·mysql
yangSnowy2 小时前
Redis数据类型
数据库·redis·wpf
@我不是大鹏2 小时前
3、Spring AI Alibaba(SAA)零基础速通实战之Ollama私有化部署和对接本地大模型
数据库·人工智能·spring
Linging_242 小时前
PGSQL与Mysql对比学习
数据库·学习·mysql·postgresql
周某人姓周2 小时前
sql报错注入常见7个函数
sql·安全·web安全·网络安全
Anarkh_Lee2 小时前
【免费开源】MCP 数据库万能连接器:用自然语言查询和分析数据
数据库·开源·ai编程·claude·自然语言·mcp·cherry studio