乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 MyBatis概述与快速上手

    • 本章定位
    • MyBatis简介
    • 环境搭建
    • 第一个MyBatis程序
    • SqlSessionFactoryBuilder与openSession重载
    • SqlSessionFactory与SqlSession
    • SqlSession核心方法
    • 不使用 XML 构建 SqlSessionFactory
    • Mapper接口与映射方式
    • Java API 目录结构
  • 第2章 全局配置文件详解

    • 本章定位
    • properties
    • settings
    • typeAliases
    • typeHandlers
    • objectFactory
    • plugins
    • environments
    • transactionManager
    • dataSource
    • databaseIdProvider
    • mappers
    • 日志配置
  • 第3章 SQL映射文件基础

    • 本章定位
    • select
    • insert
    • update
    • delete
    • 参数传递与占位符
    • 主键生成策略
    • resultType
    • resultMap
    • 自动映射详解
    • sql片段
    • SQL 语句构建器
  • 第4章 动态SQL

    • 本章定位
    • if
    • choose、when、otherwise
    • where
    • set
    • foreach
    • trim
    • bind
    • script 元素:在注解映射器中启用动态 SQL
    • _databaseId 与动态 SQL 的多数据库支持
    • 动态 SQL 中插入脚本语言
  • 第5章 结果映射与关联查询

    • 本章定位
    • resultMap详解
    • association
    • collection
    • discriminator
    • N+1查询问题
    • 延迟加载
  • 第6章 MyBatis注解开发

    • 本章定位
    • @Select
    • @Insert
    • @Update
    • @Delete
    • @Param
    • @Options
    • @SelectKey
    • @Results
    • @Result
    • @One
    • @Many
    • @SelectProvider
  • 第7章 缓存与性能优化

    • 本章定位
    • 一级缓存
    • 二级缓存
    • 缓存配置详解
    • 自定义缓存
    • Executor执行器类型
    • 分页插件

第一个MyBatis程序

导学

通过本节学习,你将能够:

  • 独立完成 MyBatis 的完整 CRUD 开发流程
  • 理解实体类、Mapper 接口、Mapper XML、测试代码四层结构的协作关系
  • 掌握 SqlSession 执行 selectOne、insert、update、delete 的完整过程
  • 通过控制台日志分析 SQL 执行细节,排查问题

定义

什么是 "第一个 MyBatis 程序"

它指从空项目到成功执行一次数据库 CRUD 的最小完整闭环。这个闭环包含四层文件:

层级文件职责
数据层数据库表持久化存储
实体层Student.java与表结构对应的 Java POJO
映射层StudentMapper.java + StudentMapper.xml定义 SQL 与 Java 方法的绑定关系
应用层Main.java获取 SqlSession,调用 Mapper 方法

与 JDBC 原始写法的对比

维度JDBC 原始写法第一个 MyBatis 程序
代码组织SQL、参数、结果处理全在一个方法SQL 在 XML,逻辑在 Java,分层清晰
新增字段改 SQL + 改参数设置 + 改结果提取改 SQL + 改实体类,框架自动映射
复用性每个方法重复连接/关闭代码连接由 SqlSession 统一托管
可读性业务逻辑被 JDBC 样板代码淹没一眼看到 SQL 和业务逻辑

核心原理

SQL 执行流程图

从 Java 代码发起查询到数据库返回结果,MyBatis 内部经历了以下完整流程:

流程解读:

  1. 接口调用:mapper.findById(1) 看似调用接口方法,实则由 MyBatis 的动态代理拦截
  2. 语句定位:根据 接口全限定名 + 方法名 从全局配置中找到对应的 SQL
  3. 参数处理:ParameterHandler 将 Java 参数 1 转换为 JDBC 的 ps.setInt(1, 1)
  4. SQL 执行:通过 StatementHandler 走 JDBC 与 MySQL 交互
  5. 结果映射:ResultSetHandler 按 resultType 将列值反射设置到实体属性
  6. 缓存写入:结果存入当前 SqlSession 的一级缓存,同一会话内重复查询直接命中

完整示例

场景说明

乐途公司学生管理系统需要实现学生的增删改查。本节提供可直接运行的完整代码,覆盖从项目配置到控制台输出的全过程。

操作前的数据库表结构及初始数据

CREATE TABLE student (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(20),
    age INT,
    major VARCHAR(20),
    score DECIMAL(5,2)
);

INSERT INTO student (name, age, major, score) VALUES
('大翔', 22, '计算机科学', 95.5),
('白歌', 21, '软件工程', 88.0),
('小崔', 20, '计算机科学', 92.0),
('黄俪', 21, '信息安全', 90.5),
('李眉', 22, '软件工程', 87.0);

初始数据状态:

