第一个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 内部经历了以下完整流程:
流程解读:
- 接口调用:
mapper.findById(1)看似调用接口方法,实则由 MyBatis 的动态代理拦截 - 语句定位:根据
接口全限定名 + 方法名从全局配置中找到对应的 SQL - 参数处理:
ParameterHandler将 Java 参数1转换为 JDBC 的ps.setInt(1, 1) - SQL 执行:通过
StatementHandler走 JDBC 与 MySQL 交互 - 结果映射:
ResultSetHandler按resultType将列值反射设置到实体属性 - 缓存写入:结果存入当前 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);
初始数据状态:
| id | name | age | major | score |
|---|---|---|---|---|
| 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 resource | StudentMapper.xml 必须放在 src/main/resources/mapper/ |
| 省略 db.properties 时区参数 | The server time zone value 'XXX' is unrecognized | URL 必须带 serverTimezone=Asia/Shanghai |
| 实体类无默认构造方法 | InstantiationException | MyBatis 通过反射调用无参构造创建对象,必须提供 |
面试考点
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 注解映射 两种方式的语法、适用场景和优劣,帮助你根据项目特点做出正确选择。