Skip to main content
Version: 1.0

整合 MyBatis

MyBatis 是国内项目最主流的持久层框架:SQL 自己写、映射框架做,既保留 SQL 的完全控制力,又免去 JDBC 样板代码。本篇从整合开始,覆盖注解与 XML 两种写法、动态 SQL、结果映射和分页插件,足以支撑真实项目开发。

第 1 步:引入依赖​

<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.4</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>

这个 starter 同时包含数据源整合(HikariCP),不需要再单独引 spring-boot-starter-jdbc。

第 2 步:配置​

spring:
datasource:
url: jdbc:mysql://localhost:3306/demo?serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: ${DB_PASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
mapper-locations: classpath:mapper/*.xml # XML 映射文件位置
type-aliases-package: com.example.demo.entity # 实体类别名包
configuration:
map-underscore-to-camel-case: true # 下划线转驼峰:user_name → userName
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl # 打印 SQL(生产关闭)

map-underscore-to-camel-case 必开,否则数据库 create_time 映射不到 Java createTime。

Mapper 的两种写法​

写法一:注解(简单 SQL)​

@Mapper
public interface UserMapper {

@Select("SELECT * FROM user WHERE id = #{id}")
User selectById(Long id);

@Insert("INSERT INTO user(username, email) VALUES (#{username}, #{email})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);
}

写法二:XML 映射文件(复杂 SQL,推荐)​

接口只留方法签名:

@Mapper
public interface OrderMapper {

Order selectWithUser(Long id);

List<Order> search(@Param("query") OrderQuery query);

int batchInsert(@Param("list") List<Order> orders);
}

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.demo.mapper.OrderMapper">

<!-- 结果映射:处理字段名不一致 / 关联对象 -->
<resultMap id="OrderWithUserMap" type="com.example.demo.entity.Order">
<id property="id" column="id"/>
<result property="orderNo" column="order_no"/>
<result property="amount" column="amount"/>
<association property="user" javaType="com.example.demo.entity.User">
<id property="id" column="user_id"/>
<result property="username" column="username"/>
</association>
</resultMap>

<!-- 关联查询:一单查出订单 + 用户 -->
<select id="selectWithUser" resultMap="OrderWithUserMap">
SELECT o.id, o.order_no, o.amount,
u.id AS user_id, u.username
FROM `order` o
JOIN `user` u ON u.id = o.user_id
WHERE o.id = #{id}
</select>

<!-- 动态条件查询 -->
<select id="search" resultType="com.example.demo.entity.Order">
SELECT * FROM `order`
<where>
<if test="query.orderNo != null and query.orderNo != ''">
AND order_no = #{query.orderNo}
</if>
<if test="query.minAmount != null">
AND amount &gt;= #{query.minAmount}
</if>
<if test="query.statusList != null and query.statusList.size() > 0">
AND status IN
<foreach collection="query.statusList" item="s" open="(" separator="," close=")">
#{s}
</foreach>
</if>
</where>
ORDER BY id DESC
</select>

<!-- 批量插入 -->
<insert id="batchInsert">
INSERT INTO `order`(order_no, user_id, amount) VALUES
<foreach collection="list" item="o" separator=",">
(#{o.orderNo}, #{o.userId}, #{o.amount})
</foreach>
</insert>
</mapper>

关键点:

  • namespace 必须是接口全限定名;id 必须与方法名一致。
  • XML 中 < 要转义为 &lt;,>= 写作 &gt;=。
  • XML 文件必须能被 mapper-locations 扫描到。

# 与 $ 的区别(面试高频)​

写法机制安全性用途
#{name}预编译占位符(PreparedStatement 的 ?)安全,防注入一切参数值
${name}字符串直接拼接有注入风险仅用于动态表名、排序字段等无法预编译的场景
-- 错误:ORDER BY #{column} 预编译后变成 ORDER BY 'id',排序失效
-- 正确:排序字段用 ${},但必须由后端白名单控制,禁止直接传前端输入
ORDER BY ${orderColumn} ${orderDir}

动态 SQL 标签速查​

标签作用
<if test="">条件包含
<where>智能 WHERE:自动去掉多余 AND/OR
<set>智能 SET:自动去掉末尾逗号
<foreach>遍历集合:IN 查询、批量插入
<choose>/<when>/<otherwise>多分支(相当于 if-else if-else)
<trim>自定义前后缀裁剪(where/set 的底层)
<sql> + <include>抽取公共 SQL 片段复用

分页:PageHelper 插件​

手写 LIMIT offset, size + COUNT(*) 太啰嗦,PageHelper 一行解决:

<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
public PageInfo<Order> list(OrderQuery query, int page, int size) {
// 只对紧跟其后的第一条查询生效
PageHelper.startPage(page, size);
List<Order> list = orderMapper.search(query);
return new PageInfo<>(list); // 含 total、pages、list 等
}

原理:通过 MyBatis 拦截器在 SQL 上自动追加 LIMIT,并自动执行 COUNT(*)。

MyBatis-Plus(进阶可选)​

真实项目大量使用 MyBatis-Plus,它在 MyBatis 之上提供:

  • 单表 CRUD 零 SQL:userMapper.selectById(1)、insert(entity) 开箱即用
  • 条件构造器:new QueryWrapper<User>().like("username", "tom").gt("age", 18)
  • 内置分页插件、逻辑删除、自动填充(create_time 等)
public interface UserMapper extends BaseMapper<User> {
// 单表增删改查全部继承获得,复杂 SQL 仍走 XML
}

适合「80% 单表操作 + 20% 复杂 SQL」的典型业务项目。

调试技巧​

# 打印完整 SQL 与参数
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
logging:
level:
com.example.demo.mapper: debug

控制台输出示例:

==> Preparing: SELECT * FROM user WHERE id = ?
==> Parameters: 1(Long)
<== Total: 1