项目实战

SpringBoot MyBatis 使用

Created: 8/21/2025 23:39
Updated: 9/1/2025 23:22
Exported: 9/1/2025 23:28
Link: https://chatgpt.com/c/68a73d70-355c-8330-a12b-671f82c5cf39

Prompt:springboot使用mybatis时什么时候要加resultType

Response:

Spring Boot + MyBatis 开发中,resultType 一般是用在 Mapper XML 的 <select> 语句中,作用是告诉 MyBatis 查询结果要映射成什么 Java 类型。是否要加 resultType,取决于你查询的返回结果和返回类型的对应关系。

我帮你总结下常见场景:


✅ 需要加 resultType 的情况

  1. 返回简单类型(基本类型或包装类、String)
    1
    2
    3
    <select id="countUsers" resultType="int">
    SELECT COUNT(*) FROM user
    </select>
    → 因为返回的是单个数值,必须指定 resultType
  2. 返回 Map(键值对)
    1
    2
    3
    <select id="getUserAsMap" resultType="map">
    SELECT id, name FROM user WHERE id = #{id}
    </select>
    → 每行结果会映射成一个 Map
  3. 返回 POJO,但没有写 resultMap,只靠字段名映射
    1
    2
    3
    <select id="getUserById" resultType="com.example.demo.User">
    SELECT id, name, age FROM user WHERE id = #{id}
    </select>
    → MyBatis 会自动根据列名和属性名对应关系封装。

🚫 不需要加 resultType 的情况

  1. 你在 XML 中使用了 <resultMap>
    1
    2
    3
    4
    5
    6
    7
    8
    <resultMap id="UserMap" type="com.example.demo.User">
    <id column="id" property="id"/>
    <result column="name" property="name"/>
    <result column="age" property="age"/>
    </resultMap>
    <select id="getUserById" resultMap="UserMap">
    SELECT id, name, age FROM user WHERE id = #{id}
    </select>
    → 已经显式用 resultMap 定义映射规则,就不用 resultType
  2. 你用的是注解方式(@Select 等)并且返回类型通过方法签名已确定
    1
    2
    @Select("SELECT id, name, age FROM user WHERE id = #{id}")
    User getUserById(int id);
    → 方法返回值是 User,不需要 resultType

💡 规则总结

  • 简单结果(基本类型、字符串、Map、POJO,且字段名和属性名一致):用 resultType
  • 复杂映射(字段名和属性名不一致、多表联合查询、需要定制映射):用 resultMap
  • 注解方式:一般不用 resultType,MyBatis 会根据返回值类型自动映射。

要不要我帮你画一个 “什么时候用 resultType / resultMap” 的判断流程图,让你一眼就能看出来?

Prompt:

实体中有一个status属性班级状态,显示为:未开班、已结课、在读中 这三种。
如果:

  • 当前时间 > 结课时间:状态未 已结课。
  • 当前时间 < 开课时间:状态未 未开班。
  • 否则,就是 在读中。但是表中没有这个字段要怎么办

Response:

你这个场景其实就是 数据库表里没有存储 status 字段,而是需要根据开课时间、结课时间动态计算。这种需求很常见,做法主要有两类:


✅ 方案一:实体类里加 非持久化字段(推荐)

在实体 Class 里定义一个 status 字段,但标记为不映射数据库列:

1
2
3
4
5
6
7
8
9
10
@Data
public class ClassEntity {
private Long id;
private LocalDateTime startTime;
private LocalDateTime endTime;

// 数据库没有这个字段,用于业务计算
@TableField(exist = false)
private String status;
}

然后在 Service 层Mapper 查询结果处理 时计算:

1
2
3
4
5
6
7
8
9
10
public void fillStatus(ClassEntity clazz) {
LocalDateTime now = LocalDateTime.now();
if (now.isBefore(clazz.getStartTime())) {
clazz.setStatus("未开班");
} else if (now.isAfter(clazz.getEndTime())) {
clazz.setStatus("已结课");
} else {
clazz.setStatus("在读中");
}
}

查询列表时可以统一处理:

1
2
List<ClassEntity> list = classMapper.selectList(null);
list.forEach(this::fillStatus);

✅ 方案二:SQL 里直接计算(如果想少写 Java 逻辑)

在 Mapper XML 里用 CASE WHEN 计算 status 并返回:

1
2
3
4
5
6
7
8
9
10
11
12
<select id="selectClassList" resultType="com.example.demo.ClassEntity">
SELECT
id,
start_time,
end_time,
CASE
WHEN NOW() < start_time THEN '未开班'
WHEN NOW() > end_time THEN '已结课'
ELSE '在读中'
END AS status
FROM class
</select>