idnameagemajorscore
1大翔22计算机科学95.50
2白歌21软件工程88.00
3小崔20计算机科学92.00
4黄俪21信息安全90.50
5李眉22软件工程87.00

1. pom.xml(项目依赖)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
                             http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.fly</groupId>
    <artifactId>fly-student-system</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.15</version>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>8.0.33</version>
        </dependency>
    </dependencies>
</project>

2. 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 resource="db.properties"/>
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>
    <typeAliases>
        <package name="com.fly.entity"/>
    </typeAliases>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="mapper/StudentMapper.xml"/>
    </mappers>
</configuration>

3. db.properties(数据库连接信息)

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/school?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8
jdbc.username=root
jdbc.password=123456

4. Student.java(实体类)

package com.fly.entity;

import java.math.BigDecimal;

/**
 * 学生实体类:与 student 表一一对应
 */
public class Student {
    private Integer id;
    private String name;
    private Integer age;
    private String major;
    private BigDecimal score;

    // 构造方法
    public Student() {}

    public Student(String name, Integer age, String major, BigDecimal score) {
        this.name = name;
        this.age = age;
        this.major = major;
        this.score = score;
    }

    // Getter / Setter
    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; }
    
    public String getMajor() { return major; }
    public void setMajor(String major) { this.major = major; }
    
    public BigDecimal getScore() { return score; }
    public void setScore(BigDecimal score) { this.score = score; }

    @Override
    public String toString() {
        return "Student{id=" + id + ", name='" + name + "', age=" + age 
            + ", major='" + major + "', score=" + score + "}";
    }
}

5. StudentMapper.java(Mapper 接口)

package com.fly.mapper;

import com.fly.entity.Student;
import java.util.List;

/**
 * 学生数据访问接口
 * 方法名与 StudentMapper.xml 中的 id 严格一致
 */
public interface StudentMapper {
    
    /** 根据 ID 查询学生 */
    Student findById(Integer id);
    
    /** 查询所有学生 */
    List<Student> findAll();
    
    /** 新增学生 */
    int insert(Student student);
    
    /** 更新学生信息 */
    int update(Student student);
    
    /** 根据 ID 删除学生 */
    int deleteById(Integer id);
}

6. StudentMapper.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.fly.mapper.StudentMapper">

    <!-- 查询单个学生 -->
    <select id="findById" resultType="Student">
        SELECT id, name, age, major, score 
        FROM student 
        WHERE id = #{id}
    </select>

    <!-- 查询所有学生 -->
    <select id="findAll" resultType="Student">
        SELECT id, name, age, major, score 
        FROM student
    </select>

    <!-- 插入学生:useGeneratedKeys 获取自增主键 -->
    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO student (name, age, major, score)
        VALUES (#{name}, #{age}, #{major}, #{score})
    </insert>

    <!-- 更新学生 -->
    <update id="update">
        UPDATE student 
        SET name = #{name}, age = #{age}, major = #{major}, score = #{score}
        WHERE id = #{id}
    </update>

    <!-- 删除学生 -->
    <delete id="deleteById">
        DELETE FROM student WHERE id = #{id}
    </delete>

</mapper>

7. Main.java(测试入口)

package com.fly;

import com.fly.entity.Student;
import com.fly.mapper.StudentMapper;
import com.fly.util.MyBatisUtil;
import org.apache.ibatis.session.SqlSession;

import java.math.BigDecimal;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        // 1. 查询单条
        System.out.println("========== 1. 查询 ID=1 的学生 ==========");
        try (SqlSession session = MyBatisUtil.getSqlSessionFactory().openSession()) {
            StudentMapper mapper = session.getMapper(StudentMapper.class);
            Student student = mapper.findById(1);
            System.out.println("结果: " + student);
        }

        // 2. 查询全部
        System.out.println("\n========== 2. 查询所有学生 ==========");
        try (SqlSession session = MyBatisUtil.getSqlSessionFactory().openSession()) {
            StudentMapper mapper = session.getMapper(StudentMapper.class);
            List<Student> list = mapper.findAll();
            list.forEach(s -> System.out.println("结果: " + s));
        }

        // 3. 插入
        System.out.println("\n========== 3. 插入新学生 ==========");
        try (SqlSession session = MyBatisUtil.getSqlSessionFactory().openSession()) {
            StudentMapper mapper = session.getMapper(StudentMapper.class);
            Student newbie = new Student("赵六", 23, "人工智能", new BigDecimal("91.00"));
            int rows = mapper.insert(newbie);
            session.commit(); // 必须手动提交!
            System.out.println("影响行数: " + rows + ", 生成主键: " + newbie.getId());
        }

        // 4. 更新
        System.out.println("\n========== 4. 更新学生 ==========");
        try (SqlSession session = MyBatisUtil.getSqlSessionFactory().openSession()) {
            StudentMapper mapper = session.getMapper(StudentMapper.class);
            Student update = new Student("大翔", 23, "计算机科学", new BigDecimal("96.00"));
            update.setId(1);
            int rows = mapper.update(update);
            session.commit();
            System.out.println("影响行数: " + rows);
        }

        // 5. 删除
        System.out.println("\n========== 5. 删除学生 ==========");
        try (SqlSession session = MyBatisUtil.getSqlSessionFactory().openSession()) {
            StudentMapper mapper = session.getMapper(StudentMapper.class);
            int rows = mapper.deleteById(6); // 删除刚插入的赵六
            session.commit();
            System.out.println("影响行数: " + rows);
        }

        // 6. 验证最终状态
        System.out.println("\n========== 6. 验证最终数据 ==========");
        try (SqlSession session = MyBatisUtil.getSqlSessionFactory().openSession()) {
            StudentMapper mapper = session.getMapper(StudentMapper.class);
            List<Student> list = mapper.findAll();
            list.forEach(s -> System.out.println("结果: " + s));
        }
    }
}

