MyBatis 解决什么问题
最早用 JDBC 写查询,代码大概是这样:加载驱动、拿连接、拼 SQL、设置参数、执行、遍历 ResultSet、手动映射字段、关资源。一套下来几十行,还容易漏关连接。MyBatis 把这些重复劳动接了过去:SQL 还是你写,但参数绑定、结果映射、连接管理都由框架完成。
它和 JPA 的思路不同:MyBatis 不隐藏 SQL,复杂查询、多表关联都能精确控制,所以国内业务系统里用得非常多。
MyBatis 的核心对象是 SqlSessionFactory,它负责创建 SqlSession;SqlSession 才是执行 SQL、管理事务的入口。Spring 集成后这些对象都由容器管理,业务代码只接触 Mapper 接口,而 Mapper 没有实现类,是 MyBatis 用动态代理生成的。排查问题时知道底层发生了什么,很有帮助。
集成与配置
Spring Boot 2.7 对应 MyBatis Starter 2.2.x。依赖加好后,在 application.yml 里配数据源和 MyBatis:
spring:
datasource:
url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.demo.entity
configuration:
map-underscore-to-camel-case: true
MySQL 8 的驱动类是 com.mysql.cj.jdbc.Driver,老教程里的 com.mysql.jdbc.Driver 已经废弃。map-underscore-to-camel-case 打开后,数据库的 created_at 会自动映射到实体的 createdAt,能省掉大量 resultMap。
XML 映射与注解
接口加 @Mapper,方法名和 XML 里的 id 一一对应:
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface UserMapper {
User selectById(Long id);
List<User> selectByName(@Param("name") String name);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper">
<select id="selectById" resultType="com.example.demo.entity.User">
select id, name, created_at
from t_user
where id = #{id}
</select>
<select id="selectByName" resultType="com.example.demo.entity.User">
select id, name, created_at
from t_user
where name like concat('%', #{name}, '%')
</select>
</mapper>
namespace 必须是接口的全限定名,id 必须和方法名相同,两个条件缺一个就会报 Invalid bound statement (not found)。
插入后要拿自增主键,在 <insert> 上加 useGeneratedKeys="true" keyProperty="id",比插入完再查一次快,也不会因为并发拿到别人的 ID。
简单 SQL 也可以直接用注解,不用建 XML:
@Select("select id, name, created_at from user where id = #{id}")
User selectById(Long id);
判断标准很朴素:一条不会变的简单 SQL 用注解,有动态条件(<if>、<foreach>)、字段多、需要复用 resultMap 的,老老实实写 XML。把几十行动态 SQL 塞进注解字符串,维护起来是灾难。
resultMap 与联表
字段名和属性名对不上时,除了开驼峰映射,还可以用 resultMap 显式指定:
<resultMap id="userMap" type="com.example.demo.entity.User">
<id column="id" property="id"/>
<result column="user_name" property="name"/>
<result column="created_at" property="createdAt"/>
</resultMap>
<select id="selectById" resultMap="userMap">
select id, user_name, created_at from t_user where id = #{id}
</select>
<id> 标记主键,<result> 标记普通字段。联表查询时,可以用 <association> 映射一对一,用 <collection> 映射一对多。写之前先想清楚:是真的需要联表,还是可以拆成两次查询再在内存里组装。前者 SQL 复杂但只有一次网络往返,后者代码清晰、更容易复用缓存。
一对多最容易踩 N+1:循环查主表,再对每条记录查一次子表,数据一多就是几百次查询。用 <collection> 一次查出来,或者先查主表拿到 ID 列表,再用 in 查子表,都比循环查强。
联表结果字段容易重名,比如两张表都有 id、created_at。用别名区分(u.id as user_id),再配合 resultMap 映射,否则后面的字段会覆盖前面的。
常见坑
1. XML 没被打包进 classpath
mapper-locations 写的是 classpath:mapper/*.xml,文件就要放在 src/main/resources/mapper 下。放在 src/main/java 里的 XML 默认不会进 target/classes,除非在 pom.xml 里配置 resources 包含 **/*.xml。
2. 多参数没加 @Param
方法有两个以上参数时,XML 里写 #{name} 可能报 Parameter 'name' not found。JDK 8 编译时不一定保留了参数名,最稳的写法是每个参数都加 @Param。
3. 查询结果字段是 null
先看 map-underscore-to-camel-case 有没有开,再看 SQL 里有没有查这一列。别用 select *,字段一多、类型一变就容易踩坑,显式列出需要的列更清楚。
4. 单条查询返回 null 当成异常
selectById 查不到返回 null,调用方要判空;列表查询 selectList 查不到返回的是空集合,不是 null,不要写 if (list == null) 而漏了 isEmpty()。
5. Mapper 没被扫描到
启动类上没加 @MapperScan("com.example.demo.mapper"),接口也没加 @Mapper,注入时就会报找不到 Bean。两种方式选一种,团队里统一即可。
排查步骤
- 报
Invalid bound statement:核对namespace、方法名、mapper-locations三处; - 打开 SQL 日志:
logging.level.com.example.demo.mapper=debug,确认实际执行的 SQL; - 字段为 null:对比 SQL 列名和实体属性名,确认驼峰映射;
- 连接失败:单独用
mysql -h -u -p验证账号密码、库名和网络; - 参数报错:给所有多参数方法补
@Param,再重启验证。
评论