Spring Boot 多表关联查询实战(一对多 / 多对一)
在Spring Boot项目开发中,单表CRUD是基础操作,当掌握单表增删改查后,多表关联查询便成为核心重点——无论是毕业设计还是企业实际项目,多表查询都是高频需求,尤其一对多、多对一的关联场景,几乎贯穿所有业务模块(如用户与订单、商品与订单、部门与员工等)。本文将以最典型的「用户-订单」场景为例,完整实战一对多、多对一的实体设计、MyBatis-Plus两种核心查询方式,以及分页+多条件查询的落地实现,兼顾理论与实操,直接适配毕设和项目开发。
一、前置准备:环境搭建与核心场景说明
1.1 环境依赖
确保项目中引入以下核心依赖(Maven示例),重点是Spring Boot Starter、MyBatis-Plus、MySQL驱动,分页插件需额外引入MyBatis-Plus分页依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis-Plus 核心依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MyBatis-Plus 分页插件 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-extension</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- lombok 简化实体类 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
1.2 核心场景定义
本文以「用户-订单」为实战场景,明确一对多、多对一关系:
-
多对一:订单 → 用户。多个订单可以属于同一个用户,订单是“多”的一方,用户是“一”的一方,订单表中需存在外键(user_id)关联用户表的主键(id)。
-
一对多:用户 → 订单。一个用户可以拥有多个订单,用户是“一”的一方,订单是“多”的一方,用户表中无需额外添加字段,通过订单表的外键关联。
数据库表设计(MySQL):先创建用户表(user),再创建订单表(order),订单表通过user_id关联用户表,注意订单表名需规避MySQL关键字,此处用t_order命名。
-- 用户表(一的一方)
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户主键',
`username` varchar(50) NOT NULL COMMENT '用户名',
`phone` varchar(11) DEFAULT NULL COMMENT '手机号',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
-- 订单表(多的一方),外键关联用户表
CREATE TABLE `t_order` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '订单主键',
`order_no` varchar(32) NOT NULL COMMENT '订单编号',
`total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额',
`user_id` bigint(20) NOT NULL COMMENT '关联用户主键(外键)',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
-- 外键约束:关联用户表的id
CONSTRAINT `fk_order_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
说明:外键约束中ON DELETE CASCADE表示删除用户时,自动删除该用户关联的所有订单,根据业务需求可调整为ON DELETE SET NULL(需user_id允许为null)。
二、核心实战一:一对多 / 多对一实体设计
实体设计是多表查询的基础,核心是通过注解(@TableName、@TableField、@TableId)映射数据库表,通过实体属性体现关联关系,无需手动编写映射文件(MyBatis-Plus自动适配)。
2.1 多对一实体设计(订单 → 用户)
订单(TOrder)是多的一方,关联用户(User),在订单实体中添加User类型的属性(user),用于存储关联的用户信息,通过@TableField(exist = false)说明该属性不对应数据库表字段(仅用于关联查询)。
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 订单实体(多的一方,多对一:订单→用户)
*/
@Data
@TableName("t_order") // 映射数据库表t_order
public class TOrder {
// 订单主键,自增
@TableId(type = IdType.AUTO)
private Long id;
// 订单编号
private String orderNo;
// 订单总金额
private BigDecimal totalAmount;
// 关联用户主键(外键,对应数据库user_id字段)
private Long userId;
// 关联的用户信息(多对一核心:订单关联一个用户)
// exist = false:表示该属性不是数据库表的字段,仅用于接收关联查询结果
@TableField(exist = false)
private User user;
// 创建时间,自动填充
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
// 自动填充配置(可选,简化代码)
@Version
private Integer version; // 乐观锁(可选)
}
/**
* 用户实体(一的一方)
*/
@Data
@TableName("user") // 映射数据库表user
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String phone;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
}
2.2 一对多实体设计(用户 → 订单)
用户(User)是一的一方,关联多个订单(TOrder),在用户实体中添加List类型的属性(orderList),用于存储该用户的所有订单,同样通过@TableField(exist = false)标识非数据库字段。
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
/**
* 用户实体(一的一方,一对多:用户→订单)
*/
@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String phone;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
// 关联的订单列表(一对多核心:一个用户拥有多个订单)
// exist = false:非数据库字段,用于接收关联查询结果
@TableField(exist = false)
private List<TOrder> orderList;
}
关键注意点:关联属性(user、orderList)必须添加@TableField(exist = false),否则MyBatis-Plus会将其当作数据库表字段处理,导致查询报错;实体类属性名与数据库字段名不一致时,可通过@TableField(“数据库字段名”)手动映射(如userId对应数据库user_id,MyBatis-Plus默认支持驼峰命名,可省略该注解)。
三、核心实战二:MyBatis-Plus 多表查询两种方式
MyBatis-Plus实现多表查询主要有两种方式:① 基于@TableName关联(通过resultMap映射,无需编写SQL);② 自定义SQL(灵活适配复杂场景),两种方式按需选择,优先使用第一种(简化代码),复杂场景用第二种。
3.1 方式一:@TableName 关联(resultMap 映射)
核心原理:通过MyBatis的resultMap标签,手动定义实体与数据库表的关联关系,映射一对多、多对一的关联属性,无需编写完整SQL,仅需调用MyBatis-Plus的基础查询方法(如selectById、selectList)即可实现关联查询。
需创建Mapper接口(继承BaseMapper),并在对应的XML映射文件中定义resultMap。
3.1.1 多对一查询(订单 → 用户)
需求:查询订单信息时,同时查询出该订单所属的用户信息(如查询订单编号为“20260306001”的订单,同时返回用户名、手机号)。
- 创建TOrderMapper接口:
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.entity.TOrder;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TOrderMapper extends BaseMapper<TOrder> {
// 无需额外编写方法,仅需在XML中定义resultMap
}
- 创建TOrderMapper.xml映射文件(resources/mybatis/mapper目录下),定义resultMap关联用户:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.TOrderMapper">
<!-- 多对一:订单→用户,定义resultMap -->
<resultMap id="orderUserMap" type="com.example.demo.entity.TOrder"><!-- 订单表自身字段映射 -->
<id column="id" property="id"/>
<result column="order_no" property="orderNo"/>
<result column="total_amount" property="totalAmount"/>
<result column="user_id" property="userId"/>
<result column="create_time" property="createTime"/>
<!-- 关联用户表(多对一:association标签) -->
<!-- property:订单实体中关联用户的属性名(user) -->
<!-- javaType:关联的实体类(User) -->
<association property="user" javaType="com.example.demo.entity.User">
<id column="u_id" property="id"/> <!-- u_id是关联查询时用户表id的别名,避免字段冲突 -->
<result column="username" property="username"/>
<result column="phone" property="phone"/>
<result column="u_create_time" property="createTime"/>
</association>
</resultMap>
<!-- 自定义查询方法,关联用户表,使用上面定义的resultMap -->
<select id="selectOrderWithUser" resultMap="orderUserMap">
SELECT
o.*,
u.id u_id,
u.username,
u.phone,
u.create_time u_create_time
FROM t_order o
LEFT JOIN user u ON o.user_id = u.id
<!-- 可添加条件,如根据订单编号查询 -->
WHERE o.order_no = #{orderNo}
</select>
</mapper>
- 测试多对一查询(Service层示例):
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.entity.TOrder;
import com.example.demo.mapper.TOrderMapper;
import org.springframework.stereotype.Service;
@Service
public class TOrderService extends ServiceImpl<TOrderMapper, TOrder> {
// 查询订单并关联用户
public TOrder getOrderWithUser(String orderNo) {
return baseMapper.selectOrderWithUser(orderNo);
}
}
// 控制器测试(Controller)
@RestController
@RequestMapping("/order")
public class TOrderController {
@Autowired
private TOrderService orderService;
@GetMapping("/{orderNo}")
public Result<TOrder> getOrderWithUser(@PathVariable String orderNo) {
TOrder order = orderService.getOrderWithUser(orderNo);
return Result.success(order);
}
}
测试结果:返回订单信息的同时,包含该订单所属用户的username、phone等信息,实现多对一关联查询。
3.1.2 一对多查询(用户 → 订单)
需求:查询用户信息时,同时查询出该用户的所有订单信息(如查询id为1的用户,同时返回该用户的所有订单列表)。
- 创建UserMapper接口:
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
// 无需额外编写方法,XML中定义resultMap
}
- 创建UserMapper.xml映射文件,定义resultMap关联订单列表:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper">
<!-- 一对多:用户→订单,定义resultMap -->
<resultMap id="userOrderMap" type="com.example.demo.entity.User">
<!-- 用户表自身字段映射 -->
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="phone" property="phone"/>
<result column="create_time" property="createTime"/>
<!-- 关联订单表(一对多:collection标签) -->
<!-- property:用户实体中关联订单列表的属性名(orderList) -->
<!-- ofType:订单列表中元素的类型(TOrder) -->
<collection property="orderList" ofType="com.example.demo.entity.TOrder">
<id column="o_id" property="id"/> <!-- o_id是订单表id的别名,避免冲突 -->
<result column="order_no" property="orderNo"/>
<result column="total_amount" property="totalAmount"/>
<result column="user_id" property="userId"/>
<result column="o_create_time" property="createTime"/>
</collection>
</resultMap>
<!-- 自定义查询方法,关联订单表 -->
<select id="selectUserWithOrder" resultMap="userOrderMap">
SELECT
u.*,
o.id o_id,
o.order_no,
o.total_amount,
o.user_id,
o.create_time o_create_time
FROM user u
LEFT JOIN t_order o ON u.id = o.user_id
<!-- 条件:根据用户id查询 -->
WHERE u.id = #{userId}
</select>
</mapper>
- 测试一对多查询(Service层示例):
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import org.springframework.stereotype.Service;
@Service
public class UserService extends ServiceImpl<UserMapper, User> {
// 查询用户并关联其所有订单
public User getUserWithOrder(Long userId) {
return baseMapper.selectUserWithOrder(userId);
}
}
// 控制器测试
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{userId}")
public Result<User> getUserWithOrder(@PathVariable Long userId) {
User user = userService.getUserWithOrder(userId);
return Result.success(user);
}
}
测试结果:返回用户信息的同时,包含该用户的所有订单列表,实现一对多关联查询。
3.2 方式二:自定义 SQL(灵活适配复杂场景)
当关联关系复杂(如多表联查、多条件筛选)时,@TableName关联方式不够灵活,此时可通过MyBatis-Plus的@Select注解直接编写自定义SQL,或在XML中编写复杂SQL,两种形式均可,以下以@Select注解为例(简化代码)。
3.2.1 多条件多表查询(示例)
需求:查询指定用户(username包含“张”)的所有订单,且订单总金额大于100元,返回订单编号、总金额、用户名、手机号。
// 在TOrderMapper接口中添加自定义SQL方法
@Mapper
public interface TOrderMapper extends BaseMapper<TOrder> {
// 自定义SQL,多表联查+多条件
@Select("SELECT o.order_no, o.total_amount, u.username, u.phone " +
"FROM t_order o " +
"LEFT JOIN user u ON o.user_id = u.id " +
"WHERE u.username LIKE CONCAT('%', #{username}, '%') " +
"AND o.total_amount > #{minAmount}")
List<Map<String, Object>> selectOrderByCondition(
@Param("username") String username,
@Param("minAmount") BigDecimal minAmount
);
}
测试:调用该方法,传入username=“张”、minAmount=100,即可返回满足条件的订单及关联用户信息,适合不需要映射完整实体、仅需部分字段的场景。
3.2.2 自定义 SQL 注意点
-
多表联查时,需给表名、字段名添加别名,避免字段冲突(如用户表和订单表都有create_time,需分别别名u_create_time、o_create_time)。
-
参数传递使用@Param注解,明确参数名,避免SQL中参数无法识别。
-
返回结果可选择Map<String, Object>(灵活接收部分字段),或自定义DTO实体(接收指定字段,更规范)。
四、核心实战三:分页 + 多条件查询实战
在实际项目中,多表查询往往需要结合分页(避免数据量过大)和多条件筛选(满足业务查询需求),MyBatis-Plus的分页插件可直接适配多表查询,以下是完整实战案例。
4.1 分页插件配置
首先配置MyBatis-Plus分页插件,全局生效,无需重复配置:
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyBatisPlusConfig {
// 分页插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加MySQL分页拦截器(根据数据库类型选择)
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
4.2 分页 + 多条件多表查询实现
需求:分页查询订单列表,支持多条件筛选(用户名模糊查询、订单金额范围查询、创建时间范围查询),同时关联查询订单所属用户信息,返回分页结果(总条数、总页数、当前页数据)。
4.2.1 定义查询条件DTO(接收前端参数)
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 订单分页查询条件DTO
*/
@Data
public class OrderQueryDTO {
// 分页参数(当前页、每页条数)
private Integer current = 1; // 默认第1页
private Integer size = 10; // 默认每页10条
// 多条件筛选参数
private String username; // 用户名模糊查询
private BigDecimal minAmount; // 最小订单金额
private BigDecimal maxAmount; // 最大订单金额
private LocalDateTime startCreateTime; // 开始创建时间
private LocalDateTime endCreateTime; // 结束创建时间
// 转换为MyBatis-Plus分页对象
public IPage<TOrder> toPage() {
return new Page<>(current, size);
}
}
4.2.2 Mapper接口编写分页查询方法
@Mapper
public interface TOrderMapper extends BaseMapper<TOrder> {
// 分页+多条件+多表关联查询,使用XML编写SQL(更清晰)
IPage<TOrder> selectOrderPage(
Page<TOrder> page, // 分页参数,MyBatis-Plus自动识别
@Param("dto") OrderQueryDTO dto // 查询条件
);
}
4.2.3 XML中编写分页关联SQL
<!-- 在TOrderMapper.xml中添加分页查询SQL -->
<select id="selectOrderPage" resultMap="orderUserMap">
SELECT
o.*,
u.id u_id,
u.username,
u.phone,
u.create_time u_create_time
FROM t_order o
LEFT JOIN user u ON o.user_id = u.id
<where>
<!-- 多条件筛选,动态拼接SQL -->
<if test="dto.username != null and dto.username != ''">
AND u.username LIKE CONCAT('%', #{dto.username}, '%')
</if>
<if test="dto.minAmount != null">
AND o.total_amount >= #{dto.minAmount}
</if>
<if test="dto.maxAmount != null">
AND o.total_amount <= #{dto.maxAmount}
</if>
<if test="dto.startCreateTime != null">
AND o.create_time >= #{dto.startCreateTime}
</if>
<if test="dto.endCreateTime != null">
AND o.create_time <= #{dto.endCreateTime}
</if>
</where>
<!-- 排序:按订单创建时间倒序 -->
ORDER BY o.create_time DESC
</select>
4.2.4 Service层与Controller层实现
// Service层
@Service
public class TOrderService extends ServiceImpl<TOrderMapper, TOrder> {
// 分页查询订单(关联用户+多条件)
public IPage<TOrder> getOrderPage(OrderQueryDTO dto) {
// 转换为分页对象
Page<TOrder> page = (Page<TOrder>) dto.toPage();
// 调用Mapper方法,返回分页结果
return baseMapper.selectOrderPage(page, dto);
}
}
// Controller层
@RestController
@RequestMapping("/order")
public class TOrderController {
@Autowired
private TOrderService orderService;
// 分页查询订单
@PostMapping("/page")
public Result<IPage<TOrder>> getOrderPage(@RequestBody OrderQueryDTO dto) {
IPage<TOrder> page = orderService.getOrderPage(dto);
return Result.success(page);
}
}
4.2.5 测试结果说明
前端传入查询条件(如current=1、size=10、username=“张”、minAmount=50),后端返回分页结果,包含:
-
分页基础信息:total(总条数)、pages(总页数)、current(当前页)、size(每页条数)。
-
当前页数据:每条订单信息包含关联的用户username、phone等信息,满足多条件筛选需求。
关键注意点:分页查询时,分页参数(Page对象)必须作为Mapper方法的第一个参数,MyBatis-Plus才能自动识别并进行分页;动态条件使用标签,自动处理AND/OR拼接,避免SQL语法错误。
五、常见问题与解决方案
-
关联查询时,关联属性(user、orderList)为null?
解决方案:① 检查XML中resultMap的association/collection标签配置,确保property、javaType/ofType正确;② 检查SQL是否正确左连接(LEFT JOIN),避免内连接(INNER JOIN)导致无关联数据时返回null;③ 确认@TableField(exist = false)是否添加。 -
分页查询无数据,或分页参数不生效?
解决方案:① 检查分页插件是否配置正确,确保数据库类型(DbType)与项目使用的数据库一致;② 确认Mapper方法中分页参数(Page对象)是第一个参数;③ 检查SQL是否有语法错误,可通过打印SQL日志排查(在application.yml中配置mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl)。 -
多表联查字段冲突,导致查询结果错误?
解决方案:给冲突的字段添加别名(如用户表id别名u_id,订单表id别名o_id),并在resultMap中正确映射别名与实体属性。
六、总结
本文围绕Spring Boot + MyBatis-Plus实现一对多、多对一关联查询,从实体设计、两种核心查询方式,到分页+多条件查询实战,完整覆盖毕设和项目中最常用的多表查询场景。核心要点:
-
实体设计:通过@TableField(exist = false)定义关联属性,多对一用association标签,一对多用collection标签。
-
查询方式:简单场景用@TableName关联(resultMap映射),复杂场景用自定义SQL,按需选择。
-
分页实战:配置分页插件,结合动态SQL实现多条件分页,适配实际项目需求。
掌握本文内容后,可轻松应对毕设中的多表查询需求,同时适配企业项目中常见的关联查询场景,后续可进一步学习多对多关联查询、级联操作等进阶内容。
1784

被折叠的 条评论
为什么被折叠?