8. MyBatisUtil.java(工具类)

package com.fly.util;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.InputStream;

public class MyBatisUtil {
    private static final SqlSessionFactory sqlSessionFactory;

    static {
        try (InputStream is = Resources.getResourceAsStream("mybatis-config.xml")) {
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
        } catch (IOException e) {
            throw new ExceptionInInitializerError(e);
        }
    }

    public static SqlSessionFactory getSqlSessionFactory() {
        return sqlSessionFactory;
    }
}

实际执行结果(控制台输出)

========== 1. 查询 ID=1 的学生 ==========
==>  Preparing: SELECT id, name, age, major, score FROM student WHERE id = ?
==> Parameters: 1(Integer)
<==    Columns: id, name, age, major, score
<==        Row: 1, 大翔, 22, 计算机科学, 95.50
<==      Total: 1
结果: Student{id=1, name='大翔', age=22, major='计算机科学', score=95.50}

========== 2. 查询所有学生 ==========
==>  Preparing: SELECT id, name, age, major, score FROM student
==> Parameters: 
<==    Columns: id, name, age, major, score
<==        Row: 1, 大翔, 22, 计算机科学, 95.50
<==        Row: 2, 白歌, 21, 软件工程, 88.00
<==        Row: 3, 小崔, 20, 计算机科学, 92.00
<==        Row: 4, 黄俪, 21, 信息安全, 90.50
<==        Row: 5, 李眉, 22, 软件工程, 87.00
<==      Total: 5
结果: Student{id=1, name='大翔', age=22, major='计算机科学', score=95.50}
结果: Student{id=2, name='白歌', age=21, major='软件工程', score=88.00}
结果: Student{id=3, name='小崔', age=20, major='计算机科学', score=92.00}
结果: Student{id=4, name='黄俪', age=21, major='信息安全', score=90.50}
结果: Student{id=5, name='李眉', age=22, major='软件工程', score=87.00}

========== 3. 插入新学生 ==========
==>  Preparing: INSERT INTO student (name, age, major, score) VALUES (?, ?, ?, ?)
==> Parameters: 赵六(String), 23(Integer), 人工智能(String), 91.00(BigDecimal)
<==    Updates: 1
影响行数: 1, 生成主键: 6

========== 4. 更新学生 ==========
==>  Preparing: UPDATE student SET name = ?, age = ?, major = ?, score = ? WHERE id = ?
==> Parameters: 大翔(String), 23(Integer), 计算机科学(String), 96.00(BigDecimal), 1(Integer)
<==    Updates: 1
影响行数: 1

========== 5. 删除学生 ==========
==>  Preparing: DELETE FROM student WHERE id = ?
==> Parameters: 6(Integer)
<==    Updates: 1
影响行数: 1

========== 6. 验证最终数据 ==========
==>  Preparing: SELECT id, name, age, major, score FROM student
==> Parameters: 
<==    Columns: id, name, age, major, score
<==        Row: 1, 大翔, 23, 计算机科学, 96.00
<==        Row: 2, 白歌, 21, 软件工程, 88.00
<==        Row: 3, 小崔, 20, 计算机科学, 92.00
<==        Row: 4, 黄俪, 21, 信息安全, 90.50
<==        Row: 5, 李眉, 22, 软件工程, 87.00
<==      Total: 5
结果: Student{id=1, name='大翔', age=23, major='计算机科学', score=96.00}
结果: Student{id=2, name='白歌', age=21, major='软件工程', score=88.00}
结果: Student{id=3, name='小崔', age=20, major='计算机科学', score=92.00}
结果: Student{id=4, name='黄俪', age=21, major='信息安全', score=90.50}
结果: Student{id=5, name='李眉', age=22, major='软件工程', score=87.00}

