Spring Boot 多表关联查询实战(一对多 _ 多对一)

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”的订单,同时返回用户名、手机号)。

  1. 创建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
}
  1. 创建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"&gt;
    <!-- 多对一:订单→用户,定义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"/&gt;

        <!-- 关联用户表(多对一:association标签) -->
        <!-- property:订单实体中关联用户的属性名(user) -->
        <!-- javaType:关联的实体类(User) -->
        <association property="user" javaType="com.example.demo.entity.User">
            <id column="u_id" property="id"/&gt; <!-- u_id是关联查询时用户表id的别名,避免字段冲突 -->
            <result column="username" property="username"/>
            <result column="phone" property="phone"/>
            <result column="u_create_time" property="createTime"/>
        </association&gt;
    &lt;/resultMap&gt;

    <!-- 自定义查询方法,关联用户表,使用上面定义的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>
  1. 测试多对一查询(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的用户,同时返回该用户的所有订单列表)。

  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
}
  1. 创建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"&gt;
    <!-- 一对多:用户→订单,定义resultMap -->
    <resultMap id="userOrderMap" type="com.example.demo.entity.User"&gt;
        <!-- 用户表自身字段映射 -->
        <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"/&gt; <!-- 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>
  1. 测试一对多查询(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>
    &lt;/where&gt;
    <!-- 排序:按订单创建时间倒序 -->
    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语法错误。

五、常见问题与解决方案

  1. 关联查询时,关联属性(user、orderList)为null?

       解决方案:① 检查XML中resultMap的association/collection标签配置,确保property、javaType/ofType正确;② 检查SQL是否正确左连接(LEFT JOIN),避免内连接(INNER JOIN)导致无关联数据时返回null;③ 确认@TableField(exist = false)是否添加。
    
  2. 分页查询无数据,或分页参数不生效?

       解决方案:① 检查分页插件是否配置正确,确保数据库类型(DbType)与项目使用的数据库一致;② 确认Mapper方法中分页参数(Page对象)是第一个参数;③ 检查SQL是否有语法错误,可通过打印SQL日志排查(在application.yml中配置mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl)。
    
  3. 多表联查字段冲突,导致查询结果错误?
    解决方案:给冲突的字段添加别名(如用户表id别名u_id,订单表id别名o_id),并在resultMap中正确映射别名与实体属性。

六、总结

本文围绕Spring Boot + MyBatis-Plus实现一对多、多对一关联查询,从实体设计、两种核心查询方式,到分页+多条件查询实战,完整覆盖毕设和项目中最常用的多表查询场景。核心要点:

  • 实体设计:通过@TableField(exist = false)定义关联属性,多对一用association标签,一对多用collection标签。

  • 查询方式:简单场景用@TableName关联(resultMap映射),复杂场景用自定义SQL,按需选择。

  • 分页实战:配置分页插件,结合动态SQL实现多条件分页,适配实际项目需求。

掌握本文内容后,可轻松应对毕设中的多表查询需求,同时适配企业项目中常见的关联查询场景,后续可进一步学习多对多关联查询、级联操作等进阶内容。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码客日记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值