MyBatis 基础入门与 CRUD 详解
定位:MyBatis 系列第 1 篇:快速入门、全局配置、Mapper 映射、参数传递、主键回填与基础 CRUD 适用版本:MyBatis 3.5.x(JDK 8+)
目录
一、MyBatis 概述
1.1 历史演进
MyBatis 的前身是 iBATIS,名称来源于 “Internet” 与 “Abatis”(鹿砦,一种防御工事)的组合,寓意「互联网的持久层防线」。
| 2002 | Clinton Begin 开源 iBATIS 项目 |
| 2004 | 项目捐赠给 Apache 基金会,更名 Apache iBATIS |
| 2010 | 项目从 Apache 迁出,更名 MyBatis;同年发布 MyBatis 3,引入 Mapper 接口 + 动态代理的新架构 |
| 2016 | MyBatis 3.4.x:注解中支持 <script> 动态 SQL,原生支持 Java 8 日期时间类型(JSR-310) |
| 2019 | MyBatis 3.5.x 发布,最低要求 JDK 8;3.5.x 系列持续维护至今 |
生态版图:
| MyBatis 核心框架 | SQL 映射引擎本体(本篇开始讲解) |
| MyBatis-Spring | Spring 整合(见系列 06 篇) |
| MyBatis-Spring-Boot-Starter | Spring Boot 自动装配(见系列 06 篇) |
| MyBatis-Plus | 国产增强工具:免 SQL 单表 CRUD(见系列 07 篇) |
| PageHelper | 第三方分页插件(原理见系列 05 篇) |
1.2 定位:半自动化 ORM
ORM(对象关系映射)框架按「SQL 由谁控制」分为两类:
| 全自动 ORM | Hibernate、JPA 实现(EclipseLink 等) | 框架根据对象模型自动生成 SQL | 开发效率高;复杂 SQL 难以精细控制 |
| 半自动 ORM | MyBatis | 开发者手写 SQL,框架负责参数映射与结果映射 | SQL 完全可控、便于优化;映射代码由框架托管 |
MyBatis 官方文档对其定位是「支持自定义 SQL、存储过程和高级映射的持久层框架」。它消除了几乎全部 JDBC 样板代码:
// 裸 JDBC:约 20 行样板代码
Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement("SELECT id, name FROM user WHERE id = ?");
ps.setLong(1, 1L);
ResultSet rs = ps.executeQuery();
User user = null;
if (rs.next()) {
user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
}
rs.close(); ps.close(); conn.close(); // 还有异常处理、事务……
// MyBatis:一行调用 + 一份映射文件
User user = userMapper.selectById(1L);
1.3 同类产品对比与选型
| 抽象层级 | 半自动 ORM | 全自动 ORM | JDBC 薄封装 | 轻量 ORM |
| SQL 控制 | 手写,完全可控 | HQL/JPQL + 自动生成 | 手写 | 部分自动生成 |
| 对象映射 | resultMap 显式/自动驼峰 | 注解实体全自动映射 | 手写 RowMapper | 注解实体映射 |
| 缓存 | 一级/二级缓存 | 一级/二级缓存 + 查询缓存 | 无 | 无 |
| 学习曲线 | 平缓(会 SQL 即可入门) | 较陡(需理解持久化上下文) | 极低 | 低 |
| 典型场景 | 复杂查询多、SQL 需 DBA 审查优化 | 领域模型稳定、CRUD 密集 | 简单项目、临时脚本 | 聚合根模型的轻量持久化 |
选型经验:互联网业务系统中表结构变更频繁、查询复杂度高、需要精细 SQL 调优,MyBatis 是国内主流选择;若领域模型稳定且以单实体 CRUD 为主,JPA 效率更高。二者并非互斥,同一系统可混合使用。ORM 通用原理与选型方法论见《01-ORM框架知识点详解》。
1.4 核心价值
二、快速入门与 SqlSession 使用
2.1 最小运行环境
脱离 Spring 的原生 MyBatis 需要四样东西:依赖、全局配置、Mapper XML、实体类。
<!– pom.xml –>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.16</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.4.0</version>
</dependency>
实体类与表结构对应:
public class User {
private Long id;
private String name;
private String email;
private LocalDateTime createTime;
// getter/setter 省略
}
2.2 核心 API 两件套
SqlSessionFactoryBuilder ──解析配置──→ SqlSessionFactory ──openSession──→ SqlSession
(应用级单例、重量级) (非线程安全、用完即关)
| SqlSessionFactoryBuilder | 用完即弃 | 否 | 构建完 Factory 后可丢弃 |
| SqlSessionFactory | 应用级单例 | 是 | 全局一个,重复创建是典型反模式 |
| SqlSession | 请求/会话级 | 否 | 必须 try-with-resources 关闭,禁止作为成员变量共享 |
2.3 原生使用完整流程
public class MyBatisDemo {
private static final SqlSessionFactory FACTORY;
static {
try (InputStream in = Resources.getResourceAsStream("mybatis-config.xml")) {
FACTORY = new SqlSessionFactoryBuilder().build(in);
} catch (IOException e) {
throw new ExceptionInInitializerError(e);
}
}
public static void main(String[] args) {
// 查询
try (SqlSession session = FACTORY.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.selectById(1L);
System.out.println(user.getName());
}
// 写操作:openSession() 默认 autoCommit=false,需手动 commit
try (SqlSession session = FACTORY.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
User u = new User();
u.setName("张三");
u.setEmail("zhangsan@example.com");
mapper.insert(u);
session.commit();
System.out.println("新主键:" + u.getId()); // 已回填
}
}
}
要点:
- session.getMapper(UserMapper.class) 返回的是 JDK 动态代理对象,Mapper 接口无需实现类(代理原理见系列 03 篇)。
- 原生模式下写操作不提交不生效;Spring 整合后由事务管理器托管,无需手动 commit(见系列 06 篇)。
- SqlSession 还提供更底层的门面 API(selectOne/selectList/insert/update/delete,以 statementId 调用),实际开发统一走 Mapper 接口。
三、全局配置基础
3.1 mybatis-config.xml 结构
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!– 元素顺序固定:properties → settings → typeAliases → typeHandlers
→ objectFactory → plugins → environments → mappers,乱序会解析报错 –>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<setting name="logImpl" value="SLF4J"/>
</settings>
<typeAliases>
<package name="com.example.entity"/>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/demo"/>
<property name="username" value="root"/>
<property name="password" value="******"/>
</dataSource>
</environment>
</environments>
<mappers>
<package name="com.example.mapper"/>
</mappers>
</configuration>
3.2 environments 环境配置
- default 属性决定激活哪个 environment(如 development/production)。
- transactionManager:
- JDBC:MyBatis 直接调用 Connection 的 commit/rollback;
- MANAGED:事务由容器(如 Spring、应用服务器)托管,MyBatis 不提交。
- dataSource 类型:
| UNPOOLED | 每次新建连接,无池化 | 仅测试 |
| POOLED | 内置连接池(poolMaximumActiveConnections 默认 10 等参数可调) | 简单应用 |
| JNDI | 从应用服务器 JNDI 查找数据源 | 传统 JavaEE |
生产实践:以上三种都会被 Druid/HikariCP 等专业连接池替代——通过 Spring 整合时 DataSource 由 Spring 管理,mybatis-config.xml 中的 environments 整节不再需要(见系列 06 篇)。
3.3 常用 settings(入门必知项)
| mapUnderscoreToCamelCase | false | 下划线列名自动转驼峰属性,强烈建议开启,否则 create_time 映射不到 createTime |
| useGeneratedKeys | false | 全局允许主键回填 |
| cacheEnabled | true | 二级缓存总开关(生产建议关闭,见系列 04 篇) |
| lazyLoadingEnabled | false(3.4.1+) | 嵌套查询延迟加载全局开关(见 02 篇) |
| logImpl | 自动探测 | 指定日志实现;开发排障可临时设 STDOUT_LOGGING 打印 SQL |
| defaultExecutorType | SIMPLE | SIMPLE/REUSE/BATCH(原理见 03 篇) |
3.4 typeAliases 类型别名
<typeAliases>
<typeAlias type="com.example.entity.User" alias="user"/>
<package name="com.example.entity"/> <!– 批量注册:别名=类名首字母小写,@Alias 可自定义 –>
</typeAliases>
作用仅是简化 XML 书写:resultType="user" 等价于 resultType="com.example.entity.User"。基本类型别名(int、string、map、list 等)内置可用且大小写不敏感。
3.5 mappers 映射注册
<mappers>
<mapper resource="mapper/UserMapper.xml"/> <!– 类路径文件 –>
<mapper url="file:///opt/app/mapper/OrderMapper.xml"/><!– 绝对路径 –>
<mapper class="com.example.mapper.UserMapper"/> <!– 注解 Mapper –>
<package name="com.example.mapper"/> <!– 包扫描:要求 XML 与接口同名同包 –>
</mappers>
<package> 方式的约定:com.example.mapper.UserMapper 接口与 com/example/mapper/UserMapper.xml 同包同名,自动配对。Spring Boot Starter 下由 mybatis.mapper-locations 替代(见 06 篇)。
四、Mapper 映射方式
4.1 XML 映射(主流)
约束两条:namespace 必须等于 Mapper 接口全限定名;id 必须等于接口方法名。二者共同构成 statementId(namespace.id),是框架定位 SQL 的唯一依据。
<?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.mapper.UserMapper">
<select id="selectById" resultType="com.example.entity.User">
SELECT id, name, email, create_time
FROM user
WHERE id = #{id}
</select>
</mapper>
对应接口:
public interface UserMapper {
User selectById(Long id);
}
4.2 注解映射
public interface UserMapper {
@Select("SELECT id, name, email FROM user WHERE id = #{id}")
User selectById(Long id);
@Insert("INSERT INTO user(name, email) VALUES(#{name}, #{email})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);
@Update("UPDATE user SET name = #{name} WHERE id = #{id}")
int updateById(User user);
@Delete("DELETE FROM user WHERE id = #{id}")
int deleteById(Long id);
}
注解族补充:
- @Options:承载 useGeneratedKeys、keyProperty、flushCache、useCache、timeout 等语句级属性。
- @Results + @Result:注解版 resultMap(复杂映射可读性差,仍推荐 XML)。
- @SelectProvider 等 Provider 注解:SQL 由 Java 方法动态构建(见 02 篇)。
选型建议:单表简单 CRUD 用注解,复杂 SQL、动态 SQL、嵌套映射用 XML;同一 Mapper 内风格尽量统一。
4.3 混用规则
五、参数传递
5.1 单参数
// 基本类型:#{} 内名称不校验(仅一个参数),任意写法都能取到值
User selectById(Long id); // XML: WHERE id = #{id}
// POJO:#{属性名},经 getter 取值
int updateById(User user); // XML: SET name = #{name} WHERE id = #{id}
// Map:#{key}
User selectByMap(Map<String, Object> params);// XML: WHERE name = #{name}
5.2 多参数
// ❌ 不推荐:只能用 param1/param2 或 arg0/arg1 引用,可读性差且受编译参数影响
User selectByNameAndStatus(String name, Integer status);
// ✅ 推荐:@Param 显式命名,参数封装为 ParamMap
User selectByNameAndStatus(@Param("name") String name,
@Param("status") Integer status);
// XML: WHERE name = #{name} AND status = #{status}
5.3 集合与数组
// List 单参数:foreach 中 collection="list"(旧约定)或 @Param 命名
List<User> selectByIds(@Param("ids") List<Long> ids);
// XML: <foreach collection="ids" …>
// 数组单参数:collection="array"
List<User> selectByIds(Long[] ids);
5.4 parameterType 属性
parameterType 可省略——MyBatis 通过反射推断实际参数类型。该属性仅作可读性提示,不做强制校验,现代项目普遍省略。
5.5 特殊类型处理
| 枚举 | 按 name() 字符串存取(EnumTypeHandler) | 换 EnumOrdinalTypeHandler(按序号)或自定义 TypeHandler |
| null 参数 | 多数数据库直接设 null | Oracle 等场景必须声明 jdbcType(如 #{status,jdbcType=NUMERIC}) |
| Java 8 日期时间 | 3.4.5+ 内置 JSR-310 TypeHandler,LocalDateTime 等直接映射 | 无需额外配置 |
TypeHandler 的完整机制(自定义 JSON 列转换等)在 02 篇结果映射章节展开。
六、#{} 与 ${}
二者在编译期与运行期的行为完全不同:
#{id} 的处理:
XML SQL: SELECT * FROM user WHERE id = #{id}
BoundSql: SELECT * FROM user WHERE id = ? ← 预编译占位符
参数绑定: ps.setLong(1, 1001L) ← PreparedStatement 绑定
${name} 的处理:
XML SQL: SELECT * FROM user WHERE name = '${name}'
BoundSql: SELECT * FROM user WHERE name = '张三' ← OGNL 求值后直接拼接
| 本质 | 预编译占位符 ? | 字符串替换 |
| SQL 注入 | 天然防御 | 有风险 |
| 执行计划缓存 | 同模板 SQL 可复用 | 每个值生成不同 SQL 文本 |
| 适用位置 | 所有数据值 | 表名、列名、ORDER BY 字段等标识符 |
${} 的合理使用场景只有标识符拼接(占位符 ? 不能替代表名/列名),且必须做白名单校验:
// ❌ 错误示例:直接拼接用户输入 → SQL 注入
List<User> sortUsers(@Param("col") String col);
// XML: ORDER BY ${col} 若 col = "id; DROP TABLE user" 后果严重
// ✅ 正确做法:Service 层白名单校验后再传入
private static final Set<String> SORTABLE = Set.of("id", "name", "create_time");
public List<User> sortUsers(String col) {
if (!SORTABLE.contains(col)) {
throw new IllegalArgumentException("非法排序列: " + col);
}
return userMapper.sortUsers(col);
}
#{} 还支持高级属性内联声明:
<!– 指定 jdbcType 与 typeHandler –>
<insert id="insert">
INSERT INTO user(name, status)
VALUES(#{name}, #{status,jdbcType=NUMERIC,typeHandler=com.example.handler.StatusTypeHandler})
</insert>
七、主键回填
7.1 自增主键:useGeneratedKeys
底层使用 JDBC getGeneratedKeys 能力:
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user(name, email) VALUES(#{name}, #{email})
</insert>
<!– 执行后入参对象的 getId() 已被回填 –>
注解版:
@Insert("INSERT INTO user(name, email) VALUES(#{name}, #{email})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);
3.5+ 支持复合回填:keyProperty="id,createTime"(配合支持多列生成的数据库)。批量插入的 foreach 语句同样支持,3.3.1+ 会把生成的主键依次回填到集合各元素。
7.2 序列与前置生成:selectKey
<!– Oracle 序列:先取主键再插入 –>
<insert id="insert">
<selectKey keyProperty="id" resultType="long" order="BEFORE">
SELECT seq_user.NEXTVAL FROM dual
</selectKey>
INSERT INTO user(id, name) VALUES(#{id}, #{name})
</insert>
<!– MySQL:插入后取 LAST_INSERT_ID(useGeneratedKeys 不可用时的替代) –>
<insert id="insert">
<selectKey keyProperty="id" resultType="long" order="AFTER">
SELECT LAST_INSERT_ID()
</selectKey>
INSERT INTO user(name, email) VALUES(#{name}, #{email})
</insert>
UUID 预生成同理(order="BEFORE" + Java 侧生成或数据库函数生成)。
7.3 注意区分
insert/update/delete 的返回值是影响行数,不是主键值;主键只通过 keyProperty 回填到入参对象。这是初学者最常见的误解。
八、基础 CRUD 模板
8.1 完整模板(XML)
<mapper namespace="com.example.mapper.UserMapper">
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user(name, email, create_time)
VALUES(#{name}, #{email}, #{createTime})
</insert>
<delete id="deleteById">
DELETE FROM user WHERE id = #{id}
</delete>
<update id="updateById">
UPDATE user
SET name = #{name}, email = #{email}
WHERE id = #{id}
</update>
<select id="selectById" resultType="com.example.entity.User">
SELECT id, name, email, create_time FROM user WHERE id = #{id}
</select>
<select id="selectAll" resultType="com.example.entity.User">
SELECT id, name, email, create_time FROM user
</select>
<select id="selectByIds" resultType="com.example.entity.User">
SELECT id, name, email, create_time FROM user
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
<select id="count" resultType="long">
SELECT COUNT(*) FROM user
</select>
</mapper>
说明:本篇 updateById 为全字段更新;生产环境推荐选择性更新(只更新非 null 字段),需要动态 SQL 的 <set>+<if> 支持,见 02 篇。
8.2 返回值约定
| 查询单条 | 实体或 null | 命中多条时 selectOne 抛 TooManyResultsException |
| 查询多条 | List<T> | 无结果返回空集合(非 null) |
| 增删改 | int(影响行数) | 不是主键值 |
| 无实体临时查询 | resultType="map" | 列名做 key,适合报表类一次性查询 |
8.3 批量操作入门
// IN 查询:foreach 拼接(调用前必须判空,防止 IN () 语法错误)
if (ids != null && !ids.isEmpty()) {
List<User> users = userMapper.selectByIds(ids);
}
// 批量写入:批量模式 Executor
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
UserMapper mapper = session.getMapper(UserMapper.class);
for (User u : userList) {
mapper.insert(u);
}
session.flushStatements(); // 统一提交批次
session.commit();
}
批量写入的进阶要点(分批大小、MySQL rewriteBatchedStatements 参数)在 08 篇性能调优章节展开。
九、总结
十、常见高频面试题
1. #{} 和 ${} 的区别?
要点:#{} 解析为预编译占位符 ?,通过 PreparedStatement 绑定参数,防 SQL 注入且利于执行计划缓存;${} 是 OGNL 求值后的字符串直接替换,有注入风险,只应用于表名/列名/排序字段等标识符场景且必须白名单校验。
2. MyBatis 的 insert 返回值是什么?如何拿到自增主键?
要点:返回值是影响行数而非主键。自增主键通过 useGeneratedKeys="true" keyProperty="id"(底层 JDBC getGeneratedKeys)回填到入参对象;序列类主键用 <selectKey order="BEFORE"> 先取后插。
3. Mapper 接口方法有多个参数时如何传递?
要点:不加 @Param 时只能用 param1/param2(或 arg0/arg1)引用;推荐 @Param("name") 显式命名,参数被封装为 ParamMap,XML 中以名称引用。单个 POJO/Map 参数则直接引用属性/key。
4. parameterType 是必须的吗?
要点:不是。MyBatis 通过反射推断实际参数类型,parameterType 仅作可读性提示且不做强制校验,现代项目普遍省略。
5. MyBatis 相比 Hibernate 的优缺点?如何选型?
要点:MyBatis 优点——SQL 完全可控、复杂查询易优化、DBA 可介入、学习曲线平缓;缺点——SQL 与映射文件维护量大、不支持数据库无关性。选型:SQL 复杂多变、需精细调优选 MyBatis;领域模型稳定的 CRUD 密集型系统选 Hibernate/JPA;二者可共存。
6. SqlSession 是线程安全的吗?应该如何使用?
要点:不是线程安全的,内部持有 Executor 与一级缓存等会话状态。纪律:作为方法内局部变量,try-with-resources 用完即关,禁止作为类成员或静态变量共享;SqlSessionFactory 才是应用级单例。
7. MyBatis 中如何实现模糊查询且防止 SQL 注入?
要点:WHERE name LIKE CONCAT('%', #{kw}, '%') 或 <bind name="kw" value="'%' + keyword + '%'"/> 后用 #{kw};禁止 '%${kw}%' 拼接。
8. mybatis-config.xml 中如何批量注册 Mapper?约定是什么?
要点:<package name="com.example.mapper"/> 包扫描,约定 XML 与接口同名同包自动配对;也可 resource/url/class 逐个注册。Spring Boot 环境下由 mybatis.mapper-locations 通配符配置替代。

网硕互联帮助中心




评论前必须登录!
注册