最终数据状态分析:

操作影响
插入赵六(id=6)被插入
更新大翔年龄从 22 变为 23,分数从 95.50 变为 96.00
删除赵六(id=6)被删除
净效果大翔信息更新,其余不变,总记录数仍为 5 条

易错场景 / 常见误区

误区错误表现正解
namespace 与接口不一致Invalid bound statement (not found)namespace="com.fly.mapper.StudentMapper" 必须与接口全限定名完全一致
方法名与 XML id 不一致同上findById 接口方法名必须等于 XML 中 <select id="findById">
插入后未 commit控制台显示 Updates: 1,但数据库无数据DML 操作后必须调用 session.commit()
resultType 写全限定名但已配别名冗长且易错在 mybatis-config.xml 中配 <package name="com.fly.entity"/>,XML 中直接用 Student
Mapper XML 未放入 resources启动报 IOException: Could not find resourceStudentMapper.xml 必须放在 src/main/resources/mapper/
省略 db.properties 时区参数The server time zone value 'XXX' is unrecognizedURL 必须带 serverTimezone=Asia/Shanghai
实体类无默认构造方法InstantiationExceptionMyBatis 通过反射调用无参构造创建对象,必须提供

面试考点

Q1:MyBatis 的 Mapper 接口为什么没有实现类也能运行?

A: MyBatis 使用 JDK 动态代理 为 Mapper 接口生成代理对象。当调用 session.getMapper(StudentMapper.class) 时,MyBatis 的 MapperRegistry 会为该接口创建一个代理,代理对象的 invoke 方法会拦截接口调用,根据 接口全限定名 + 方法名 从 Configuration 中找到对应的 MappedStatement,然后交由 Executor 执行 SQL。因此开发者只需定义接口和 XML,无需编写实现类。

Q2:<insert> 中的 useGeneratedKeys="true" 和 keyProperty="id" 有什么作用?

A: useGeneratedKeys="true" 告诉 MyBatis 在插入后向数据库索取自动生成的主键(如 MySQL 的 AUTO_INCREMENT)。keyProperty="id" 指定将获取到的主键值回填到传入实体对象的 id 属性中。这样插入后无需再次查询,student.getId() 即可拿到数据库分配的主键值。

Q3:为什么 MyBatis 的 DML 操作需要手动 commit,而查询不需要?

A: MyBatis 默认使用 JDBC 事务管理器(transactionManager type="JDBC"),且 openSession() 默认创建手动提交模式的 Session。查询操作(SELECT)不涉及数据变更,无需事务边界。而插入、更新、删除会修改数据库状态,必须由业务层决定何时提交(保证原子性)或回滚(保证一致性)。若改为 openSession(true) 自动提交模式,则每条 DML 执行后立即提交,但会失去事务控制能力。

Q4:控制台日志中的 Preparing、Parameters、Total 分别代表什么?

A: Preparing 显示 MyBatis 最终发送给 JDBC 的 SQL 语句(? 表示预编译占位符)。Parameters 显示实际绑定的参数值及类型。Total 表示返回的结果行数。这三行日志是排查 SQL 问题的核心依据:若 Parameters 显示的值与预期不符,说明参数绑定阶段出错;若 Total 为 0 但数据库有数据,可能是条件拼接错误或缓存命中异常。


小结

本节完成了第一个可运行的 MyBatis 程序,覆盖了完整的 CRUD 流程。从 pom.xml 到 mybatis-config.xml,从实体类 Student.java 到 Mapper 接口与 XML,再到测试入口 Main.java,我们展示了四层结构的协作方式。控制台日志清晰地呈现了 SQL 预编译、参数绑定、结果映射的全过程。

关键记忆点:

  • 四层结构:实体类 → Mapper 接口 → Mapper XML → 测试代码
  • namespace 必须等于接口全限定名,方法名必须等于 XML 中的 id
  • DML 必须 session.commit(),实体类必须有无参构造
  • useGeneratedKeys 可自动回填自增主键到实体对象
  • 通过 STDOUT_LOGGING 可在控制台观察完整 SQL 执行链路

下一章引子

第一个程序已经跑通,但 SQL 与 Java 的绑定方式不止 XML 一种。MyBatis 同时支持注解映射(@Select、@Insert 等),在简单场景下可以省去 XML 文件。下一节将对比 XML 映射文件 vs 注解映射 两种方式的语法、适用场景和优劣,帮助你根据项目特点做出正确选择。

上一页
环境搭建
下一页
SqlSessionFactoryBuilder与openSession重载