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

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 在运行期自动生成实现类

九、入门建议

  1. 优先阅读官方文档,MyBatis 官方文档简洁清晰,是最好的参考资料
  2. 配置日志框架(如 Logback/Log4j2),可以在控制台看到实际执行的 SQL,极大提升排查问题的效率
  3. 简单 SQL 用注解@Select/@Insert 等),复杂 SQL 写 XML,两种方式可混用
  4. 理解 #{}${} 的本质区别,养成用 #{} 的习惯,防止 SQL 注入
  5. 熟练使用动态 SQL 标签(<if><foreach>)是实际开发中的高频需求
相关推荐
野犬寒鸦2 小时前
高并发利器:SingleFlight优化指南(Java版实现与项目实战)
服务器·开发语言·redis·后端·面试
gelald2 小时前
JVM - 类加载机制
java·jvm·后端
weixin_449190412 小时前
golang中int8溢出
开发语言·后端·golang
清汤饺子2 小时前
Everything Claude Code:让我把 AI 编程效率再翻一倍的东西
前端·javascript·后端
leikooo2 小时前
我用 SubAgent 做了一个 AI 自动修复闭环:流式修代码、自动构建、失败重试
后端·spring·ai编程
小谢小哥2 小时前
11-Java语言核心-JVM原理-JVM调优详解
后端
飞鱼8412 小时前
订单总丢?PHP队列的正确打开方式
后端
元俭2 小时前
【Eino 框架入门】Backend 是怎么变成工具的
后端
小谢小哥2 小时前
08-Java语言核心-JVM原理-垃圾收集详解
后端