MyBatis 从入门到实践:ORM 核心机制与动态 SQL 全解析
本文以 H2 内存数据库为载体,系统梳理 MyBatis 的核心概念、配置方式、动态 SQL 能力以及底层设计模式,所有示例均可直接运行。
一、什么是 MyBatis?
MyBatis 是一款优秀的持久层框架 ,其核心思想是对象关系映射(ORM,Object Relationship Mapping) ------自动完成 Java 对象与数据库表之间的映射,让开发者从繁琐的 JDBC 模板代码中解放出来。
对于 Web 应用而言,数据库本质上只是一个连接字符串。MyBatis 在中间扮演了桥梁角色:
css
Java Object ←→ MyBatis ←→ 数据库
MyBatis 通过动态代理自动装配 Mapper 接口,开发者只需定义接口和 SQL,无需手写实现类。
二、快速上手:极简 Demo(H2 内存数据库)
2.1 Maven 依赖
xml
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.15</version>
</dependency>
<!-- H2 内存数据库(学习阶段推荐,无需安装) -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
<scope>runtime</scope>
</dependency>
2.2 标准项目结构
arduino
src/
└── main/
├── java/
│ └── com/
│ ├── pojo/
│ │ └── User.java // 实体类
│ └── mapper/
│ └── UserMapper.java // Mapper 接口
└── resources/
├── mybatis-config.xml // MyBatis 核心配置
└── com/
└── mapper/
└── UserMapper.xml // SQL 映射文件
2.3 核心文件逐一实现
① 实体类 User.java
typescript
package com.pojo;
public class User {
private Integer id;
private String name;
private Integer age;
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
@Override
public String toString() {
return "User{id=" + id + ", name='" + name + "', age=" + age + "}";
}
}
② Mapper 接口 UserMapper.java
java
package com.mapper;
import com.pojo.User;
import java.util.List;
public interface UserMapper {
List<User> findAll();
}
③ SQL 映射文件 UserMapper.xml
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">
<!-- namespace 必须对应接口全类名 -->
<mapper namespace="com.mapper.UserMapper">
<select id="findAll" resultType="com.pojo.User">
SELECT * FROM user
</select>
</mapper>
④ MyBatis 核心配置 mybatis-config.xml
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>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="org.h2.Driver"/>
<!-- DB_CLOSE_DELAY=-1:保持数据库在 JVM 存活期间不关闭 -->
<property name="url" value="jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/mapper/UserMapper.xml"/>
</mappers>
</configuration>
⑤ 测试主程序
java
import com.mapper.UserMapper;
import com.pojo.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.*;
import java.sql.Connection;
import java.sql.Statement;
import java.util.List;
public class MyBatisTest {
public static void main(String[] args) throws Exception {
// 1. 读取配置,构建 SqlSessionFactory
SqlSessionFactory factory = new SqlSessionFactoryBuilder()
.build(Resources.getResourceAsStream("mybatis-config.xml"));
// 2. 初始化 H2 数据库(建表 + 插入测试数据)
try (SqlSession session = factory.openSession()) {
Connection conn = session.getConnection();
Statement stmt = conn.createStatement();
stmt.execute("CREATE TABLE IF NOT EXISTS user (" +
"id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(20), age INT)");
stmt.executeUpdate("INSERT INTO user(name, age) VALUES ('张三', 20), ('李四', 22)");
session.commit();
}
// 3. 使用 Mapper 查询数据
try (SqlSession session = factory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
List<User> list = mapper.findAll();
list.forEach(System.out::println);
}
}
}
运行结果:
ini
User{id=1, name='张三', age=20}
User{id=2, name='李四', age=22}
三、核心概念:parameterType 与 resultType
3.1 类型别名(typeAlias)
在 mybatis-config.xml 中配置别名,可以简化 XML 中的全类名书写:
bash
<typeAliases>
<typeAlias type="com.example.pojo.User" alias="User"/>
</typeAliases>
3.2 Mapper XML 示例
xml
<mapper namespace="com.example.mapper.UserMapper">
<!-- parameterType=int:传入基本类型参数 -->
<!-- resultType=User:返回结果映射到 User 对象(使用别名) -->
<select id="selectById" parameterType="int" resultType="User">
SELECT id, name, age FROM user WHERE id = #{id}
</select>
<!-- parameterType=User:MyBatis 按 Java Bean 约定读取属性(调用 getter) -->
<insert id="insert" parameterType="User">
INSERT INTO user (name, age) VALUES (#{name}, #{age})
</insert>
<!-- 集合类型只需指定元素类型 -->
<select id="selectAll" resultType="User">
SELECT id, name, age FROM user
</select>
</mapper>
3.3 关键知识点
| 特性 | 说明 |
|---|---|
parameterType |
指定传入参数类型;Java Bean 通过 getter 读取属性值 |
resultType |
指定返回结果类型;MyBatis 通过 setter 按列名映射属性 |
#{} |
预编译参数,防止 SQL 注入,推荐使用 |
${} |
字符串直接拼接,存在 SQL 注入风险,仅用于动态表名/列名等特殊场景 |
四、动态 SQL:MyBatis 的灵魂
动态 SQL 是 MyBatis 最强大的特性之一,能根据传入条件动态拼接 SQL,避免硬编码多套查询语句。
4.1 <if>:条件判断
最常用的标签,配合 <where> 自动处理多余的 AND/OR。
bash
<select id="selectByCondition" parameterType="User" resultType="User">
SELECT id, name, age, status FROM user
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="age != null">
AND age = #{age}
</if>
<if test="status != null and status != ''">
AND status = #{status}
</if>
</where>
</select>
4.2 <choose>:多分支选择(类似 switch-case)
只匹配第一个满足条件的 <when>,否则走 <otherwise>。
bash
<select id="selectByChoose" parameterType="User" resultType="User">
SELECT id, name, age, status FROM user
<where>
<choose>
<when test="id != null">
AND id = #{id}
</when>
<when test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</when>
<when test="age != null">
AND age = #{age}
</when>
<otherwise>
AND status = 'ACTIVE'
</otherwise>
</choose>
</where>
</select>
4.3 <foreach>:遍历集合(批量操作)
常用于 IN 子句批量查询或批量插入:
sql
<select id="selectByIds" resultType="User">
SELECT id, name, age, status FROM user
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
注意 :多参数时需用 @Param 注解指定集合名称,如 @Param("ids") List<Integer> ids。
4.4 <script>:在注解中使用动态 SQL
不写 XML 但又需要动态 SQL 时,可在 @Select 等注解中使用 <script> 标签包裹:
xml
@Select("<script>"
+ "SELECT id, name, age, status FROM user "
+ "<where>"
+ " <if test='name != null'>AND name LIKE CONCAT('%', #{name}, '%')</if>"
+ " <if test='age != null'>AND age = #{age}</if>"
+ "</where>"
+ "</script>")
List<User> selectByAnnotationScript(User user);
4.5 动态 SQL 标签速查
| 标签 | 作用 | 典型场景 |
|---|---|---|
<if> |
条件判断,动态拼接 SQL | 多条件筛选(搜索功能) |
<choose> |
多分支选择(类似 switch) | 优先匹配某一条件,否则走默认逻辑 |
<foreach> |
遍历集合/数组 | 批量查询(IN 子句)、批量插入/删除 |
<script> |
在注解中包裹动态 SQL | 不想写 XML 但需要动态 SQL |
<where> |
自动处理多余的 AND/OR | 配合 <if> 使用,避免 SQL 语法错误 |
五、关联映射:<association> 一对一
5.1 场景说明
一个 User 对应一个 Account(一对一关系),需要通过 <association> 完成嵌套对象映射。
5.2 实体类定义
arduino
// User 主表
public class User {
private Integer id;
private String name;
private Account account; // 一对一关联
// getter/setter...
}
// Account 从表
public class Account {
private Integer id;
private String accountNo;
private Integer userId; // 外键
// getter/setter...
}
5.3 Mapper XML(核心:resultMap + association)
xml
<mapper namespace="com.mapper.UserMapper">
<select id="selectUserWithAccount" resultMap="userAccountMap">
SELECT
u.id AS user_id,
u.name,
a.id AS account_id,
a.account_no,
a.user_id
FROM user u
LEFT JOIN account a ON u.id = a.user_id
WHERE u.id = #{userId}
</select>
<resultMap id="userAccountMap" type="User">
<id property="id" column="user_id"/>
<result property="name" column="name"/>
<!-- association:一对一嵌套对象映射 -->
<association property="account" javaType="Account">
<id property="id" column="account_id"/>
<result property="accountNo" column="account_no"/>
<result property="userId" column="user_id"/>
</association>
</resultMap>
</mapper>
5.4 association 核心规则
- 必须使用
resultMap,不能直接用resultType property对应主实体类中的字段名javaType指定嵌套对象的类型
5.5 扩展:懒加载写法(发两条 SQL)
ini
<resultMap id="userMap" type="User">
<id property="id" column="id"/>
<result property="name" column="name"/>
<association
property="account"
javaType="Account"
select="com.mapper.AccountMapper.selectByUserId"
column="id"/>
</resultMap>
这种方式会在访问 account 属性时再发起第二条 SQL 查询,适合按需加载场景。
六、Service 层的标准写法
在实际项目中,SqlSession 应封装在 Service 层,通过依赖注入 SqlSessionFactory:
java
public class UserService {
private final SqlSessionFactory sqlSessionFactory;
public UserService(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
}
public void deleteUserById(Integer id) {
// openSession(true) 表示自动提交事务
try (SqlSession session = sqlSessionFactory.openSession(true)) {
UserMapper mapper = session.getMapper(UserMapper.class);
mapper.deleteUserById(id);
}
}
}
interface UserMapper {
@Delete("DELETE FROM user WHERE id = #{id}")
void deleteUserById(@Param("id") Integer id);
}
七、MyBatis 中的设计模式
MyBatis 的源码中大量运用了经典设计模式,理解这些模式有助于深入掌握框架原理:
| 设计模式 | 在 MyBatis 中的体现 | 作用 |
|---|---|---|
| 抽象工厂模式 | SqlSessionFactory |
统一创建 SqlSession 对象 |
| 单例模式 | ErrorContext |
保证每个线程只有一个错误上下文对象 |
| 代理模式 | Mapper 动态代理 | 接口无需实现类,MyBatis 自动生成代理对象 |
| 装饰器模式 | CachingExecutor |
在基础执行器上叠加缓存功能 |
| 模板方法模式 | BaseExecutor |
定义 SQL 执行的骨架流程,子类实现具体细节 |
| 适配器模式 | 日志模块 | 统一适配 Log4j、Slf4j、JDK Logging 等不同日志框架 |
八、核心概念一句话总结
| 概念 | 说明 |
|---|---|
| Mapper | 接口 + XML(或注解),负责定义 SQL 与方法的绑定关系 |
| SqlSessionFactory | MyBatis 核心工厂,负责创建 SqlSession,建议单例使用 |
| SqlSession | 代表一次数据库会话,用完即关(推荐 try-with-resources) |
| H2 | 内存数据库,无需安装,程序关闭即销毁,适合学习和单元测试 |
| 动态代理 | Mapper 接口由 MyBatis 在运行期自动生成实现类 |
九、入门建议
- 优先阅读官方文档,MyBatis 官方文档简洁清晰,是最好的参考资料
- 配置日志框架(如 Logback/Log4j2),可以在控制台看到实际执行的 SQL,极大提升排查问题的效率
- 简单 SQL 用注解 (
@Select/@Insert等),复杂 SQL 写 XML,两种方式可混用 - 理解
#{}与${}的本质区别,养成用#{}的习惯,防止 SQL 注入 - 熟练使用动态 SQL 标签(
<if>、<foreach>)是实际开发中的高频需求