前言
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 的使用效率。