这样查询出来的实体 status 就有值了,前端直接显示即可。

⚠️ 注意:这种写法需要 ClassEntity 里有 status 字段(但数据库表里可以没有,因为 SQL 已经返回了这个别名字段)。


✅ 方案三:视图(不常用)

如果业务大量依赖这个 status,也可以在数据库里建一个 视图 view,视图里包含计算好的 status 字段,Mapper 查询视图即可。


📌 总结

  • 如果只是在业务层用到,推荐方案一(Java 层计算,灵活且数据库无侵入)
  • 如果查询出来就要直接显示,推荐方案二(SQL 里用 CASE WHEN 计算,返回 status 别名字段)
  • 如果是全局需求,方案三(视图)也可考虑

Prompt:CASE WHEN常见使用场景和使用方法

Response:

好问题 👍,CASE WHEN 在 SQL 中是非常常见的条件判断语句,相当于 SQL 里的 if-else
我帮你总结下 常见使用场景具体用法,同时给你一些例子。


🔹 CASE WHEN 的基本语法

1
2
3
4
5
6
CASE
WHEN condition1 THEN result1
WHEN condition2 THEN result2
...
ELSE default_result
END

👉 说明:

  • WHEN ... THEN ...:条件成立返回对应结果
  • ELSE:所有条件都不满足时的返回值(可省略,省略则返回 NULL
  • END:结束

🔹 常见使用场景

1. 字段值映射(字典/枚举翻译)

比如数据库里存储的是数字,前端需要显示文字:

1
2
3
4
5
6
7
8
9
10
SELECT 
id,
name,
CASE status
WHEN 0 THEN '未开班'
WHEN 1 THEN '在读中'
WHEN 2 THEN '已结课'
ELSE '未知'
END AS status_name
FROM class;

👉 相当于枚举值翻译。


2. 动态分组统计

统计不同区间的人数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SELECT 
CASE
WHEN age < 18 THEN '未成年'
WHEN age BETWEEN 18 AND 59 THEN '成年人'
ELSE '老年人'
END AS age_group,
COUNT(*) AS cnt
FROM user
GROUP BY
CASE
WHEN age < 18 THEN '未成年'
WHEN age BETWEEN 18 AND 59 THEN '成年人'
ELSE '老年人'
END;

👉 按年龄段分组统计人数。


3. 条件聚合(在 SELECT 里做分类统计)

1
2
3
4
SELECT
SUM(CASE WHEN gender = 'M' THEN 1 ELSE 0 END) AS male_count,
SUM(CASE WHEN gender = 'F' THEN 1 ELSE 0 END) AS female_count
FROM user;

👉 相当于把一列数据拆成多列统计。


4. 条件排序

根据不同条件自定义排序规则:

1
2
3
4
5
6
7
8
SELECT id, name, status
FROM class
ORDER BY
CASE status
WHEN '在读中' THEN 1
WHEN '未开班' THEN 2
WHEN '已结课' THEN 3
END;

👉 控制排序优先级,而不是按字典序。


5. 时间状态判断(很常用)

课程状态计算:

1
2
3
4
5
6
7
8
9
SELECT
id,
name,
CASE
WHEN NOW() < start_time THEN '未开班'
WHEN NOW() > end_time THEN '已结课'
ELSE '在读中'
END AS status
FROM class;

👉 类似你刚才问的场景。


6. 避免 NULL(默认值替代)

1
2
3
4
5
6
7
8
SELECT 
id,
name,
CASE
WHEN phone IS NULL THEN '暂无手机号'
ELSE phone
END AS phone
FROM user;

👉 对 NULL 做友好处理。


🔹 小技巧

  1. 可以嵌套使用
    1
    2
    3
    4
    CASE WHEN a > 10 
    THEN CASE WHEN b > 5 THEN 'X' ELSE 'Y' END
    ELSE 'Z'
    END
  2. 可用于 WHERE 子句(但不推荐,逻辑容易乱):
    1
    2
    3
    4
    5
    6
    SELECT * FROM orders
    WHERE
    CASE
    WHEN status = '已付款' THEN 1
    ELSE 0
    END = 1;
  3. 最好别滥用:简单的判断推荐交给 Java 业务层处理,SQL 里主要用在 分组、聚合、统计、展示翻译 这些场景。

Prompt:

为什么 when now()<c.begin_date then ‘未开班’报错Element c.begin_date is not allowed here:

4/4

Response:

我帮你分析下这个问题 👇

报错信息:

1
Element c.begin_date is not allowed here

这是 MyBatis 在解析 XML 的时候 报出来的,并不是 SQL 本身的错误。
原因是:MyBatis 的 XML 标签里,<> 会被 XML 解析器当作标签符号,导致解析失败。

在你的 SQL 里有:

1
when now()<c.begin_date then '未开班'

XML 解析器看到 <c.begin_date 以后,会以为是个新的标签 <c...>,于是报错。


✅ 解决办法:用转义字符

在 MyBatis 的 XML 里,所有比较符号要写成转义形式:

  • <&lt;
  • >&gt;

所以你的 SQL 改成这样就对了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<select id="list" resultType="com.example.pojo.Clazz">
select c.*, d.name masterName,
(case
when now() &gt; c.end_date then '已结课'
when now() &lt; c.begin_date then '未开班'
else '在读中'
end) status
from clazz c left join dept d on c.master_id = d.id
<where>
<if test="name != null and name != ''">
and c.name like concat('%',#{name},'%')
</if>
<if test="begin != null and end != null">
and c.begin_date between #{begin} and #{end}
</if>
</where>
order by update_time desc
</select>

🔹 补充说明

这是 MyBatis 里非常常见的坑:

  • 写在 <select><if><where> 等 XML 标签里的 SQL,都会先经过 XML 解析器解析,所以 < > 必须写成转义符
  • 如果你用的是 注解方式@Select),就不需要转义,直接写 < > 就行,因为注解不是 XML。

Prompt:自定义异常 + 全局异常处理器实现错误信息提示

Response:

Spring Boot 项目里,实现「自定义异常 + 全局异常处理器」是一个非常常见的做法,用来统一管理错误提示。

我给你一个完整的实现思路和代码示例:


🔹 1. 定义自定义异常类

通常我们会定义一个 业务异常,继承 RuntimeException,携带错误码和错误信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class BusinessException extends RuntimeException {
private Integer code; // 错误码
private String message; // 错误信息

public BusinessException(Integer code, String message) {
super(message);
this.code = code;
this.message = message;
}

public Integer getCode() {
return code;
}

@Override
public String getMessage() {
return message;
}
}

🔹 2. 定义统一返回结果类

方便前端收到一致的 JSON 格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import lombok.Data;

@Data
public class ApiResponse<T> {
private Integer code; // 状态码
private String message; // 提示信息
private T data; // 返回数据

public static <T> ApiResponse<T> success(T data) {
ApiResponse<T> res = new ApiResponse<>();
res.setCode(200);
res.setMessage("成功");
res.setData(data);
return res;
}

public static <T> ApiResponse<T> fail(Integer code, String message) {
ApiResponse<T> res = new ApiResponse<>();
res.setCode(code);
res.setMessage(message);
return res;
}
}

🔹 3. 定义全局异常处理器

@RestControllerAdvice + @ExceptionHandler 来捕获异常,返回统一的 JSON。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

// 处理业务异常
@ExceptionHandler(BusinessException.class)
public ApiResponse<?> handleBusinessException(BusinessException ex) {
return ApiResponse.fail(ex.getCode(), ex.getMessage());
}

// 处理其他未知异常
@ExceptionHandler(Exception.class)
public ApiResponse<?> handleException(Exception ex) {
// 打印日志方便排查
ex.printStackTrace();
return ApiResponse.fail(500, "服务器内部错误,请联系管理员");
}
}

