通用模板
运行/停止 Java 项目 - Linux
#!/bin/bash
# 定义要清理的 Redis Key 数组
REDIS_KEY_PATTERNS=(
"xxx-token:*"
"xxx-code:*"
"xxx-key"
)
# Redis 连接配置(根据需要修改)
# --raw 必须加:脚本/管道环境下 redis-cli 的输出会带引号,cursor 拿到的是 "0" 而非 0,循环永远退不出去
REDIS_CLI="redis-cli --raw"
# 如果需要认证,可以这样设置:
# REDIS_CLI="redis-cli -a your_password"
# 如果需要指定主机和端口:
# REDIS_CLI="redis-cli -h your_host -p your_port"
# 检查 Redis 是否可用
if ! $REDIS_CLI ping | grep -q "PONG"; then
echo "Redis 未启动或无法连接!"
exit 1
fi
echo "开始清理 Redis 缓存..."
# 初始化总计数
total_deleted=0
# 逐个模式清理
for pattern in "${REDIS_KEY_PATTERNS[@]}"; do
echo "清理模式: $pattern"
# 初始化变量
cursor=0
pattern_deleted=0
# 使用 SCAN 和 DEL 逐步清理键
while :; do
# SCAN 返回游标和匹配的键列表
result=$($REDIS_CLI scan $cursor match "$pattern" count 1000)
cursor=$(echo "$result" | head -n 1)
keys=$(echo "$result" | tail -n +2 | grep -v '^$')
# 删除匹配的键
if [[ ! -z "$keys" ]]; then
count=$(echo "$keys" | wc -l)
$REDIS_CLI del $keys > /dev/null 2>&1
pattern_deleted=$((pattern_deleted + count))
echo " 已删除 $count 个键"
fi
# 如果游标为 0,表示 SCAN 已完成
if [[ "$cursor" == "0" ]]; then
break
fi
done
total_deleted=$((total_deleted + pattern_deleted))
echo "模式 '$pattern' 完成: 删除 $pattern_deleted 个键"
echo
done
echo "Redis 缓存清理完成!"
echo "总共删除键数量: $total_deleted"#!/bin/bash
# 运行参数
PORT=8080
JAVA_HOME=/usr/local/jdk21/jdk-21.0.2
JAR_FILE="./xxx.jar"
PROFILES=dev
# 检查JAR文件是否存在
if [ ! -f "$JAR_FILE" ]; then
echo "错误: JAR文件 $JAR_FILE 不存在!"
exit 1
fi
# 查找占用指定端口的Java进程ID
PID=$(lsof -ti :$PORT)
if [ -z "$PID" ]; then
echo "没有找到占用端口 $PORT 的Java进程。"
else
echo "停止 Java 端口号: $PORT , 进程 (PID: $PID) 中。。。"
# 使用kill命令优雅地停止Java进程
kill -15 $PID 2>/dev/null
# 添加超时机制,避免无限等待
COUNTER=0
while ps -p $PID > /dev/null 2>&1 && [ $COUNTER -lt 30 ]; do
echo "等待 Java 进程 (PID: $PID) 停止... ($((COUNTER+1))/30秒)"
sleep 1
COUNTER=$((COUNTER+1))
done
# 如果优雅停止失败,强制停止
if ps -p $PID > /dev/null 2>&1; then
echo "优雅停止失败,强制停止进程..."
kill -9 $PID 2>/dev/null
sleep 2
fi
echo "Java程序(端口: $PORT) 已完全停止。"
fi
echo "启动Java应用..."
# nohup 后台启动,不占用当前窗口;日志必须落文件,重定向到 /dev/null 出了问题无从排查
# 不设 -Xmn:JDK 9+ 默认 G1,手动固定新生代会关闭自适应调节,那是 Parallel GC 时代的习惯
nohup $JAVA_HOME/bin/java \
-Dspring.profiles.active=$PROFILES \
-Xms2048m \
-Xmx2048m \
-jar "$JAR_FILE" > app.log 2>&1 &
# 回显后台进程 PID,方便跟踪进程和 tail 日志
echo "应用已启动,日志: app.log, PID: $!"# 程序运行端口号
PORT=8080
# 查找占用指定端口的Java进程ID
PID=$(lsof -ti :$PORT)
if [ -z "$PID" ]; then
echo "没有找到占用端口 $PORT 的Java进程。"
else
# 先 kill -15 优雅停止,给 Spring 足够的时间收尾(释放连接、执行 shutdown 钩子)
kill -15 $PID 2>/dev/null
echo "停止 Java 端口号: $PORT , 进程 (PID: $PID) 中。。。"
# 等待优雅停止,最多 30 秒
COUNTER=0
while ps -p $PID > /dev/null 2>&1 && [ $COUNTER -lt 30 ]; do
echo "等待 Java 进程 (PID: $PID) 停止... ($((COUNTER+1))/30秒)"
sleep 1
COUNTER=$((COUNTER+1))
done
# 优雅停止失败,强制停止
if ps -p $PID > /dev/null 2>&1; then
echo "优雅停止失败,强制停止进程..."
kill -9 $PID 2>/dev/null
sleep 2
fi
echo "Java程序(占用端口号port: $PORT ) 已完全停止。"
fi#!/bin/bash
# 定义要停止的端口数组
PORTS=(8080 7001 7002)
# 遍历端口数组并停止对应Java进程
for PORT in "${PORTS[@]}"; do
echo "处理端口: $PORT"
# 查找占用指定端口的Java进程ID
PID=$(lsof -ti :$PORT)
if [ -z "$PID" ]; then
echo "没有找到占用端口 $PORT 的Java进程。"
else
echo "停止 Java 端口号: $PORT , 进程 (PID: $PID)"
# 使用kill命令停止Java进程
kill -9 $PID
echo "Java程序(端口: $PORT) 已停止。"
fi
echo
done
echo "所有指定端口的Java程序停止完成。"运行/停止 Java 项目 - Win
Windows 中关闭 Java 程序手动将 CMD 黑窗口关掉就行(管他多少个,直接右击【全部关闭】)。
@echo off
setlocal enabledelayedexpansion
REM 本脚本请保存为 ANSI(GBK)编码,且不要加 chcp 65001,否则中文输出会乱码
:: 运行参数
set PORT=8080
set "JAVA_HOME=D:\start-package\back\jdk-8"
set "JAR_FILE=D:\start-package\back\apps\xxx.jar"
set CMD_TITLE=管理端
set PROFILES=dev
:: 设置控制台标题显示应用信息
title Java应用 - %CMD_TITLE% (端口:%PORT%)
:: 参数日志
echo JAVA_HOME: %JAVA_HOME%
echo JAR_FILE: %JAR_FILE%
echo PROFILES: %PROFILES%
:: 检查JAR文件是否存在
if not exist "%JAR_FILE%" (
echo 错误: JAR文件 %JAR_FILE% 不存在!
pause
exit /b 1
)
echo √ JAR文件存在
:: 检查Java是否可用
if not exist "%JAVA_HOME%\bin\java.exe" (
echo 错误: Java路径不正确或未安装!
echo 请检查JAVA_HOME: %JAVA_HOME%
pause
exit /b 1
)
echo √ Java环境正常
:: 查找占用指定端口的进程
echo 检查端口 %PORT% 占用情况...
set PID=
for /f "tokens=5" %%i in ('netstat -ano ^| findstr ":%PORT% " ^| findstr "LISTENING" 2^>nul') do (
set PID=%%i
)
if defined PID (
echo 发现占用端口 %PORT% 的进程 (PID: %PID%)
echo 正在停止该进程...
taskkill /pid %PID% /f >nul 2>&1
if errorlevel 1 (
echo 警告: 无法停止进程 %PID%,可能进程已退出
) else (
REM 等待进程停止
echo 等待进程停止...
timeout /t 3 /nobreak >nul
echo √ 进程已停止
)
) else (
echo √ 端口 %PORT% 未被占用
)
echo Java程序(端口: %PORT%) 已完全停止。
:START_APP
echo 启动Java应用...
:: 不设 -Xmn:JDK 9+ 默认 G1,手动固定新生代会关闭自适应调节,那是 Parallel GC 时代的习惯
:: 直接在当前窗口运行Java程序(会显示日志)
"%JAVA_HOME%\bin\java.exe" ^
-Dspring.profiles.active=%PROFILES% ^
-Xms2048m ^
-Xmx2048m ^
-jar "%JAR_FILE%"@echo off
setlocal enabledelayedexpansion
:: 此脚本用于关闭指定端口的 【Java进程】 和 【CMD窗口进程】
:: 仅适合关闭单个端口,多个端口貌似兼容性不行,试了很多方法都不能关掉 CMD 窗口
:: 本脚本请保存为 ANSI(GBK)编码,且不要加 chcp 65001,否则中文输出会乱码
:: 设置要停止的端口
set PORT=8080
:: 查找占用指定端口的进程
echo 检查端口 %PORT% 占用情况...
set PID=
for /f "tokens=5" %%i in ('netstat -ano ^| findstr ":%PORT% " ^| findstr "LISTENING" 2^>nul') do (
set PID=%%i
)
if defined PID (
echo 发现占用端口 %PORT% 的进程 (PID: %PID%)
echo 正在停止该进程...
REM 首先获取进程名称,用于查找相关的CMD窗口
REM wmic 在 Win11 24H2 起默认移除,新系统可改用 PowerShell 查询:
REM powershell -c "(Get-CimInstance Win32_Process -Filter 'ProcessId=%PID%').Name"
set PROCESS_NAME=
for /f "tokens=1" %%n in ('wmic process where processid^="%PID%" get name 2^>nul ^| findstr /v "Name"') do (
set PROCESS_NAME=%%n
)
taskkill /pid %PID% /f >nul 2>&1
if errorlevel 1 (
echo 警告: 无法停止进程 %PID%,可能进程已退出
) else (
REM 等待进程停止
echo 等待进程停止...
timeout /t 2 /nobreak >nul
echo √ Java进程已停止
)
REM 查找并关闭运行Java程序的CMD窗口
echo 查找相关的CMD窗口...
set WINDOW_FOUND=0
for /f "tokens=2" %%w in ('tasklist /fi "imagename eq cmd.exe" /fo table /nh') do (
REM 检查这个CMD窗口是否在运行Java程序
for /f "tokens=1" %%j in ('wmic process where "name='cmd.exe' and processid='%%w'" get commandline 2^>nul ^| findstr /i "java.*jar"') do (
echo 关闭相关的CMD窗口 (PID: %%w)...
taskkill /pid %%w /f >nul 2>&1
set WINDOW_FOUND=1
)
)
if "!WINDOW_FOUND!" == "1" (
echo √ CMD窗口已关闭
) else (
echo 未找到相关的CMD窗口,可能已自动关闭
)
) else (
echo √ 端口 %PORT% 未被占用
)
echo.
echo Java程序(端口: %PORT%) 已完全停止。
echo.
:: pause提示
有些极端场景,甲方服务器(Win 系统)无法联网,需要离线部署程序,跑过去帮甲方?呸!不可能!将前后端代码全部打包,让甲方自己传上去,无脑点击一下脚本文件就能自动运行最方便了。
在 Windows 中使用 bat 脚本快速启动一个 Java 程序的前后端,这里提供一个通用模板,下载下来,自己跟着说明玩一下,就能轻松实现一键在 Win 系统上运行 Java 程序。
数据库通用字段补齐
ALTER TABLE table_name
ADD COLUMN `version` int NULL DEFAULT NULL COMMENT '乐观锁',
ADD COLUMN `is_delete` int NULL DEFAULT NULL COMMENT '是否删除;0:否,1:是' AFTER `version`,
ADD COLUMN `create_by` varchar(255) NULL COMMENT '创建人' AFTER `is_delete`,
ADD COLUMN `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间' AFTER `create_by`,
ADD COLUMN `update_by` varchar(255) NULL COMMENT '修改人' AFTER `create_time`,
ADD COLUMN `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间' AFTER `update_by`;
-- MySQL 中,date 只包含年月日;datetime 的精度可选范围是 0~6,秒的小数位数,几乎都使用这个时间类型;
-- timestamp 与 datetime 类似,但内部会进行时区转换,用的较少
-- AFTER 引用的列必须是已存在的列,或本语句中先于它添加的列,否则部分版本报 Unknown columnALTER TABLE "table_name"
ADD (
"version" INT DEFAULT NULL,
"is_delete" INT DEFAULT NULL,
"create_by" VARCHAR2(255) DEFAULT NULL,
"update_by" VARCHAR2(255) DEFAULT NULL,
"create_time" DATE DEFAULT NULL,
"update_time" DATE DEFAULT NULL
-- Oracle 中,使用 DATE 存储时间是最常见的,它能存储的最小单位是秒;
-- 如有特殊业务对时间精度要求较高,可使用 TIMESTAMP 类型
-- "create_time" TIMESTAMP(0) DEFAULT NULL, -- 只存到秒,不包含小数秒部分
-- "create_time" TIMESTAMP, -- 默认小数秒精度为 6 位(微秒,最高精度)
-- "create_time" TIMESTAMP(3), -- 精确到毫秒
);
COMMENT ON COLUMN "table_name"."version" IS '乐观锁';
COMMENT ON COLUMN "table_name"."is_delete" IS '是否删除;0:否,1:是';
COMMENT ON COLUMN "table_name"."create_by" IS '创建人';
COMMENT ON COLUMN "table_name"."update_by" IS '修改人';
COMMENT ON COLUMN "table_name"."create_time" IS '创建时间';
COMMENT ON COLUMN "table_name"."update_time" IS '更新时间';-- 达梦数据库通用建表语言
ALTER TABLE table_name ADD COLUMN "version" INT DEFAULT NULL;
COMMENT ON COLUMN table_name."version" IS '乐观锁';
ALTER TABLE table_name ADD COLUMN "is_delete" INT DEFAULT 0;
COMMENT ON COLUMN table_name."is_delete" IS '0--未删除 1--已删除';
ALTER TABLE table_name ADD COLUMN "create_by" VARCHAR(128);
COMMENT ON COLUMN table_name."create_by" IS '创建人';
ALTER TABLE table_name ADD COLUMN "update_by" VARCHAR(128);
COMMENT ON COLUMN table_name."update_by" IS '修改人';
ALTER TABLE table_name ADD COLUMN "create_time" DATETIME(0);
COMMENT ON COLUMN table_name."create_time" IS '创建时间';
ALTER TABLE table_name ADD COLUMN "update_time" DATETIME(0);
COMMENT ON COLUMN table_name."update_time" IS '更新时间';
-- 达梦的 DATE 只包含年月日(除非初始化库时开 Oracle 兼容模式),存时分秒要用 DATETIME/TIMESTAMP;
-- DATETIME(0)/TIMESTAMP(0) 精确到秒,精度可写 0~6,默认 TIMESTAMP(6) 微秒数据库通用 Mapper 语法
通用实体类
import com.baomidou.mybatisplus.annotation.*; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import java.time.LocalDateTime; @Data public class BaseDomain { /** * 主键(使用自定义的 id 生成策略,详见 MP 官网) */ @TableId(value = "id", type = IdType.ASSIGN_ID) private String id; /** * 乐观锁版本号(需注册 OptimisticLockerInnerInterceptor,更新时自动 version + 1 并作为 where 条件) */ @Version private Integer version; /** * 是否删除 0否 1是 */ @TableLogic private Integer isDelete; /** * 创建人 */ @TableField(fill = FieldFill.INSERT) private String createBy; /** * 创建时间 */ @TableField(fill = FieldFill.INSERT) @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime createTime; /** * 修改人 */ @TableField(fill = FieldFill.INSERT_UPDATE) private String updateBy; /** * 更新时间 */ @TableField(fill = FieldFill.INSERT_UPDATE) @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime updateTime; }分页查询
Service// Mybatis-Plus 分页查询示例 @Override public IPage<UserVo> getList(UserDto dto) { Page<User> page = new Page<>(dto.getPageNum(), dto.getPageSize()); return userMapper.selectUserByPage(page, dto); }Mapperpublic interface UserMapper extends BaseMapper<User> { IPage<UserVo> selectUserByPage(IPage<?> page, @Param("dto") UserDto dto); }mapper.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.zjx.UserMapper"> <select id="selectUserByPage" resultType="com.zjx.vo.UserVo"> select u.id, u.name, addr.location, u.update_time from user u left join address addr on u.id = addr.user_id where u.is_delete = 0 <if test="dto.name != null and dto.name != ''"> and u.name like concat('%', #{dto.name}, '%') </if> <if test="dto.userTypes != null and dto.userTypes.size() > 0"> and u.type_id in <foreach collection="dto.userTypes" item="type" open="(" separator="," close=")"> #{type} </foreach> </if> <if test="dto.startTime != null and dto.endTime != null"> and u.update_time between #{dto.startTime} and #{dto.endTime} <!-- and u.update_time >= #{dto.startTime} and u.update_time <= #{dto.endTime} --> </if> -- 排序开始 ORDER BY <choose> <when test="dto.sortType == 0">u.age</when> <when test="dto.sortType == 1">u.age DESC</when> <otherwise>u.update_time DESC</otherwise> </choose> -- 排序结束 </select> </mapper>批量插入
Service// Mybatis-Plus 批量插入示例 @Override public void saveUsers(UserDto dto) { List<User> users = dto.getUsers().stream() .peek(item -> item.setId(snowflakeGenerator.next().toString())) .collect(Collectors.toList()); userMapper.batchInsert(users); }Mapperpublic interface UserMapper extends BaseMapper<User> { void batchInsert(@Param("users") List<User> users); }mapper.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.zjx.UserMapper"> <insert id="batchInsert" parameterType="java.util.List"> insert into user(id, name, age) values <foreach collection="users" item="item" separator=","> ( #{item.id}, #{item.name}, #{item.age} ) </foreach> </insert> </mapper>
Mybatis 联表查询
Mybatis 的 resultMap 在嵌套对象赋值(复杂对象映射)的场景下是其最强大、最适用的地方,嵌套对象数据格式天然契合 RESTful API 的资源关系表达,数据层次清晰,职责明确,十分推荐使用!
一对一嵌套查询
假设 user 表和 user_info 表是一对一关系,现在需要联表查询将所有用户列表返回给前端。对应的 Java 实体类、视图对象、mapper 接口定义如下:
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("user")
public class User {
@TableId
private String id;
private String name;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
public static User create(String name) {
User user = new User();
user.setName(name);
return user;
}
}@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("user_info")
public class UserInfo {
@TableId
private String id;
private String userId;
private String school;
private String phone;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
public static UserInfo create(String userId, String school, String phone) {
UserInfo info = new UserInfo();
info.setUserId(userId);
info.setSchool(school);
info.setPhone(phone);
return info;
}
}@Data
public class UserVO {
private String id;
private String name;
/**
* 嵌套用户信息
*/
private UserInfoVO info;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime updateTime;
}@Data
public class UserInfoVO {
private String id;
private String userId;
private String school;
private String phone;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime updateTime;
}@Mapper
public interface UserMapper extends BaseMapper<User> {
List<UserVO> selectUserListOneToOne();
List<UserVO> selectUserListOneToMany();
}关键在于 mapper.xml 的定义,resultMap 一对一嵌套使用 association 实现,如下:
<?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.zjx.UserMapper">
<!-- autoMapping 属性用于自动映射,若不开启,需要手动为每个属性绑定映射关系;开启后仅绑定主键即可 -->
<resultMap type="com.zjx.vo.UserVO" autoMapping="true" id="UserResult">
<!-- 主键必须显式配置 -->
<id property="id" column="id"/>
<!-- 嵌套的 info 对象 -->
<association property="info" autoMapping="true" javaType="com.zjx.vo.UserInfoVO">
<!-- 属性名与列名不一致的字段需要显式配置 -->
<id property="id" column="info_id"/>
<result property="createTime" column="info_create_time"/>
<result property="updateTime" column="info_update_time"/>
</association>
</resultMap>
<sql id="selectUserVo">
SELECT
u.id,
u.`name`,
u.create_time,
u.update_time,
info.id AS info_id,
info.user_id,
info.school,
info.phone,
info.create_time as info_create_time,
info.update_time as info_update_time
FROM user u
LEFT JOIN user_info info ON u.id = info.user_id
</sql>
<select id="selectUserListOneToOne" resultMap="UserResult">
<include refid="selectUserVo"/>
WHERE u.create_time IS NOT NULL
</select>
</mapper>这样,最终得到的 UserVO 中,既包含了 user 表的所有字段,也包含了 user_info 表的所有字段,且字段名与表名一致,方便前端使用。
提示
聪明的你可能会有疑问,为什么搞这么复杂一定要嵌套呢?直接在 UserVO 里将查询语句中的所有字段定义一下不就玩事儿,还不用去写 resultMap,这样岂不是更简单?
但是仔细思考一下,什么是单一职责原则,如果联表是三张或者更多,所有字段定义在一个 VO 中,且不说十分臃肿,前端也会疑惑,修改用户该传哪个 id 呢,修改 info 该传哪个 id 呢...如果数据层次分明,什么时候该用哪些数据就十分清晰了。
一对多嵌套查询
一对多与一对一的写法几乎一致,差别只有两处:VO 的嵌套字段从对象换成 List,resultMap 的 association 换成 collection。假设 user 表和 orders 表是一对多关系,将所有用户及其订单列表一次性查出:
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("orders")
public class Order {
@TableId
private String id;
private String userId;
private String orderNo;
private BigDecimal amount;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
public static Order create(String userId, String orderNo, BigDecimal amount) {
Order order = new Order();
order.setUserId(userId);
order.setOrderNo(orderNo);
order.setAmount(amount);
return order;
}
}@Data
public class OrderVO {
private String id;
private String orderNo;
private BigDecimal amount;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;
}@Data
public class UserVO {
private String id;
private String name;
/**
* 订单列表(一对多嵌套)
*/
private List<OrderVO> orders;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime updateTime;
}Mapper 接口在前面一对一的 UserMapper 上再加一个 selectUserListOneToMany() 方法即可。resultMap 的关键在 collection,ofType 指定集合的元素类型,注意与 association 的 javaType 区分:
<?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.zjx.UserMapper">
<resultMap type="com.zjx.vo.UserVO" autoMapping="true" id="UserOrderResult">
<!-- 主键必须显式配置 -->
<id property="id" column="id"/>
<!-- 一对多嵌套:ofType 指定集合元素类型,区别于 association 的 javaType -->
<collection property="orders" autoMapping="true" ofType="com.zjx.vo.OrderVO">
<id property="id" column="order_id"/>
</collection>
</resultMap>
<sql id="selectUserOrderVo">
SELECT
u.id,
u.`name`,
o.id AS order_id,
o.order_no,
o.amount
FROM user u
LEFT JOIN orders o ON u.id = o.user_id
</sql>
<select id="selectUserListOneToMany" resultMap="UserOrderResult">
<include refid="selectUserOrderVo"/>
WHERE u.is_delete = 0
</select>
</mapper>LEFT JOIN 会让同一个用户重复出现多行——有几个订单就重复几行,MyBatis 按 resultMap 中的主键 <id> 将这些行折叠成一个 UserVO,订单自动聚合为 orders 列表,这也是主键必须显式配置的原因。没有订单的用户 orders 是空数组而不是 null。最终返回给前端的结构:
[
{
"id": "1",
"name": "张三",
"orders": [
{ "id": "101", "orderNo": "SN202501", "amount": 99.00 },
{ "id": "102", "orderNo": "SN202502", "amount": 199.00 }
]
},
{
"id": "2",
"name": "李四",
"orders": []
}
]N+1 查询
association 和 collection 还有一种嵌套 select 写法:不写 join,MyBatis 拿着主查询的每一行结果,逐条再发一次 SQL 查子表:
<!-- 嵌套 select 写法:column 指定传给子查询的参数列,select 指向另一条语句 -->
<resultMap type="com.zjx.vo.UserVO" autoMapping="true" id="UserOrderSelectResult">
<id property="id" column="id"/>
<collection property="orders" column="id" select="selectOrdersByUserId"/>
</resultMap>
<select id="selectOrdersByUserId" resultType="com.zjx.vo.OrderVO">
select id, order_no as orderNo, amount
from orders
where user_id = #{id}
</select>这种写法 SQL 各自简单直观,但代价是著名的 N+1 问题:1 次主查询 + N 次子查询,50 个用户就是 51 次数据库往返。两种写法的取舍:
| 写法 | SQL 次数 | 适用场景 |
|---|---|---|
| 嵌套 select | 1 + N | 详情页等主记录少的场景 |
| join + 嵌套结果 | 1 | 列表页等记录多的场景(推荐) |
N+1 不是 MyBatis 独有,JPA/Hibernate 的关联抓取同样受困于此,解法思路一致:记录多就 join 一次拿全,记录少、层级深再考虑分步。

