相关开源地址:
系列教程:从零到生产落地SimpleDAO,本集聚焦联表查询+分页场景,全程原生SQL、零XML、高性能,仅需Java基础即可上手。
📋 前置知识
本教程无需复杂框架经验,你只需要掌握:
| Java(类、接口、注解、泛型) | MyBatis/Hibernate 任何知识 |
| SQL(SELECT、JOIN、WHERE) | XML配置、动态标签、插件开发 |
| Spring Boot(数据源配置、依赖注入) | Spring Boot自动配置原理 |
| IDE运行Maven项目 | Maven高级配置、父子工程 |
📚 全套教程总览
| 01 · 单表 CRUD + 审计 + 逻辑删除 | 约 6 min | 零代码单表操作、自动填充审计、内置逻辑删除 |
| 02 · 联表查询 + 分页 | 约 5 min | 单/多表统一API,无需ResultMap |
| 03 · 条件进阶:IN + 子查询 | 约 5 min | 替代MyBatis foreach标签,动态片段拼接 |
| 04 · 多表联查 + 复杂条件 | 约 5 min | 多表关联、区间筛选通用写法 |
| 05 · 报表聚合:GROUP BY + 聚合函数 | 约 6 min | 原生SQL报表无限制,不受ORM束缚 |
| 06 · mergeParams 多组条件合并 | 约 6 min | 复杂报表拆分多条件,解耦复用 |
| 07 · 多租户 + 数据权限 · AOP 破局 | 约 7 min | 不用MyBatis拦截器,Spring原生AOP扩展 |
| 08 · 脱敏 + 审计扩展 · 框架不设限 | 约 7 min | 纠正MyBatis持久层脱敏错误分层思路 |
🎯 本集核心目标
📦 项目基础准备
1. 项目结构
标准Spring Boot工程结构,核心文件如下:
demo02_join_query/
├── pom.xml # 依赖配置
└── src/main/
├── java/example/
│ ├── DemoApplication.java # 启动类
│ └── order/ # 订单业务模块
│ ├── Order.java # 订单实体类
│ ├── OrderVO.java # 联表结果VO
│ ├── OrderCond.java # 条件类
│ └── OrderDao.java # DAO层
└── resources/
├── application.yml # 数据源配置
└── schema.sql # 建表&测试数据
2. 核心依赖
仅需4个核心依赖,无冗余依赖,开箱即用:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.simple</groupId>
<artifactId>simple-dao</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
- spring-boot-starter-jdbc:Spring JDBC基础能力
- simple-dao:核心框架,提供CRUD、分页、条件拼接能力
- h2:内存数据库,无需额外部署,测试便捷
- lombok:简化实体类编写(Getter/Setter/ToString等)
3. 数据源配置(application.yml)
标准Spring Boot数据源配置,替换URL/driver即可适配MySQL/Oracle等:
spring:
datasource:
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=–1 # H2内存库
driver-class-name: org.h2.Driver
username: sa
password:
sql:
init:
schema-locations: classpath:schema.sql # 启动时执行建表SQL
mode: always
4. 建表&测试数据(schema.sql)
(1)表结构定义
用户表+订单表,包含业务字段+审计字段(创建时间、逻辑删除等):
— 用户表(sys_user)
CREATE TABLE IF NOT EXISTS sys_user (
id INTEGER PRIMARY KEY,
name VARCHAR(50) NOT NULL, — 用户名
age INTEGER, — 年龄
email VARCHAR(100) UNIQUE NOT NULL, — 邮箱
phone VARCHAR(20) UNIQUE, — 手机号
password VARCHAR(64) NOT NULL, — 密码
sex CHAR(1), — 性别 1男 2女
status INTEGER DEFAULT 1, — 状态 1正常 0禁用
create_time TIMESTAMP,
create_by INTEGER,
update_time TIMESTAMP,
update_by INTEGER,
dr INTEGER DEFAULT 0 — 逻辑删除标记
);
— 订单表(bus_order)
CREATE TABLE IF NOT EXISTS bus_order (
id INTEGER PRIMARY KEY,
order_no VARCHAR(50) UNIQUE NOT NULL, — 订单编号
user_id INTEGER NOT NULL, — 关联用户ID
total_amount DECIMAL(12,2) NOT NULL, — 订单总金额
order_status INTEGER DEFAULT 0, — 订单状态 0待支付 1已支付
create_time TIMESTAMP,
create_by INTEGER,
update_time TIMESTAMP,
update_by INTEGER,
dr INTEGER DEFAULT 0 — 逻辑删除标记
);
(2)测试数据
插入3个用户+4个订单,用于联表查询测试:
— 插入用户数据
INSERT INTO sys_user (id, name, age, email, phone, password, sex, status) VALUES
(1, '张三', 25, 'zhangsan@test.com', '13800138001', 'e10adc3949ba59abbe56e057f20f883e', 1, 1),
(2, '李四', 30, 'lisi@test.com', '13800138002', 'e10adc3949ba59abbe56e057f20f883e', 1, 1),
(3, '王五', 28, 'wangwu@test.com', '13800138003', 'e10adc3949ba59abbe56e057f20f883e', 2, 1);
— 插入订单数据
INSERT INTO bus_order (id, order_no, user_id, total_amount, order_status) VALUES
(1, 'ORD2026001', 1, 1999.00, 1),
(2, 'ORD2026002', 2, 299.00, 1),
(3, 'ORD2026003', 1, 399.00, 1),
(4, 'ORD2026004', 1, 1299.00, 1);
👨💻 核心代码实现
1. 实体类:Order(订单表映射)
用@Table指定数据库表名,@Id标记主键,审计字段可通过@Schema(hidden=true)隐藏:
@Data
@Schema(description = "【订单表】实体")
@Table("bus_order") // 关联数据库表名
public class Order {
@Id // 主键标记
@Schema(description = "主键ID")
private Integer id;
@Schema(description = "订单编号")
private String orderNo;
@Schema(description = "用户ID")
private Integer userId;
// 省略其他业务字段(totalAmount、orderStatus等)
// 审计字段(隐藏,无需前端展示)
@Schema(hidden = true)
private LocalDateTime createTime;
@Schema(hidden = true)
private Integer createBy;
@Schema(hidden = true)
private Integer dr; // 逻辑删除标记
}
2. 联表结果VO:OrderVO
继承Order实体,扩展用户表字段(联表查询结果映射):
@Getter
@Setter
@ToString(callSuper = true) // 打印时包含父类字段
public class OrderVO extends Order {
@Schema(description = "用户姓名")
private String userName; // 对应sys_user.name
@Schema(description = "用户手机号")
private String userPhone; // 对应sys_user.phone
@Schema(description = "用户邮箱")
private String userEmail; // 对应sys_user.email
}
说明:VO字段名(如userName)需与联表SQL中的别名(u.name user_name)对应,框架自动完成字段映射。
3. DAO层:OrderDao(联表分页核心)
核心逻辑:原生LEFT JOIN SQL + page方法一键分页
@Repository
public class OrderDao extends BaseDao<Order> {
// 联表SQL:订单表LEFT JOIN用户表,查询订单+用户字段
private final static String SQL = """
SELECT t.id, t.order_no, t.user_id, t.total_amount, t.order_status,
t.create_time, t.dr,
u.name user_name, u.phone user_phone, u.email user_email
FROM bus_order t
LEFT JOIN sys_user u ON t.user_id = u.id — 关联用户表
""";
/**
* 订单+用户联表分页查询
* @param cond 查询条件
* @return 分页结果(OrderVO)
*/
public Page<OrderVO> pageJoin(OrderCond cond) {
// page方法:自动拼接条件、生成COUNT、分页LIMIT
return page(SQL, cond, OrderVO.class);
}
}
关键说明:
4. 条件类:OrderCond(跨表条件透传)
核心是addCondition方法,通过add方法拼接跨表条件,一行搞定:
@Data
public class OrderCond extends BaseCondition {
// 查询条件字段(支持订单表+用户表字段)
private String orderNo; // 订单编号
private Integer userId; // 订单-用户ID
private String userName; // 用户姓名
private String userPhone; // 用户手机号
private String[] orderNos; // 订单编号数组(IN查询)
@Override
protected void addCondition() {
// 1. 订单编号模糊查询(%xxx%),第三个参数3表示前后模糊匹配
add("AND t.order_no LIKE ?", orderNo, 3);
// 2. 订单-用户ID精准匹配
add("AND t.user_id = ?", userId);
// 3. 用户名反向模糊查询(NOT LIKE %xxx%)
add("AND u.name NOT LIKE ?", userName, 3);
// 4. 用户手机号精准匹配
add("AND u.phone = ?", userPhone);
// 5. 订单编号IN查询(数组自动展开为 (?, ?, …))
add("AND t.order_no IN ", orderNos);
}
}
add方法核心规则:
- 第一个参数:SQL条件片段(支持任意跨表别名,如t./u.);
- 第二个参数:条件值(null时自动忽略该条件,无需手动判断);
- 第三个参数(可选):模糊匹配类型(1:前模糊 %xxx;2:后模糊 xxx%;3:前后模糊 %xxx%);
- IN查询:只需写AND 字段 IN ,数组参数自动展开为IN (?, ?, …)。
🚀 业务层调用&效果验证
1. 调用示例
@Service
public class OrderService {
@Autowired
private OrderDao orderDao;
public void demoJoinPage() {
// 构建查询条件:用户名含“张” + 订单编号IN指定值
OrderCond cond = OrderCond.builder()
.userName("张")
.orderNos(new String[]{"ORD2026001", "ORD2026003", "ORD2026004"})
.build();
// 联表分页查询(默认页码1,每页10条)
Page<OrderVO> page = orderDao.pageJoin(cond);
// 打印结果
log.info("总记录数: {}", page.getRowCount());
page.getDataList().forEach(vo -> log.info("订单+用户信息: {}", vo));
}
}
2. 框架自动生成的SQL(日志)
— 高性能COUNT语句(替换法,非子查询)
[INFO] SELECT COUNT(1) FROM bus_order t LEFT JOIN sys_user u ON t.user_id = u.id WHERE u.name LIKE '%张%' AND t.order_no IN ('ORD2026001', 'ORD2026003', 'ORD2026004')
— 分页查询语句(自动拼接LIMIT)
[INFO] SELECT t.id, t.order_no, …, u.name user_name, …
FROM bus_order t LEFT JOIN sys_user u ON t.user_id = u.id
WHERE u.name LIKE '%张%' AND t.order_no IN ('ORD2026001', 'ORD2026003', 'ORD2026004')
LIMIT 0,10
高性能COUNT关键:框架并非将原SQL包裹为SELECT COUNT(1) FROM (原SQL),而是直接替换SELECT … FROM为SELECT COUNT(1) FROM,避免生成中间结果集,索引直接计数,高并发场景性能提升显著。
🥊 MyBatis 传统写法 VS SimpleDAO 写法清晰对比
对比1:模糊查询(用户名LIKE)
MyBatis XML 写法
<if test="userName != null">
AND u.name LIKE CONCAT('%', #{userName}, '%')
</if>
说明:需要切换XML语法、OGNL判断、SQL函数三种写法,代码冗长,多文件跳转。
SimpleDAO 写法
add("AND u.name LIKE ?", userName, 3);
说明:纯Java一行代码,数字3代表前后模糊匹配,参数为空自动忽略,无需判空。
对比2:IN 多条件批量匹配
MyBatis XML 写法
<if test="orderNos != null and orderNos.size() > 0">
AND t.order_no IN
<foreach collection="orderNos" item="no" open="(" separator="," close=")">
#{no}
</foreach>
</if>
说明:多层标签嵌套,标签属性多,漏写任一属性直接报错,维护成本高。
SimpleDAO 写法
add("AND t.order_no IN ", orderNos);
说明:直接传入数组,框架自动生成占位符,无需循环标签。当前文件内容过长,豆包只阅读了前 93%。
📝 本集总结
🎬 下集预告
条件进阶:IN + 子查询,掌握addIn方法和条件类复用技巧,进一步简化复杂查询场景。
源码获取:置顶链接(克隆即跑,零配置)。如果本教程对你有帮助,欢迎点赞+收藏,持续关注SimpleDAO企业实战系列!