🔹 4. 使用示例

在 Service 或 Controller 里,你可以直接抛出 BusinessException

1
2
3
4
5
6
7
8
@Service
public class ClassService {
public void checkClassStatus(LocalDateTime start, LocalDateTime end) {
if (end.isBefore(start)) {
throw new BusinessException(400, "结课时间不能早于开课时间");
}
}
}

在 Controller 里调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
@RequestMapping("/class")
public class ClassController {

@Autowired
private ClassService classService;

@GetMapping("/check")
public ApiResponse<String> check() {
classService.checkClassStatus(
LocalDateTime.now().plusDays(1),
LocalDateTime.now().minusDays(1)
);
return ApiResponse.success("ok");
}
}

前端收到的 JSON 错误信息:

1
2
3
4
5
{
"code": 400,
"message": "结课时间不能早于开课时间",
"data": null
}

🔹 5. 总结

  • 自定义异常:携带业务错误信息
  • 全局异常处理器:统一拦截,返回 JSON
  • 统一返回格式:前端不用关心后端具体抛的什么异常,始终能拿到 code + message

Prompt:

org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException:

Error updating database. Cause: java.lang.IllegalArgumentException: invalid comparison: java.time.LocalDate and java.lang.String

The error may exist in com/example/mapper/StudentMapper.xml

The error may involve com.example.mapper.StudentMapper.updateById

The error occurred while executing an update

Cause: java.lang.IllegalArgumentException: invalid comparison: java.time.LocalDate and java.lang.String

2/2

Response:

