MyBatis 从入门到实践:ORM 核心机制与动态 SQL 全解析

19 阅读2分钟

MyBatis 从入门到实践:ORM 核心机制与动态 SQL 全解析

本文以 H2 内存数据库为载体,系统梳理 MyBatis 的核心概念、配置方式、动态 SQL 能力以及底层设计模式,所有示例均可直接运行。


一、什么是 MyBatis?

MyBatis 是一款优秀的持久层框架,其核心思想是对象关系映射(ORM,Object Relationship Mapping) ——自动完成 Java 对象与数据库表之间的映射,让开发者从繁琐的 JDBC 模板代码中解放出来。

对于 Web 应用而言,数据库本质上只是一个连接字符串。MyBatis 在中间扮演了桥梁角色:

Java Object  ←→  MyBatis  ←→  数据库

MyBatis 通过动态代理自动装配 Mapper 接口,开发者只需定义接口和 SQL,无需手写实现类。


二、快速上手:极简 Demo(H2 内存数据库)

2.1 Maven 依赖

<!-- 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 标准项目结构

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

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

package com.mapper;

import com.pojo.User;
import java.util.List;

public interface UserMapper {
    List<User> findAll();
}

③ SQL 映射文件 UserMapper.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 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>

⑤ 测试主程序

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);
        }
    }
}

运行结果:

User{id=1, name='张三', age=20}
User{id=2, name='李四', age=22}

三、核心概念:parameterType 与 resultType

3.1 类型别名(typeAlias)

mybatis-config.xml 中配置别名,可以简化 XML 中的全类名书写:

<typeAliases>
    <typeAlias type="com.example.pojo.User" alias="User"/>
</typeAliases>

3.2 Mapper 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

<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>

<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 子句批量查询或批量插入:

<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> 标签包裹:

@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 实体类定义

// 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)

<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)

<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

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 与方法的绑定关系
SqlSessionFactoryMyBatis 核心工厂,负责创建 SqlSession,建议单例使用
SqlSession代表一次数据库会话,用完即关(推荐 try-with-resources)
H2内存数据库,无需安装,程序关闭即销毁,适合学习和单元测试
动态代理Mapper 接口由 MyBatis 在运行期自动生成实现类

九、入门建议

  1. 优先阅读官方文档,MyBatis 官方文档简洁清晰,是最好的参考资料
  2. 配置日志框架(如 Logback/Log4j2),可以在控制台看到实际执行的 SQL,极大提升排查问题的效率
  3. 简单 SQL 用注解@Select/@Insert 等),复杂 SQL 写 XML,两种方式可混用
  4. 理解 #{}${} 的本质区别,养成用 #{} 的习惯,防止 SQL 注入
  5. 熟练使用动态 SQL 标签(<if><foreach>)是实际开发中的高频需求