MyBatis-Flex 多表联查完整实战指南
目录
项目概述
本教程通过完整的实战案例,详细讲解 MyBatis-Flex 框架中多表联查的各种应用场景,包括:
一对一关联查询
一对多关联查询
多对多关联查询
复杂嵌套查询
分页查询优化
环境准备
1. 开发环境要求
2. 创建数据库
-- 创建数据库
CREATE DATABASE IF NOT EXISTS mybatis_flex_demo
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE mybatis_flex_demo;数据库设计
实体关系图
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ user │ │ user_order │ │order_detail │
├─────────────┤ ├─────────────┤ ├─────────────┤
│ id │──┐ │ id │──┐ │ id │
│ username │ │ │ user_id │ │ │ order_id │
│ email │ │ │ order_no │ │ │ product_id │
│ phone │ └───>│ total_amount│ └───>│ quantity │
│ create_time │ │ status │ │ price │
└─────────────┘ │ create_time │ └─────────────┘
└─────────────┘ │
│
┌─────────────┐ │
│ product │<─────────────┘
├─────────────┤
│ id │
│ name │
│ price │
│ category_id │
└─────────────┘
│
┌─────────────┐
│ category │
├─────────────┤
│ id │
│ name │
│ description │
└─────────────┘表结构设计
1. 用户表 (user)
-- 用户表:存储用户信息
CREATE TABLE `user` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`username` VARCHAR(50) NOT NULL COMMENT '用户名',
`email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
`phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号',
`status` TINYINT DEFAULT 1 COMMENT '状态:1-启用,0-禁用',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`),
KEY `idx_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';2. 订单表 (user_order)
-- 订单表:存储订单信息,与用户表一对多关联
CREATE TABLE `user_order` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` BIGINT NOT NULL COMMENT '用户ID(外键)',
`order_no` VARCHAR(64) NOT NULL COMMENT '订单编号',
`total_amount` DECIMAL(10,2) DEFAULT 0.00 COMMENT '订单总金额',
`status` TINYINT DEFAULT 0 COMMENT '状态:0-待支付,1-已支付,2-已发货,3-已完成',
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`),
KEY `idx_user_id` (`user_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';3. 订单详情表 (order_detail)
-- 订单详情表:存储订单商品信息,与订单表一对多关联
CREATE TABLE `order_detail` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`order_id` BIGINT NOT NULL COMMENT '订单ID(外键)',
`product_id` BIGINT NOT NULL COMMENT '商品ID(外键)',
`quantity` INT DEFAULT 1 COMMENT '购买数量',
`unit_price` DECIMAL(10,2) DEFAULT 0.00 COMMENT '单价',
`total_price` DECIMAL(10,2) DEFAULT 0.00 COMMENT '总价',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_order_id` (`order_id`),
KEY `idx_product_id` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单详情表';4. 商品表 (product)
-- 商品表:存储商品信息
CREATE TABLE `product` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` VARCHAR(200) NOT NULL COMMENT '商品名称',
`description` VARCHAR(1000) DEFAULT NULL COMMENT '商品描述',
`price` DECIMAL(10,2) DEFAULT 0.00 COMMENT '商品价格',
`stock` INT DEFAULT 0 COMMENT '库存数量',
`category_id` BIGINT DEFAULT NULL COMMENT '分类ID(外键)',
`status` TINYINT DEFAULT 1 COMMENT '状态:1-上架,0-下架',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_category_id` (`category_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';5. 分类表 (category)
-- 分类表:存储商品分类信息
CREATE TABLE `category` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` VARCHAR(100) NOT NULL COMMENT '分类名称',
`description` VARCHAR(500) DEFAULT NULL COMMENT '分类描述',
`sort_order` INT DEFAULT 0 COMMENT '排序序号',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_sort_order` (`sort_order`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分类表';6. 用户角色关联表(多对多)
-- 角色表
CREATE TABLE `role` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` VARCHAR(50) NOT NULL COMMENT '角色名称',
`code` VARCHAR(50) NOT NULL COMMENT '角色编码',
`description` VARCHAR(200) DEFAULT NULL COMMENT '角色描述',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';
-- 用户角色关联表(多对多关系)
CREATE TABLE `user_role` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` BIGINT NOT NULL COMMENT '用户ID',
`role_id` BIGINT NOT NULL COMMENT '角色ID',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_role` (`user_id`, `role_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_role_id` (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';插入测试数据
-- 插入分类数据
INSERT INTO `category` (`name`, `description`, `sort_order`) VALUES
('电子产品', '手机、电脑等数码产品', 1),
('服装', '男装、女装、童装', 2),
('食品', '零食、饮料、生鲜', 3);
-- 插入商品数据
INSERT INTO `product` (`name`, `description`, `price`, `stock`, `category_id`, `status`) VALUES
('iPhone 15 Pro', '苹果最新旗舰手机', 8999.00, 100, 1, 1),
('MacBook Pro', '苹果笔记本电脑', 14999.00, 50, 1, 1),
('纯棉T恤', '舒适透气的纯棉T恤', 99.00, 500, 2, 1),
('牛仔裤', '经典款牛仔裤', 299.00, 300, 2, 1),
('坚果礼盒', '精选坚果组合', 168.00, 200, 3, 1);
-- 插入用户数据
INSERT INTO `user` (`username`, `email`, `phone`, `status`) VALUES
('zhangsan', 'zhangsan@example.com', '13800138001', 1),
('lisi', 'lisi@example.com', '13800138002', 1),
('wangwu', 'wangwu@example.com', '13800138003', 1);
-- 插入角色数据
INSERT INTO `role` (`name`, `code`, `description`) VALUES
('管理员', 'ADMIN', '系统管理员'),
('普通用户', 'USER', '普通注册用户'),
('VIP用户', 'VIP', 'VIP会员用户');
-- 插入用户角色关联
INSERT INTO `user_role` (`user_id`, `role_id`) VALUES
(1, 1), -- zhangsan是管理员
(1, 2), -- zhangsan也是普通用户
(2, 2), -- lisi是普通用户
(3, 3); -- wangwu是VIP用户
-- 插入订单数据
INSERT INTO `user_order` (`user_id`, `order_no`, `total_amount`, `status`, `remark`) VALUES
(1, 'ORDER20240310001', 9098.00, 1, '请尽快发货'),
(1, 'ORDER20240310002', 168.00, 0, NULL),
(2, 'ORDER20240310003', 398.00, 2, NULL);
-- 插入订单详情数据
INSERT INTO `order_detail` (`order_id`, `product_id`, `quantity`, `unit_price`, `total_price`) VALUES
(1, 1, 1, 8999.00, 8999.00), -- iPhone
(1, 3, 1, 99.00, 99.00), -- T恤
(2, 5, 1, 168.00, 168.00), -- 坚果
(3, 3, 2, 99.00, 198.00), -- T恤x2
(3, 4, 1, 299.00, 299.00); -- 牛仔裤项目搭建
1. 创建Maven项目
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 父工程 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>mybatis-flex-demo</artifactId>
<version>1.0.0</version>
<name>MyBatis-Flex多表联查Demo</name>
<description>MyBatis-Flex框架多表联查完整实战案例</description>
<properties>
<java.version>1.8</java.version>
<mybatis-flex.version>1.7.5</mybatis-flex.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- MyBatis-Flex 核心依赖 -->
<dependency>
<groupId>com.mybatis-flex</groupId>
<artifactId>mybatis-flex-spring-boot-starter</artifactId>
<version>${mybatis-flex.version}</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- 连接池 -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<!-- Lombok(简化代码) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>2. 配置文件
application.yml
# 服务器配置
server:
port: 8080
# Spring配置
spring:
application:
name: mybatis-flex-demo
# 数据源配置
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/mybatis_flex_demo?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
hikari:
minimum-idle: 5
maximum-pool-size: 20
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
# MyBatis-Flex配置
mybatis-flex:
# 配置Mapper XML文件位置(如果使用XML方式)
mapper-locations: classpath*:/mapper/**/*.xml
# 配置实体类包扫描路径
type-aliases-package: com.example.entity
# 驼峰命名自动映射
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 全局配置
global-config:
# 逻辑删除字段名
logic-delete-column: is_deleted
# 逻辑删除值(已删除)
logic-delete-value: 1
# 逻辑未删除值(未删除)
logic-not-delete-value: 0
# 日志配置
logging:
level:
com.example.mapper: debug3. 项目结构
mybatis-flex-demo/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── example/
│ │ │ ├── MybatisFlexDemoApplication.java # 启动类
│ │ │ ├── config/ # 配置类
│ │ │ │ └── MybatisFlexConfig.java
│ │ │ ├── entity/ # 实体类
│ │ │ │ ├── User.java
│ │ │ │ ├── Order.java
│ │ │ │ ├── OrderDetail.java
│ │ │ │ ├── Product.java
│ │ │ │ ├── Category.java
│ │ │ │ ├── Role.java
│ │ │ │ └── UserRole.java
│ │ │ ├── mapper/ # Mapper接口
│ │ │ │ ├── UserMapper.java
│ │ │ │ ├── OrderMapper.java
│ │ │ │ ├── ProductMapper.java
│ │ │ │ └── RoleMapper.java
│ │ │ ├── service/ # 业务层
│ │ │ │ ├── UserService.java
│ │ │ │ └── OrderService.java
│ │ │ └── controller/ # 控制层
│ │ │ ├── UserController.java
│ │ │ └── OrderController.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── com/
│ └── example/
│ └── MybatisFlexDemoApplicationTests.java
└── pom.xml多表联查实战案例
案例1:一对一关联查询(用户+订单)
场景:查询订单时,同时获取下单用户的信息
实体类定义
User.java - 用户实体
package com.example.entity;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.Table;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 用户实体类
* @Table注解指定表名,schema指定数据库(可选)
*/
@Data
@Table("user")
public class User {
/**
* 主键ID
* @Id 标识主键字段
* @Column 映射数据库列,可指定别名、是否忽略等
*/
@Id
private Long id;
/**
* 用户名
*/
@Column("username")
private String username;
/**
* 邮箱
*/
@Column("email")
private String email;
/**
* 手机号
*/
@Column("phone")
private String phone;
/**
* 状态
*/
@Column("status")
private Integer status;
/**
* 创建时间
* isIgnore = true 表示该字段不参与CRUD操作
*/
@Column(value = "create_time", isIgnore = true)
private LocalDateTime createTime;
/**
* 更新时间
*/
@Column(value = "update_time", isIgnore = true)
private LocalDateTime updateTime;
}Order.java - 订单实体(包含用户关联)
package com.example.entity;
import com.mybatisflex.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 订单实体类
* 演示一对一关联:一个订单对应一个用户
*/
@Data
@Table("user_order")
public class Order {
@Id
private Long id;
/**
* 用户ID(外键)
*/
@Column("user_id")
private Long userId;
/**
* 订单编号
*/
@Column("order_no")
private String orderNo;
/**
* 订单总金额
*/
@Column("total_amount")
private BigDecimal totalAmount;
/**
* 订单状态
*/
@Column("status")
private Integer status;
/**
* 备注
*/
@Column("remark")
private String remark;
/**
* 创建时间
*/
@Column(value = "create_time", isIgnore = true)
private LocalDateTime createTime;
// ==================== 关联字段 ====================
/**
* 关联用户对象(一对一)
* @RelationOneToOne 标识一对一关联
* selfField = "userId" 本表的外键字段
* targetField = "id" 关联表的主键字段
*
* 查询时会自动执行:
* SELECT * FROM user WHERE id = #{order.userId}
*/
@RelationOneToOne(selfField = "userId", targetField = "id")
private User user;
}Mapper接口
OrderMapper.java
package com.example.mapper;
import com.mybatisflex.core.BaseMapper;
import com.example.entity.Order;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 订单Mapper接口
* 继承BaseMapper获得基础CRUD方法
*/
public interface OrderMapper extends BaseMapper<Order> {
/**
* 方式1:使用注解查询订单及关联用户(一对一)
* @param orderId 订单ID
* @return 包含用户信息的订单
*
* 注意:使用@RelationOneToOne注解后,MyBatis-Flex会自动处理关联查询
* 不需要手动编写JOIN语句
*/
@Select("SELECT * FROM user_order WHERE id = #{orderId}")
Order selectOrderWithUser(Long orderId);
/**
* 方式2:查询用户的所有订单(带用户信息)
* @param userId 用户ID
* @return 订单列表
*/
@Select("SELECT * FROM user_order WHERE user_id = #{userId}")
List<Order> selectOrdersByUserId(@Param("userId") Long userId);
/**
* 方式3:使用XML方式(复杂查询推荐)
* 在resources/mapper/OrderMapper.xml中定义
*/
List<Order> selectOrderListWithUser(@Param("status") Integer status);
}XML映射文件(可选)
resources/mapper/OrderMapper.xml
<?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.mapper.OrderMapper">
<!-- 结果映射:订单+用户 -->
<resultMap id="OrderWithUserResultMap" type="com.example.entity.Order">
<id column="id" property="id"/>
<result column="user_id" property="userId"/>
<result column="order_no" property="orderNo"/>
<result column="total_amount" property="totalAmount"/>
<result column="status" property="status"/>
<result column="remark" property="remark"/>
<result column="create_time" property="createTime"/>
<!-- 一对一关联:用户 -->
<association property="user" javaType="com.example.entity.User">
<id column="user_id" property="id"/>
<result column="username" property="username"/>
<result column="email" property="email"/>
<result column="phone" property="phone"/>
</association>
</resultMap>
<!-- 查询订单列表(带用户信息) -->
<select id="selectOrderListWithUser" resultMap="OrderWithUserResultMap">
SELECT
o.*,
u.username,
u.email,
u.phone
FROM user_order o
LEFT JOIN user u ON o.user_id = u.id
<where>
<if test="status != null">
AND o.status = #{status}
</if>
</where>
ORDER BY o.create_time DESC
</select>
</mapper>测试代码
@SpringBootTest
public class OneToOneTest {
@Autowired
private OrderMapper orderMapper;
/**
* 测试1:根据ID查询订单(自动关联用户)
* 使用@RelationOneToOne注解,会自动查询关联的用户信息
*/
@Test
public void testSelectOrderWithUser() {
// 查询订单ID为1的订单
Order order = orderMapper.selectOneById(1L);
System.out.println("订单编号:" + order.getOrderNo());
System.out.println("订单金额:" + order.getTotalAmount());
// 获取关联的用户信息
if (order.getUser() != null) {
System.out.println("下单用户:" + order.getUser().getUsername());
System.out.println("用户邮箱:" + order.getUser().getEmail());
}
}
/**
* 测试2:查询所有订单(自动关联用户)
*/
@Test
public void testSelectAllOrdersWithUser() {
// 查询所有订单
List<Order> orders = orderMapper.selectAll();
for (Order order : orders) {
System.out.println("订单:" + order.getOrderNo());
if (order.getUser() != null) {
System.out.println(" 用户:" + order.getUser().getUsername());
}
System.out.println("---");
}
}
/**
* 测试3:使用XML查询(更灵活)
*/
@Test
public void testSelectOrderListWithUser() {
// 查询状态为1(已支付)的订单
List<Order> orders = orderMapper.selectOrderListWithUser(1);
orders.forEach(order -> {
System.out.println("订单:" + order.getOrderNo());
System.out.println("用户:" + order.getUser().getUsername());
System.out.println("===============");
});
}
}案例2:一对多关联查询(订单+订单详情)
场景说明
查询订单时,同时获取该订单的所有商品详情信息。一个订单包含多个商品,这是一对多关系。
实体类定义
OrderDetail.java - 订单详情实体
package com.example.entity;
import com.mybatisflex.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 订单详情实体类
* 记录订单中的每个商品信息
*/
@Data
@Table("order_detail")
public class OrderDetail {
@Id
private Long id;
/**
* 订单ID(外键,关联order表)
*/
@Column("order_id")
private Long orderId;
/**
* 商品ID(外键,关联product表)
*/
@Column("product_id")
private Long productId;
/**
* 购买数量
*/
@Column("quantity")
private Integer quantity;
/**
* 单价
*/
@Column("unit_price")
private BigDecimal unitPrice;
/**
* 总价(quantity * unit_price)
*/
@Column("total_price")
private BigDecimal totalPrice;
/**
* 创建时间
*/
@Column(value = "create_time", isIgnore = true)
private LocalDateTime createTime;
// ==================== 关联字段 ====================
/**
* 关联商品对象(一对一)
* 通过product_id关联商品表
*/
@RelationOneToOne(selfField = "productId", targetField = "id")
private Product product;
}Order.java - 订单实体(增加一对多关联)
package com.example.entity;
import com.mybatisflex.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* 订单实体类
* 演示一对多关联:一个订单包含多个订单详情
*/
@Data
@Table("user_order")
public class Order {
@Id
private Long id;
@Column("user_id")
private Long userId;
@Column("order_no")
private String orderNo;
@Column("total_amount")
private BigDecimal totalAmount;
@Column("status")
private Integer status;
@Column("remark")
private String remark;
@Column(value = "create_time", isIgnore = true)
private LocalDateTime createTime;
// ==================== 一对一关联 ====================
@RelationOneToOne(selfField = "userId", targetField = "id")
private User user;
// ==================== 一对多关联 ====================
/**
* 订单详情列表(一对多)
* @RelationOneToMany 标识一对多关联
* selfField = "id" 本表的主键字段
* targetField = "orderId" 关联表的外键字段
*
* 查询时会自动执行:
* SELECT * FROM order_detail WHERE order_id = #{order.id}
*/
@RelationOneToMany(selfField = "id", targetField = "orderId")
private List<OrderDetail> orderDetails;
}Mapper接口
OrderMapper.java(增加一对多查询方法)
package com.example.mapper;
import com.mybatisflex.core.BaseMapper;
import com.mybatisflex.core.query.QueryWrapper;
import com.example.entity.Order;
import com.example.entity.table.OrderTableDef;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
import static com.example.entity.table.OrderTableDef.ORDER;
/**
* 订单Mapper接口
*/
public interface OrderMapper extends BaseMapper<Order> {
/**
* 方式1:使用注解查询订单及关联详情(一对多)
* MyBatis-Flex会自动处理@RelationOneToMany注解
*/
@Select("SELECT * FROM user_order WHERE id = #{orderId}")
Order selectOrderWithDetails(Long orderId);
/**
* 方式2:使用QueryWrapper链式查询(推荐)
* 不需要手写SQL,类型安全
*/
default List<Order> selectOrdersWithDetailsByUserId(Long userId) {
QueryWrapper wrapper = QueryWrapper.create()
.from(ORDER)
.where(ORDER.USER_ID.eq(userId))
.orderBy(ORDER.CREATE_TIME.desc());
return this.selectListWithRelations(wrapper);
}
/**
* 方式3:查询订单及所有关联(用户+详情+商品)
*/
default Order selectOrderWithAllRelations(Long orderId) {
return this.selectOneWithRelationsById(orderId);
}
}XML映射文件
OrderMapper.xml(一对多结果映射)
<?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.mapper.OrderMapper">
<!-- 结果映射:订单+用户+订单详情 -->
<resultMap id="OrderWithDetailsResultMap" type="com.example.entity.Order">
<id column="id" property="id"/>
<result column="user_id" property="userId"/>
<result column="order_no" property="orderNo"/>
<result column="total_amount" property="totalAmount"/>
<result column="status" property="status"/>
<result column="remark" property="remark"/>
<result column="create_time" property="createTime"/>
<!-- 一对一关联:用户 -->
<association property="user" javaType="com.example.entity.User">
<id column="user_id" property="id"/>
<result column="username" property="username"/>
<result column="email" property="email"/>
<result column="phone" property="phone"/>
</association>
<!-- 一对多关联:订单详情 -->
<collection property="orderDetails" ofType="com.example.entity.OrderDetail">
<id column="detail_id" property="id"/>
<result column="detail_order_id" property="orderId"/>
<result column="product_id" property="productId"/>
<result column="quantity" property="quantity"/>
<result column="unit_price" property="unitPrice"/>
<result column="total_price" property="totalPrice"/>
<!-- 嵌套关联:商品信息 -->
<association property="product" javaType="com.example.entity.Product">
<id column="product_id" property="id"/>
<result column="product_name" property="name"/>
<result column="product_price" property="price"/>
</association>
</collection>
</resultMap>
<!-- 查询订单及所有关联信息 -->
<select id="selectOrderWithAllInfo" resultMap="OrderWithDetailsResultMap">
SELECT
o.*,
u.username,
u.email,
u.phone,
od.id as detail_id,
od.order_id as detail_order_id,
od.product_id,
od.quantity,
od.unit_price,
od.total_price,
p.name as product_name,
p.price as product_price
FROM user_order o
LEFT JOIN user u ON o.user_id = u.id
LEFT JOIN order_detail od ON o.id = od.order_id
LEFT JOIN product p ON od.product_id = p.id
WHERE o.id = #{orderId}
</select>
</mapper>测试代码
@SpringBootTest
public class OneToManyTest {
@Autowired
private OrderMapper orderMapper;
/**
* 测试1:查询订单及详情(一对多)
* 使用@RelationOneToMany注解自动关联
*/
@Test
public void testSelectOrderWithDetails() {
// 查询订单ID为1的订单,会自动关联订单详情
Order order = orderMapper.selectOneWithRelationsById(1L);
System.out.println("订单编号:" + order.getOrderNo());
System.out.println("订单金额:" + order.getTotalAmount());
// 获取订单详情列表
List<OrderDetail> details = order.getOrderDetails();
System.out.println("商品数量:" + details.size());
for (OrderDetail detail : details) {
System.out.println(" 商品ID:" + detail.getProductId());
System.out.println(" 数量:" + detail.getQuantity());
System.out.println(" 单价:" + detail.getUnitPrice());
// 获取关联的商品信息
if (detail.getProduct() != null) {
System.out.println(" 商品名称:" + detail.getProduct().getName());
}
System.out.println(" ---");
}
}
/**
* 测试2:查询用户的所有订单及详情
*/
@Test
public void testSelectUserOrdersWithDetails() {
// 查询用户ID为1的所有订单
List<Order> orders = orderMapper.selectOrdersWithDetailsByUserId(1L);
for (Order order : orders) {
System.out.println("订单:" + order.getOrderNo());
System.out.println("用户:" + order.getUser().getUsername());
// 遍历订单详情
List<OrderDetail> details = order.getOrderDetails();
System.out.println("包含商品:" + details.size() + " 种");
for (OrderDetail detail : details) {
System.out.println(" - " + detail.getProduct().getName()
+ " x " + detail.getQuantity());
}
System.out.println("===============");
}
}
/**
* 测试3:使用XML查询(更复杂的嵌套关联)
*/
@Test
public void testSelectOrderWithAllInfo() {
Order order = orderMapper.selectOrderWithAllInfo(1L);
System.out.println("订单信息:" + order.getOrderNo());
System.out.println("用户信息:" + order.getUser().getUsername());
System.out.println("\n商品清单:");
order.getOrderDetails().forEach(detail -> {
System.out.println("- " + detail.getProduct().getName()
+ " 数量:" + detail.getQuantity()
+ " 单价:" + detail.getUnitPrice());
});
}
}执行结果示例
订单编号:ORDER20240310001
订单金额:9098.00
商品数量:2
商品ID:1
数量:1
单价:8999.00
商品名称:iPhone 15 Pro
---
商品ID:3
数量:1
单价:99.00
商品名称:纯棉T恤
---关键点总结
@RelationOneToMany注解:用于标识一对多关联关系
selfField:本表用于关联的字段(通常是主键)targetField:关联表的外键字段
自动查询原理:
先查询主表(user_order)
根据主表结果中的id,自动查询从表(order_detail)
通过order_id关联,将结果封装到List中
嵌套关联:
Order → OrderDetail(一对多)
OrderDetail → Product(一对一)
可以无限层级嵌套,但要注意性能
性能优化建议:
对于大数据量,建议使用分页查询
避免过深的嵌套层级(建议不超过3层)
可以使用@RelationManyToMany替代多次一对多查询
案例3:多对多关联查询(用户+角色)
场景说明
一个用户可以有多个角色,一个角色也可以分配给多个用户,这是典型的多对多关系。需要通过中间表(user_role)来维护关联关系。
数据库设计
-- 用户表(已创建)
-- 角色表(已创建)
-- 用户角色关联表(已创建)实体类定义
Role.java - 角色实体
package com.example.entity;
import com.mybatisflex.annotation.*;
import lombok.Data;
/**
* 角色实体类
*/
@Data
@Table("role")
public class Role {
@Id
private Long id;
/**
* 角色名称
*/
@Column("name")
private String name;
/**
* 角色编码(唯一)
*/
@Column("code")
private String code;
/**
* 角色描述
*/
@Column("description")
private String description;
}UserRole.java - 用户角色关联实体(中间表)
package com.example.entity;
import com.mybatisflex.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 用户角色关联实体类
* 多对多关系的中间表
*/
@Data
@Table("user_role")
public class UserRole {
@Id
private Long id;
/**
* 用户ID(外键)
*/
@Column("user_id")
private Long userId;
/**
* 角色ID(外键)
*/
@Column("role_id")
private Long roleId;
/**
* 创建时间
*/
@Column(value = "create_time", isIgnore = true)
private LocalDateTime createTime;
// ==================== 关联字段 ====================
/**
* 关联用户(一对一)
*/
@RelationOneToOne(selfField = "userId", targetField = "id")
private User user;
/**
* 关联角色(一对一)
*/
@RelationOneToOne(selfField = "roleId", targetField = "id")
private Role role;
}User.java - 用户实体(增加多对多关联)
package com.example.entity;
import com.mybatisflex.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
/**
* 用户实体类(增加多对多关联)
*/
@Data
@Table("user")
public class User {
@Id
private Long id;
@Column("username")
private String username;
@Column("email")
private String email;
@Column("phone")
private String phone;
@Column("status")
private Integer status;
@Column(value = "create_time", isIgnore = true)
private LocalDateTime createTime;
@Column(value = "update_time", isIgnore = true)
private LocalDateTime updateTime;
// ==================== 多对多关联 ====================
/**
* 用户角色列表(多对多)
* @RelationManyToMany 标识多对多关联
*
* selfField = "id" 本表主键
* targetField = "id" 目标表主键
* joinTable = "user_role" 中间表名称
* joinSelfColumn = "user_id" 中间表中关联本表的字段
* joinTargetColumn = "role_id" 中间表中关联目标表的字段
*
* 查询时会自动执行:
* SELECT r.* FROM role r
* INNER JOIN user_role ur ON r.id = ur.role_id
* WHERE ur.user_id = #{user.id}
*/
@RelationManyToMany(
selfField = "id",
targetField = "id",
joinTable = "user_role",
joinSelfColumn = "user_id",
joinTargetColumn = "role_id"
)
private List<Role> roles;
}Role.java - 角色实体(双向关联)
package com.example.entity;
import com.mybatisflex.annotation.*;
import lombok.Data;
import java.util.List;
/**
* 角色实体类(增加多对多关联)
*/
@Data
@Table("role")
public class Role {
@Id
private Long id;
@Column("name")
private String name;
@Column("code")
private String code;
@Column("description")
private String description;
// ==================== 多对多关联(双向) ====================
/**
* 拥有该角色的用户列表(多对多)
* 与User实体中的roles形成双向关联
*/
@RelationManyToMany(
selfField = "id",
targetField = "id",
joinTable = "user_role",
joinSelfColumn = "role_id",
joinTargetColumn = "user_id"
)
private List<User> users;
}Mapper接口
UserMapper.java
package com.example.mapper;
import com.mybatisflex.core.BaseMapper;
import com.mybatisflex.core.query.QueryWrapper;
import com.example.entity.User;
import com.example.entity.table.UserTableDef;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
import static com.example.entity.table.UserTableDef.USER;
/**
* 用户Mapper接口
*/
public interface UserMapper extends BaseMapper<User> {
/**
* 方式1:根据ID查询用户及角色(多对多)
* 使用@RelationManyToMany注解自动关联
*/
default User selectUserWithRoles(Long userId) {
return this.selectOneWithRelationsById(userId);
}
/**
* 方式2:查询拥有指定角色的所有用户
*/
@Select("SELECT u.* FROM user u " +
"INNER JOIN user_role ur ON u.id = ur.user_id " +
"WHERE ur.role_id = #{roleId}")
List<User> selectUsersByRoleId(@Param("roleId") Long roleId);
/**
* 方式3:使用QueryWrapper查询用户及角色
*/
default List<User> selectAllUsersWithRoles() {
QueryWrapper wrapper = QueryWrapper.create()
.from(USER)
.where(USER.STATUS.eq(1))
.orderBy(USER.CREATE_TIME.desc());
return this.selectListWithRelations(wrapper);
}
/**
* 方式4:根据角色编码查询用户
*/
List<User> selectUsersByRoleCode(@Param("roleCode") String roleCode);
}RoleMapper.java
package com.example.mapper;
import com.mybatisflex.core.BaseMapper;
import com.example.entity.Role;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 角色Mapper接口
*/
public interface RoleMapper extends BaseMapper<Role> {
/**
* 查询角色及关联的用户(多对多)
*/
default Role selectRoleWithUsers(Long roleId) {
return this.selectOneWithRelationsById(roleId);
}
/**
* 查询用户的所有角色
*/
@Select("SELECT r.* FROM role r " +
"INNER JOIN user_role ur ON r.id = ur.role_id " +
"WHERE ur.user_id = #{userId}")
List<Role> selectRolesByUserId(@Param("userId") Long userId);
}XML映射文件
UserMapper.xml
<?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.mapper.UserMapper">
<!-- 结果映射:用户+角色列表 -->
<resultMap id="UserWithRolesResultMap" type="com.example.entity.User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="email" property="email"/>
<result column="phone" property="phone"/>
<result column="status" property="status"/>
<!-- 多对多关联:角色列表 -->
<collection property="roles" ofType="com.example.entity.Role">
<id column="role_id" property="id"/>
<result column="role_name" property="name"/>
<result column="role_code" property="code"/>
<result column="role_desc" property="description"/>
</collection>
</resultMap>
<!-- 根据角色编码查询用户 -->
<select id="selectUsersByRoleCode" resultMap="UserWithRolesResultMap">
SELECT
u.*,
r.id as role_id,
r.name as role_name,
r.code as role_code,
r.description as role_desc
FROM user u
INNER JOIN user_role ur ON u.id = ur.user_id
INNER JOIN role r ON ur.role_id = r.id
WHERE r.code = #{roleCode}
</select>
</mapper>测试代码
@SpringBootTest
public class ManyToManyTest {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleMapper roleMapper;
/**
* 测试1:查询用户及角色(多对多)
* 使用@RelationManyToMany注解自动关联
*/
@Test
public void testSelectUserWithRoles() {
// 查询用户ID为1的用户,会自动关联角色
User user = userMapper.selectUserWithRoles(1L);
System.out.println("用户名:" + user.getUsername());
System.out.println("邮箱:" + user.getEmail());
// 获取角色列表
List<Role> roles = user.getRoles();
System.out.println("拥有角色数:" + roles.size());
System.out.println("角色列表:");
for (Role role : roles) {
System.out.println(" - " + role.getName() + " (" + role.getCode() + ")");
System.out.println(" " + role.getDescription());
}
}
/**
* 测试2:查询角色及用户(反向多对多)
*/
@Test
public void testSelectRoleWithUsers() {
// 查询角色ID为2(普通用户)的角色
Role role = roleMapper.selectRoleWithUsers(2L);
System.out.println("角色名称:" + role.getName());
System.out.println("角色编码:" + role.getCode());
// 获取拥有该角色的用户
List<User> users = role.getUsers();
System.out.println("拥有该角色的用户数:" + users.size());
System.out.println("用户列表:");
for (User user : users) {
System.out.println(" - " + user.getUsername() + " (" + user.getEmail() + ")");
}
}
/**
* 测试3:查询所有用户及角色
*/
@Test
public void testSelectAllUsersWithRoles() {
List<User> users = userMapper.selectAllUsersWithRoles();
for (User user : users) {
System.out.println("用户:" + user.getUsername());
List<Role> roles = user.getRoles();
if (roles != null && !roles.isEmpty()) {
System.out.print("角色:");
for (Role role : roles) {
System.out.print(role.getName() + " ");
}
System.out.println();
}
System.out.println("---");
}
}
/**
* 测试4:根据角色编码查询用户
*/
@Test
public void testSelectUsersByRoleCode() {
// 查询所有管理员
List<User> adminUsers = userMapper.selectUsersByRoleCode("ADMIN");
System.out.println("管理员列表:");
for (User user : adminUsers) {
System.out.println("- " + user.getUsername());
}
// 查询所有VIP用户
List<User> vipUsers = userMapper.selectUsersByRoleCode("VIP");
System.out.println("\nVIP用户列表:");
for (User user : vipUsers) {
System.out.println("- " + user.getUsername());
}
}
/**
* 测试5:使用原生SQL查询
*/
@Test
public void testSelectUsersByRoleId() {
// 查询拥有角色ID为2的所有用户
List<User> users = userMapper.selectUsersByRoleId(2L);
System.out.println("普通用户列表:");
users.forEach(user -> {
System.out.println("- " + user.getUsername());
});
}
}执行结果示例
用户名:zhangsan
邮箱:zhangsan@example.com
拥有角色数:2
角色列表:
- 管理员 (ADMIN)
系统管理员
- 普通用户 (USER)
普通注册用户关键点总结
@RelationManyToMany注解:用于标识多对多关联关系
selfField:本表主键字段targetField:目标表主键字段joinTable:中间表名称joinSelfColumn:中间表中关联本表的字段joinTargetColumn:中间表中关联目标表的字段
中间表设计:
必须包含两个外键字段,分别关联两张主表
建议添加唯一约束(user_id + role_id),防止重复关联
可以添加额外字段(如create_time)记录关联时间
双向关联:
可以在两个实体中都配置@RelationManyToMany
形成双向导航,方便从不同角度查询
性能考虑:
多对多查询会产生JOIN操作,注意索引优化
大数据量时建议使用分页
避免循环查询(N+1问题),MyBatis-Flex会自动优化
实际应用场景:
用户-角色-权限系统
商品-标签系统
文章-分类系统
学生-课程系统
案例4:复杂嵌套查询与动态SQL
场景说明
实际业务中经常需要复杂的查询条件,如多表联查+动态条件+分页+排序。本案例演示如何使用MyBatis-Flex的QueryWrapper构建复杂查询。
需求场景
订单查询系统:
支持按用户名、订单号、订单状态、金额范围查询
支持按时间范围查询
需要关联用户信息和订单详情
支持分页和排序
统计功能(订单总数、总金额)
DTO设计
OrderQueryDTO.java - 查询条件DTO
package com.example.dto;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 订单查询条件DTO
* 用于接收前端查询参数
*/
@Data
public class OrderQueryDTO {
/**
* 用户名(模糊查询)
*/
private String username;
/**
* 订单号(模糊查询)
*/
private String orderNo;
/**
* 订单状态
*/
private Integer status;
/**
* 最小金额
*/
private BigDecimal minAmount;
/**
* 最大金额
*/
private BigDecimal maxAmount;
/**
* 开始时间
*/
private LocalDateTime startTime;
/**
* 结束时间
*/
private LocalDateTime endTime;
/**
* 用户ID
*/
private Long userId;
/**
* 商品名称(关联查询)
*/
private String productName;
}OrderVO.java - 订单视图对象
package com.example.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* 订单视图对象
* 用于返回给前端的数据结构
*/
@Data
public class OrderVO {
/**
* 订单ID
*/
private Long id;
/**
* 订单编号
*/
private String orderNo;
/**
* 订单金额
*/
private BigDecimal totalAmount;
/**
* 订单状态(中文)
*/
private String statusText;
/**
* 订单状态(数字)
*/
private Integer status;
/**
* 备注
*/
private String remark;
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
/**
* 用户信息
*/
private UserInfoVO user;
/**
* 订单详情列表
*/
private List<OrderDetailVO> details;
/**
* 商品总数
*/
private Integer totalQuantity;
// ==================== 内部类 ====================
/**
* 用户信息VO
*/
@Data
public static class UserInfoVO {
private Long id;
private String username;
private String email;
private String phone;
}
/**
* 订单详情VO
*/
@Data
public static class OrderDetailVO {
private Long id;
private String productName;
private Integer quantity;
private BigDecimal unitPrice;
private BigDecimal totalPrice;
}
}Mapper接口
OrderMapper.java(复杂查询方法)
package com.example.mapper;
import com.mybatisflex.core.BaseMapper;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.core.query.RawQuery;
import com.example.dto.OrderQueryDTO;
import com.example.entity.Order;
import com.example.entity.table.OrderTableDef;
import com.example.entity.table.UserTableDef;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.math.BigDecimal;
import java.util.List;
import static com.example.entity.table.OrderTableDef.ORDER;
import static com.example.entity.table.UserTableDef.USER;
import static com.example.entity.table.OrderDetailTableDef.ORDER_DETAIL;
/**
* 订单Mapper接口(复杂查询)
*/
public interface OrderMapper extends BaseMapper<Order> {
/**
* 方式1:使用QueryWrapper构建动态查询(推荐)
*
* 优点:
* 1. 类型安全,编译期检查
* 2. 代码优雅,避免字符串拼接
* 3. 自动处理空值判断
*
* @param queryDTO 查询条件
* @return 订单列表
*/
default List<Order> selectOrderListByQuery(OrderQueryDTO queryDTO) {
QueryWrapper wrapper = QueryWrapper.create()
// 主表
.from(ORDER)
// 左连接用户表
.leftJoin(USER).on(ORDER.USER_ID.eq(USER.ID));
// 动态条件:用户名模糊查询
if (queryDTO.getUsername() != null && !queryDTO.getUsername().isEmpty()) {
wrapper.where(USER.USERNAME.like("%" + queryDTO.getUsername() + "%"));
}
// 动态条件:订单号模糊查询
if (queryDTO.getOrderNo() != null && !queryDTO.getOrderNo().isEmpty()) {
wrapper.where(ORDER.ORDER_NO.like("%" + queryDTO.getOrderNo() + "%"));
}
// 动态条件:订单状态
if (queryDTO.getStatus() != null) {
wrapper.where(ORDER.STATUS.eq(queryDTO.getStatus()));
}
// 动态条件:金额范围
if (queryDTO.getMinAmount() != null) {
wrapper.where(ORDER.TOTAL_AMOUNT.ge(queryDTO.getMinAmount()));
}
if (queryDTO.getMaxAmount() != null) {
wrapper.where(ORDER.TOTAL_AMOUNT.le(queryDTO.getMaxAmount()));
}
// 动态条件:时间范围
if (queryDTO.getStartTime() != null) {
wrapper.where(ORDER.CREATE_TIME.ge(queryDTO.getStartTime()));
}
if (queryDTO.getEndTime() != null) {
wrapper.where(ORDER.CREATE_TIME.le(queryDTO.getEndTime()));
}
// 动态条件:用户ID
if (queryDTO.getUserId() != null) {
wrapper.where(ORDER.USER_ID.eq(queryDTO.getUserId()));
}
// 排序:按创建时间倒序
wrapper.orderBy(ORDER.CREATE_TIME.desc());
// 执行查询(带关联)
return this.selectListWithRelations(wrapper);
}
/**
* 方式2:分页查询
*
* @param page 分页参数(页码、每页大小)
* @param queryDTO 查询条件
* @return 分页结果
*/
default Page<Order> selectOrderPage(Page<Order> page, OrderQueryDTO queryDTO) {
QueryWrapper wrapper = buildQueryWrapper(queryDTO);
return this.paginateWithRelations(page, wrapper);
}
/**
* 构建QueryWrapper(提取公共方法)
*/
private QueryWrapper buildQueryWrapper(OrderQueryDTO queryDTO) {
QueryWrapper wrapper = QueryWrapper.create()
.from(ORDER)
.leftJoin(USER).on(ORDER.USER_ID.eq(USER.ID));
// 使用更简洁的条件判断
wrapper.where(USER.USERNAME.like(queryDTO.getUsername()).when(queryDTO.getUsername() != null))
.where(ORDER.ORDER_NO.like(queryDTO.getOrderNo()).when(queryDTO.getOrderNo() != null))
.where(ORDER.STATUS.eq(queryDTO.getStatus()).when(queryDTO.getStatus() != null))
.where(ORDER.TOTAL_AMOUNT.ge(queryDTO.getMinAmount()).when(queryDTO.getMinAmount() != null))
.where(ORDER.TOTAL_AMOUNT.le(queryDTO.getMaxAmount()).when(queryDTO.getMaxAmount() != null))
.where(ORDER.CREATE_TIME.ge(queryDTO.getStartTime()).when(queryDTO.getStartTime() != null))
.where(ORDER.CREATE_TIME.le(queryDTO.getEndTime()).when(queryDTO.getEndTime() != null))
.where(ORDER.USER_ID.eq(queryDTO.getUserId()).when(queryDTO.getUserId() != null));
return wrapper.orderBy(ORDER.CREATE_TIME.desc());
}
/**
* 方式3:使用子查询
* 查询购买了指定商品的所有订单
*/
default List<Order> selectOrdersByProductName(String productName) {
// 子查询:查询包含该商品的订单ID
RawQuery subQuery = RawQuery.create()
.select("order_id")
.from("order_detail od")
.join("product p ON od.product_id = p.id")
.where("p.name LIKE ?", "%" + productName + "%");
QueryWrapper wrapper = QueryWrapper.create()
.from(ORDER)
.where(ORDER.ID.in(subQuery));
return this.selectListWithRelations(wrapper);
}
/**
* 方式4:聚合查询(统计)
* 统计订单总金额、总数量
*/
@Select("SELECT " +
" COUNT(*) as totalCount, " +
" SUM(total_amount) as totalAmount, " +
" AVG(total_amount) as avgAmount, " +
" MAX(total_amount) as maxAmount, " +
" MIN(total_amount) as minAmount " +
"FROM user_order " +
"WHERE status = #{status}")
OrderStatisticsVO selectOrderStatistics(@Param("status") Integer status);
/**
* 方式5:分组查询
* 按状态统计订单数量和金额
*/
@Select("SELECT " +
" status, " +
" COUNT(*) as count, " +
" SUM(total_amount) as totalAmount " +
"FROM user_order " +
"GROUP BY status")
List<OrderStatusStatisticsVO> selectOrderStatusStatistics();
}统计VO
OrderStatisticsVO.java
package com.example.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
* 订单统计VO
*/
@Data
public class OrderStatisticsVO {
/**
* 订单总数
*/
private Long totalCount;
/**
* 订单总金额
*/
private BigDecimal totalAmount;
/**
* 平均订单金额
*/
private BigDecimal avgAmount;
/**
* 最大订单金额
*/
private BigDecimal maxAmount;
/**
* 最小订单金额
*/
private BigDecimal minAmount;
}OrderStatusStatisticsVO.java
package com.example.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
* 订单状态统计VO
*/
@Data
public class OrderStatusStatisticsVO {
/**
* 状态码
*/
private Integer status;
/**
* 订单数量
*/
private Long count;
/**
* 订单金额
*/
private BigDecimal totalAmount;
}Service层
OrderService.java
package com.example.service;
import com.mybatisflex.core.paginate.Page;
import com.example.dto.OrderQueryDTO;
import com.example.entity.Order;
import com.example.mapper.OrderMapper;
import com.example.vo.OrderStatisticsVO;
import com.example.vo.OrderStatusStatisticsVO;
import com.example.vo.OrderVO;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.stream.Collectors;
/**
* 订单Service
*/
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
/**
* 查询订单列表
*/
public List<OrderVO> queryOrderList(OrderQueryDTO queryDTO) {
List<Order> orders = orderMapper.selectOrderListByQuery(queryDTO);
return convertToVOList(orders);
}
/**
* 分页查询订单
*/
public Page<OrderVO> queryOrderPage(int pageNum, int pageSize, OrderQueryDTO queryDTO) {
Page<Order> page = Page.of(pageNum, pageSize);
Page<Order> orderPage = orderMapper.selectOrderPage(page, queryDTO);
// 转换为VO分页
List<OrderVO> voList = convertToVOList(orderPage.getRecords());
Page<OrderVO> voPage = new Page<>();
BeanUtils.copyProperties(orderPage, voPage);
voPage.setRecords(voList);
return voPage;
}
/**
* 根据商品名称查询订单
*/
public List<OrderVO> queryOrdersByProductName(String productName) {
List<Order> orders = orderMapper.selectOrdersByProductName(productName);
return convertToVOList(orders);
}
/**
* 获取订单统计
*/
public OrderStatisticsVO getOrderStatistics(Integer status) {
return orderMapper.selectOrderStatistics(status);
}
/**
* 获取订单状态统计
*/
public List<OrderStatusStatisticsVO> getOrderStatusStatistics() {
return orderMapper.selectOrderStatusStatistics();
}
/**
* 实体转换为VO
*/
private List<OrderVO> convertToVOList(List<Order> orders) {
if (CollectionUtils.isEmpty(orders)) {
return List.of();
}
return orders.stream().map(order -> {
OrderVO vo = new OrderVO();
BeanUtils.copyProperties(order, vo);
// 设置状态文本
vo.setStatusText(convertStatusToText(order.getStatus()));
// 转换用户信息
if (order.getUser() != null) {
OrderVO.UserInfoVO userVO = new OrderVO.UserInfoVO();
BeanUtils.copyProperties(order.getUser(), userVO);
vo.setUser(userVO);
}
// 转换订单详情
if (!CollectionUtils.isEmpty(order.getOrderDetails())) {
List<OrderVO.OrderDetailVO> detailVOs = order.getOrderDetails().stream()
.map(detail -> {
OrderVO.OrderDetailVO detailVO = new OrderVO.OrderDetailVO();
BeanUtils.copyProperties(detail, detailVO);
if (detail.getProduct() != null) {
detailVO.setProductName(detail.getProduct().getName());
}
return detailVO;
})
.collect(Collectors.toList());
vo.setDetails(detailVOs);
// 计算商品总数
int totalQty = order.getOrderDetails().stream()
.mapToInt(detail -> detail.getQuantity())
.sum();
vo.setTotalQuantity(totalQty);
}
return vo;
}).collect(Collectors.toList());
}
/**
* 状态码转文本
*/
private String convertStatusToText(Integer status) {
if (status == null) return "未知";
return switch (status) {
case 0 -> "待支付";
case 1 -> "已支付";
case 2 -> "已发货";
case 3 -> "已完成";
default -> "未知";
};
}
}Controller层
OrderController.java
package com.example.controller;
import com.mybatisflex.core.paginate.Page;
import com.example.dto.OrderQueryDTO;
import com.example.service.OrderService;
import com.example.vo.OrderStatisticsVO;
import com.example.vo.OrderStatusStatisticsVO;
import com.example.vo.OrderVO;
import com.example.common.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 订单Controller
*/
@RestController
@RequestMapping("/api/order")
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 查询订单列表
*/
@PostMapping("/list")
public Result<List<OrderVO>> list(@RequestBody OrderQueryDTO queryDTO) {
List<OrderVO> list = orderService.queryOrderList(queryDTO);
return Result.success(list);
}
/**
* 分页查询订单
*/
@PostMapping("/page")
public Result<Page<OrderVO>> page(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "10") int pageSize,
@RequestBody OrderQueryDTO queryDTO) {
Page<OrderVO> page = orderService.queryOrderPage(pageNum, pageSize, queryDTO);
return Result.success(page);
}
/**
* 根据商品名称查询订单
*/
@GetMapping("/by-product")
public Result<List<OrderVO>> byProduct(@RequestParam String productName) {
List<OrderVO> list = orderService.queryOrdersByProductName(productName);
return Result.success(list);
}
/**
* 获取订单统计
*/
@GetMapping("/statistics")
public Result<OrderStatisticsVO> statistics(@RequestParam(required = false) Integer status) {
OrderStatisticsVO statistics = orderService.getOrderStatistics(status);
return Result.success(statistics);
}
/**
* 获取订单状态统计
*/
@GetMapping("/status-statistics")
public Result<List<OrderStatusStatisticsVO>> statusStatistics() {
List<OrderStatusStatisticsVO> list = orderService.getOrderStatusStatistics();
return Result.success(list);
}
}通用返回结果
Result.java
package com.example.common;
import lombok.Data;
/**
* 通用返回结果
*/
@Data
public class Result<T> {
/**
* 状态码
*/
private Integer code;
/**
* 提示信息
*/
private String message;
/**
* 数据
*/
private T data;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("success");
result.setData(data);
return result;
}
public static <T> Result<T> error(String message) {
Result<T> result = new Result<>();
result.setCode(500);
result.setMessage(message);
return result;
}
}测试代码
@SpringBootTest
public class ComplexQueryTest {
@Autowired
private OrderService orderService;
/**
* 测试1:动态条件查询
*/
@Test
public void testDynamicQuery() {
OrderQueryDTO queryDTO = new OrderQueryDTO();
// 只设置部分条件
queryDTO.setUsername("zhang"); // 模糊查询用户名
queryDTO.setStatus(1); // 已支付状态
queryDTO.setMinAmount(new BigDecimal("1000"));
List<OrderVO> list = orderService.queryOrderList(queryDTO);
System.out.println("查询结果:" + list.size() + " 条");
list.forEach(order -> {
System.out.println("订单:" + order.getOrderNo());
System.out.println("用户:" + order.getUser().getUsername());
System.out.println("金额:" + order.getTotalAmount());
System.out.println("---");
});
}
/**
* 测试2:分页查询
*/
@Test
public void testPageQuery() {
OrderQueryDTO queryDTO = new OrderQueryDTO();
Page<OrderVO> page = orderService.queryOrderPage(1, 2, queryDTO);
System.out.println("总页数:" + page.getTotalPage());
System.out.println("总记录数:" + page.getTotalRow());
System.out.println("当前页数据:" + page.getRecords().size());
page.getRecords().forEach(order -> {
System.out.println("订单:" + order.getOrderNo());
});
}
/**
* 测试3:子查询
*/
@Test
public void testSubQuery() {
// 查询购买了iPhone的订单
List<OrderVO> list = orderService.queryOrdersByProductName("iPhone");
System.out.println("购买了iPhone的订单:" + list.size() + " 条");
list.forEach(order -> {
System.out.println("订单:" + order.getOrderNo());
System.out.println("用户:" + order.getUser().getUsername());
});
}
/**
* 测试4:统计查询
*/
@Test
public void testStatistics() {
// 统计已支付订单
OrderStatisticsVO statistics = orderService.getOrderStatistics(1);
System.out.println("订单统计:");
System.out.println(" 总数:" + statistics.getTotalCount());
System.out.println(" 总金额:" + statistics.getTotalAmount());
System.out.println(" 平均金额:" + statistics.getAvgAmount());
}
/**
* 测试5:分组统计
*/
@Test
public void testGroupByStatistics() {
List<OrderStatusStatisticsVO> list = orderService.getOrderStatusStatistics();
System.out.println("按状态统计:");
list.forEach(stat -> {
System.out.println(" 状态" + stat.getStatus() +
":" + stat.getCount() + " 单," +
"金额:" + stat.getTotalAmount());
});
}
}关键点总结
QueryWrapper动态条件:
使用
.when()方法简化空值判断支持链式调用,代码优雅
类型安全,避免SQL注入
分页查询:
使用
Page对象封装分页参数paginateWithRelations()自动处理关联查询支持排序、过滤
子查询:
使用
RawQuery构建子查询支持IN、EXISTS等操作
聚合查询:
使用注解方式编写复杂统计SQL
支持COUNT、SUM、AVG、MAX、MIN等函数
支持GROUP BY分组
性能优化建议:
大数据量时使用分页,避免内存溢出
关联查询注意索引优化
统计查询可以使用缓存
案例5:性能优化与最佳实践
场景说明
在实际生产环境中,多表联查往往面临性能挑战。本案例讲解如何优化MyBatis-Flex的多表查询性能。
常见问题与解决方案
1. N+1查询问题
问题描述: 查询100条订单,每条订单关联用户和详情,会产生1+100+100=201次查询。
解决方案:使用延迟加载或批量查询
/**
* 优化前:N+1问题
* 查询订单列表时,每条订单都会单独查询用户和详情
*/
@RelationOneToOne(selfField = "userId", targetField = "id")
private User user;
@RelationOneToMany(selfField = "id", targetField = "orderId")
private List<OrderDetail> orderDetails;
/**
* 优化后:使用@RelationManyToMany代替多次一对多
* 或者使用XML中的嵌套结果映射
*/
// 在XML中使用JOIN查询,一次查询所有数据
<select id="selectOrdersWithAllInfo" resultMap="OrderFullResultMap">
SELECT o.*, u.*, od.*, p.*
FROM user_order o
LEFT JOIN user u ON o.user_id = u.id
LEFT JOIN order_detail od ON o.id = od.order_id
LEFT JOIN product p ON od.product_id = p.id
</select>2. 分页查询优化
/**
* 分页查询优化
* 先分页查询主表,再批量查询关联数据
*/
default Page<Order> selectOrderPageOptimized(Page<Order> page, OrderQueryDTO queryDTO) {
// 第一步:只查询主表(带分页)
QueryWrapper wrapper = buildQueryWrapper(queryDTO);
Page<Order> orderPage = this.paginate(page, wrapper);
// 第二步:批量查询关联数据(避免N+1)
if (!orderPage.getRecords().isEmpty()) {
// 提取所有订单ID
List<Long> orderIds = orderPage.getRecords().stream()
.map(Order::getId)
.collect(Collectors.toList());
// 批量查询用户
List<Long> userIds = orderPage.getRecords().stream()
.map(Order::getUserId)
.distinct()
.collect(Collectors.toList());
// 使用IN查询批量获取用户
Map<Long, User> userMap = userMapper.selectListByIds(userIds).stream()
.collect(Collectors.toMap(User::getId, Function.identity()));
// 批量查询订单详情
Map<Long, List<OrderDetail>> detailMap = orderDetailMapper
.selectListByOrderIds(orderIds).stream()
.collect(Collectors.groupingBy(OrderDetail::getOrderId));
// 组装数据
orderPage.getRecords().forEach(order -> {
order.setUser(userMap.get(order.getUserId()));
order.setOrderDetails(detailMap.get(order.getId()));
});
}
return orderPage;
}3. 索引优化建议
-- 订单表索引优化
-- 1. 用户ID索引(加速用户订单查询)
CREATE INDEX idx_order_user_id ON user_order(user_id);
-- 2. 订单状态索引(加速状态筛选)
CREATE INDEX idx_order_status ON user_order(status);
-- 3. 创建时间索引(加速时间范围查询)
CREATE INDEX idx_order_create_time ON user_order(create_time);
-- 4. 复合索引(用户+状态,常用组合查询)
CREATE INDEX idx_order_user_status ON user_order(user_id, status);
-- 5. 订单号唯一索引
CREATE UNIQUE INDEX uk_order_no ON user_order(order_no);
-- 订单详情表索引
-- 6. 订单ID索引(加速订单详情查询)
CREATE INDEX idx_detail_order_id ON order_detail(order_id);
-- 7. 商品ID索引(加速商品销售统计)
CREATE INDEX idx_detail_product_id ON order_detail(product_id);
-- 用户角色关联表索引
-- 8. 复合唯一索引
CREATE UNIQUE INDEX uk_user_role ON user_role(user_id, role_id);4. 查询字段优化
/**
* 只查询需要的字段,避免SELECT *
*/
default List<Order> selectOrderListOptimized(OrderQueryDTO queryDTO) {
QueryWrapper wrapper = QueryWrapper.create()
// 指定查询字段
.select(ORDER.ID, ORDER.ORDER_NO, ORDER.TOTAL_AMOUNT, ORDER.STATUS)
.from(ORDER)
.leftJoin(USER).on(ORDER.USER_ID.eq(USER.ID))
// 用户表也只查需要的字段
.select(USER.USERNAME, USER.EMAIL)
.where(ORDER.STATUS.eq(queryDTO.getStatus()));
return this.selectListByQueryAs(wrapper, Order.class);
}5. 缓存优化
/**
* 使用Spring Cache缓存频繁查询的数据
*/
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
/**
* 缓存订单详情(5分钟过期)
*/
@Cacheable(value = "order", key = "#orderId")
public OrderVO getOrderById(Long orderId) {
Order order = orderMapper.selectOneWithRelationsById(orderId);
return convertToVO(order);
}
/**
* 更新订单时清除缓存
*/
@CacheEvict(value = "order", key = "#orderId")
public void updateOrder(Long orderId, OrderUpdateDTO dto) {
// 更新逻辑
}
/**
* 缓存用户订单列表(1分钟过期)
*/
@Cacheable(value = "userOrders", key = "#userId + '_' + #pageNum + '_' + #pageSize")
public Page<OrderVO> getUserOrders(Long userId, int pageNum, int pageSize) {
// 查询逻辑
}
}
/**
* Redis缓存配置
*/
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(5)) // 默认5分钟过期
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}
}6. 异步查询优化
/**
* 使用CompletableFuture并行查询关联数据
*/
@Service
public class OrderService {
@Autowired
private ExecutorService executorService; // 线程池
public OrderVO getOrderWithAllInfoAsync(Long orderId) {
// 1. 查询订单主表
Order order = orderMapper.selectById(orderId);
// 2. 并行查询关联数据
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(
() -> userMapper.selectById(order.getUserId()),
executorService
);
CompletableFuture<List<OrderDetail>> detailsFuture = CompletableFuture.supplyAsync(
() -> orderDetailMapper.selectByOrderId(orderId),
executorService
);
CompletableFuture<List<OrderLog>> logsFuture = CompletableFuture.supplyAsync(
() -> orderLogMapper.selectByOrderId(orderId),
executorService
);
// 3. 等待所有查询完成
CompletableFuture.allOf(userFuture, detailsFuture, logsFuture).join();
// 4. 组装结果
OrderVO vo = new OrderVO();
BeanUtils.copyProperties(order, vo);
vo.setUser(userFuture.join());
vo.setOrderDetails(detailsFuture.join());
vo.setLogs(logsFuture.join());
return vo;
}
}7. 批量操作优化
/**
* 批量插入优化
* 使用MyBatis-Flex的批量插入功能
*/
default void batchInsertOrders(List<Order> orders) {
// 分批插入,每批1000条
int batchSize = 1000;
ListUtils.partition(orders, batchSize).forEach(batch -> {
this.insertBatch(batch);
});
}
/**
* 批量更新优化
*/
default void batchUpdateOrderStatus(List<Long> orderIds, Integer status) {
QueryWrapper wrapper = QueryWrapper.create()
.set(ORDER.STATUS, status)
.where(ORDER.ID.in(orderIds));
this.updateByQuery(wrapper);
}最佳实践总结
1. 查询优化 checklist
是否使用了索引?
是否只查询了需要的字段?
是否存在N+1问题?
大数据量是否使用了分页?
是否使用了缓存?
复杂查询是否使用了异步并行?
2. 代码规范
/**
* 推荐的Mapper方法命名规范
*/
public interface OrderMapper extends BaseMapper<Order> {
// 查询单个(带关联)
Order selectOrderWithUser(Long orderId);
// 查询列表(带关联)
List<Order> selectOrderListWithUser(OrderQueryDTO queryDTO);
// 分页查询(带关联)
Page<Order> selectOrderPageWithUser(Page<Order> page, OrderQueryDTO queryDTO);
// 批量查询
List<Order> selectOrderListByIds(List<Long> orderIds);
// 统计查询
OrderStatisticsVO selectOrderStatistics(OrderQueryDTO queryDTO);
}3. 监控与调优
/**
* SQL执行时间监控
*/
@Component
public class SqlPerformanceInterceptor implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger(SqlPerformanceInterceptor.class);
@Override
public Object intercept(Invocation invocation) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = invocation.proceed();
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
// 记录慢查询(超过1秒)
if (duration > 1000) {
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
logger.warn("慢查询: {},执行时间: {}ms", mappedStatement.getId(), duration);
}
return result;
}
}项目完整文件清单
1. 启动类
MybatisFlexDemoApplication.java
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Spring Boot启动类
*/
@SpringBootApplication
public class MybatisFlexDemoApplication {
public static void main(String[] args) {
SpringApplication.run(MybatisFlexDemoApplication.class, args);
System.out.println("MyBatis-Flex多表联查Demo启动成功!");
System.out.println("访问地址: http://localhost:8080");
}
}2. 配置类
MybatisFlexConfig.java
package com.example.config;
import com.mybatisflex.core.FlexGlobalConfig;
import com.mybatisflex.core.MybatisFlexBootstrap;
import com.mybatisflex.core.dialect.DbType;
import com.mybatisflex.core.dialect.DialectFactory;
import com.mybatisflex.core.logicdelete.LogicDeleteManager;
import com.mybatisflex.core.mybatis.FlexConfiguration;
import com.mybatisflex.core.query.SqlOperators;
import com.mybatisflex.spring.boot.MybatisFlexCustomizer;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MyBatis-Flex配置类
* 用于自定义MyBatis-Flex的各种配置
*/
@Configuration
@MapperScan("com.example.mapper") // 扫描Mapper接口
public class MybatisFlexConfig implements MybatisFlexCustomizer {
/**
* 自定义MyBatis-Flex配置
* 此方法在MyBatis-Flex初始化时自动调用
*/
@Override
public void customize(FlexConfiguration configuration) {
// 设置数据库方言(自动识别,一般不需要手动设置)
// DialectFactory.setDialect(DbType.MYSQL);
// 配置逻辑删除处理器
// 全局配置已在application.yml中设置,这里可以覆盖
LogicDeleteManager.setProcessor(new FlexGlobalConfig.LogicDeleteConfig());
// 配置SQL操作符(用于复杂查询)
SqlOperators operators = new SqlOperators();
// 可以自定义操作符
// operators.addOperator("CUSTOM_OP", "自定义操作符");
}
/**
* 配置全局设置
* 如:驼峰命名映射、下划线转驼峰等
*/
@Bean
public FlexGlobalConfig flexGlobalConfig() {
FlexGlobalConfig globalConfig = new FlexGlobalConfig();
// 设置逻辑删除字段(全局)
globalConfig.setLogicDeleteColumn("is_deleted");
// 设置逻辑删除值(已删除)
globalConfig.setLogicDeleteValue("1");
// 设置逻辑未删除值(未删除)
globalConfig.setLogicNormalValue("0");
// 设置是否打印SQL(开发环境建议开启)
globalConfig.setPrintBanner(false); // 关闭启动banner
return globalConfig;
}
/**
* 配置审计功能(可选)
* 自动填充创建时间、更新时间、创建人、更新人等字段
*/
// @Bean
// public MetaObjectHandler metaObjectHandler() {
// return new MetaObjectHandler() {
// @Override
// public void insertFill(Object entity) {
// // 插入时自动填充
// if (entity instanceof BaseEntity) {
// BaseEntity baseEntity = (BaseEntity) entity;
// baseEntity.setCreateTime(LocalDateTime.now());
// baseEntity.setUpdateTime(LocalDateTime.now());
// }
// }
//
// @Override
// public void updateFill(Object entity) {
// // 更新时自动填充
// if (entity instanceof BaseEntity) {
// BaseEntity baseEntity = (BaseEntity) entity;
// baseEntity.setUpdateTime(LocalDateTime.now());
// }
// }
// };
// }
}3. 实体类完整代码
Product.java
package com.example.entity;
import com.mybatisflex.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 商品实体类
*/
@Data
@Table("product")
public class Product {
@Id
private Long id;
@Column("name")
private String name;
@Column("description")
private String description;
@Column("price")
private BigDecimal price;
@Column("stock")
private Integer stock;
@Column("category_id")
private Long categoryId;
@Column("status")
private Integer status;
@Column(value = "create_time", isIgnore = true)
private LocalDateTime createTime;
@Column(value = "update_time", isIgnore = true)
private LocalDateTime updateTime;
// 关联分类
@RelationOneToOne(selfField = "categoryId", targetField = "id")
private Category category;
}Category.java
package com.example.entity;
import com.mybatisflex.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
/**
* 分类实体类
*/
@Data
@Table("category")
public class Category {
@Id
private Long id;
@Column("name")
private String name;
@Column("description")
private String description;
@Column("sort_order")
private Integer sortOrder;
@Column(value = "create_time", isIgnore = true)
private LocalDateTime createTime;
// 一对多:该分类下的商品
@RelationOneToMany(selfField = "id", targetField = "categoryId")
private List<Product> products;
}4. TableDef类(QueryWrapper类型安全)
OrderTableDef.java
package com.example.entity.table;
import com.mybatisflex.core.query.QueryColumn;
import com.mybatisflex.core.table.TableDef;
/**
* 订单表定义
* 用于QueryWrapper的类型安全查询
*/
public class OrderTableDef extends TableDef {
public static final OrderTableDef ORDER = new OrderTableDef();
public final QueryColumn ID = new QueryColumn(this, "id");
public final QueryColumn USER_ID = new QueryColumn(this, "user_id");
public final QueryColumn ORDER_NO = new QueryColumn(this, "order_no");
public final QueryColumn TOTAL_AMOUNT = new QueryColumn(this, "total_amount");
public final QueryColumn STATUS = new QueryColumn(this, "status");
public final QueryColumn REMARK = new QueryColumn(this, "remark");
public final QueryColumn CREATE_TIME = new QueryColumn(this, "create_time");
public final QueryColumn UPDATE_TIME = new QueryColumn(this, "update_time");
public OrderTableDef() {
super("user_order");
}
}UserTableDef.java
package com.example.entity.table;
import com.mybatisflex.core.query.QueryColumn;
import com.mybatisflex.core.table.TableDef;
/**
* 用户表定义
*/
public class UserTableDef extends TableDef {
public static final UserTableDef USER = new UserTableDef();
public final QueryColumn ID = new QueryColumn(this, "id");
public final QueryColumn USERNAME = new QueryColumn(this, "username");
public final QueryColumn EMAIL = new QueryColumn(this, "email");
public final QueryColumn PHONE = new QueryColumn(this, "phone");
public final QueryColumn STATUS = new QueryColumn(this, "status");
public final QueryColumn CREATE_TIME = new QueryColumn(this, "create_time");
public final QueryColumn UPDATE_TIME = new QueryColumn(this, "update_time");
public UserTableDef() {
super("user");
}
}OrderDetailTableDef.java
package com.example.entity.table;
import com.mybatisflex.core.query.QueryColumn;
import com.mybatisflex.core.table.TableDef;
/**
* 订单详情表定义
*/
public class OrderDetailTableDef extends TableDef {
public static final OrderDetailTableDef ORDER_DETAIL = new OrderDetailTableDef();
public final QueryColumn ID = new QueryColumn(this, "id");
public final QueryColumn ORDER_ID = new QueryColumn(this, "order_id");
public final QueryColumn PRODUCT_ID = new QueryColumn(this, "product_id");
public final QueryColumn QUANTITY = new QueryColumn(this, "quantity");
public final QueryColumn UNIT_PRICE = new QueryColumn(this, "unit_price");
public final QueryColumn TOTAL_PRICE = new QueryColumn(this, "total_price");
public final QueryColumn CREATE_TIME = new QueryColumn(this, "create_time");
public OrderDetailTableDef() {
super("order_detail");
}
}5. Mapper接口完整代码
ProductMapper.java
package com.example.mapper;
import com.mybatisflex.core.BaseMapper;
import com.example.entity.Product;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 商品Mapper接口
*/
public interface ProductMapper extends BaseMapper<Product> {
/**
* 根据分类ID查询商品
*/
@Select("SELECT * FROM product WHERE category_id = #{categoryId} AND status = 1")
List<Product> selectByCategoryId(@Param("categoryId") Long categoryId);
/**
* 查询商品及分类信息
*/
default Product selectProductWithCategory(Long productId) {
return this.selectOneWithRelationsById(productId);
}
}CategoryMapper.java
package com.example.mapper;
import com.mybatisflex.core.BaseMapper;
import com.example.entity.Category;
import java.util.List;
/**
* 分类Mapper接口
*/
public interface CategoryMapper extends BaseMapper<Category> {
/**
* 查询分类及商品列表
*/
default Category selectCategoryWithProducts(Long categoryId) {
return this.selectOneWithRelationsById(categoryId);
}
/**
* 查询所有分类及商品
*/
default List<Category> selectAllWithProducts() {
return this.selectAllWithRelations();
}
}UserRoleMapper.java
package com.example.mapper;
import com.mybatisflex.core.BaseMapper;
import com.example.entity.UserRole;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 用户角色关联Mapper接口
*/
public interface UserRoleMapper extends BaseMapper<UserRole> {
/**
* 根据用户ID查询角色关联
*/
@Select("SELECT * FROM user_role WHERE user_id = #{userId}")
List<UserRole> selectByUserId(@Param("userId") Long userId);
/**
* 根据角色ID查询用户关联
*/
@Select("SELECT * FROM user_role WHERE role_id = #{roleId}")
List<UserRole> selectByRoleId(@Param("roleId") Long roleId);
}6. 完整pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>mybatis-flex-demo</artifactId>
<version>1.0.0</version>
<name>MyBatis-Flex多表联查Demo</name>
<description>MyBatis-Flex框架多表联查完整实战案例</description>
<properties>
<java.version>1.8</java.version>
<mybatis-flex.version>1.7.5</mybatis-flex.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- MyBatis-Flex -->
<dependency>
<groupId>com.mybatis-flex</groupId>
<artifactId>mybatis-flex-spring-boot-starter</artifactId>
<version>${mybatis-flex.version}</version>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- 连接池 -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>7. 测试入口
AllTests.java
package com.example;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
/**
* 所有测试入口
*/
@SpringBootTest
public class AllTests {
@Test
void contextLoads() {
System.out.println("Spring Boot上下文加载成功!");
}
}8. 项目构建与运行
构建命令
# 1. 进入项目目录
cd mybatis-flex-demo
# 2. 编译项目
mvn clean compile
# 3. 运行测试
mvn test
# 4. 打包
mvn clean package
# 5. 运行
java -jar target/mybatis-flex-demo-1.0.0.jar
# 或
mvn spring-boot:runAPI测试
# 测试查询订单列表
curl -X POST http://localhost:8080/api/order/list \
-H "Content-Type: application/json" \
-d '{"status":1}'
# 测试分页查询
curl -X POST "http://localhost:8080/api/order/page?pageNum=1&pageSize=10" \
-H "Content-Type: application/json" \
-d '{}'
# 测试统计
curl http://localhost:8080/api/order/statistics?status=19. 学习路径建议
新手学习顺序
第1天:环境搭建 + 案例1(一对一)
第2天:案例2(一对多)+ 案例3(多对多)
第3天:案例4(复杂查询)+ 案例5(性能优化)
第4天:自己实现一个完整功能模块
重点掌握
@RelationOneToOne 一对一关联
@RelationOneToMany 一对多关联
@RelationManyToMany 多对多关联
QueryWrapper 动态查询
分页查询
性能优化技巧