这个报错关键信息是:

1
invalid comparison: java.time.LocalDate and java.lang.String

👉 说明 MyBatis 在执行 update 时,LocalDate 类型和数据库中的 String 类型对比/赋值时不匹配


🔍 常见原因

  1. 数据库字段类型和实体字段类型不匹配
    • 实体类用的是 LocalDate
    • 数据库列可能是 varchar / char / text(字符串类型),而不是 datedatetime
      ⚠️ MyBatis 在执行参数设置时,发现 LocalDate 无法自动转换为 String,就报错了。

  1. SQL 写法导致的隐式比较
    比如 XML 里写了类似:
    1
    2
    3
    <if test="beginDate != null">
    and begin_date = #{beginDate}
    </if>
    如果 begin_date 在数据库是字符串类型,而 beginDateLocalDate,就会触发 LocalDate vs String 的比较错误。

  1. MyBatis 类型处理器没生效
    MyBatis 默认对 java.time.LocalDate 有支持,但前提是数据库字段类型要是 DATE
    如果数据库字段是 VARCHAR,它不知道怎么把 LocalDate 自动转成字符串。

✅ 解决办法

方案一:改数据库字段类型(推荐 ✅)

  • 如果字段确实表示日期,数据库字段改为 DATEDATETIME
  • 这样 MyBatis + JDBC 就能自动把 LocalDate 映射到 DATE 了。
1
ALTER TABLE student MODIFY begin_date DATE;

方案二:在实体类改字段类型

  • 如果数据库就是 VARCHAR 存储日期,那实体类里字段也改成 String
  • 逻辑层需要转换时自己用 LocalDate.parse(str)
1
private String beginDate;  // 实体类保持和数据库一致

方案三:自定义 TypeHandler(不改表结构也不改实体)

如果你希望数据库用 VARCHAR 存日期,但 Java 里用 LocalDate,可以写一个自定义 TypeHandler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(LocalDate.class)
public class LocalDateTypeHandler extends BaseTypeHandler<LocalDate> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, LocalDate parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, parameter.toString()); // LocalDate -> String
}

@Override
public LocalDate getNullableResult(ResultSet rs, String columnName) throws SQLException {
String dateStr = rs.getString(columnName);
return dateStr != null ? LocalDate.parse(dateStr) : null;
}

@Override
public LocalDate getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String dateStr = rs.getString(columnIndex);
return dateStr != null ? LocalDate.parse(dateStr) : null;
}

@Override
public LocalDate getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String dateStr = cs.getString(columnIndex);
return dateStr != null ? LocalDate.parse(dateStr) : null;
}
}

然后在 application.yml 里注册:

1
2
mybatis:
type-handlers-package: com.example.handler

📌 总结

  • 最根本的原因:Java 字段和数据库字段类型不一致(LocalDate vs String)
  • 最佳做法:改数据库字段为 DATE/DATETIME,和 LocalDate 保持一致
  • 如果不能改表结构,就用 TypeHandler实体字段用 String 来解决。

Prompt:总结

Response:

当然可以 👍,我帮你梳理一下你这几轮问答里学到的知识点:

  1. MyBatis 映射
    • resultType 用于简单结果映射(基本类型、POJO、Map),而 resultMap 适合复杂映射(字段名和属性名不一致、多表查询)。
    • 在 XML 中写 <> 要用转义 &lt;&gt;,否则会被解析成标签。
  2. CASE WHEN 的使用
    • 可以做字段值翻译(枚举映射)、动态分组、条件聚合、条件排序、时间状态判断、避免 NULL。
    • 写法相当于 SQL 里的 if-else
  3. 动态状态计算
    • 如果表里没有 status 字段,可以在 Java 实体类加 @TableField(exist = false) 非持久化字段,在 Service 层计算;
    • 或者在 SQL 中用 CASE WHEN 直接生成 status
  4. 异常处理
    • 定义自定义异常类(带错误码和信息),结合全局异常处理器(@RestControllerAdvice + @ExceptionHandler)统一返回 JSON。
    • 这样前端能拿到统一格式的错误提示。
  5. 类型映射问题
    • MyBatis 报 LocalDate vs String 错误,原因是 Java 字段和数据库字段类型不一致。
    • 解决方案:
      • 改数据库字段为 DATE/DATETIME
      • 或实体字段改为 String
      • 或写自定义 TypeHandler 处理 LocalDateVARCHAR 的转换。

👉 简单一句话总结:
你学会了 MyBatis 查询结果映射的正确用法、SQL 里 CASE WHEN 的典型应用、如何处理表中不存在的计算字段、Spring Boot 全局异常处理的实现,以及 MyBatis 与 Java/数据库类型不匹配时的解决思路