MyBatis 增删改查 CRUD ------ 从零开始的持久层框架学习
原创 · 适合刚学完 Java 基础、第一次碰框架的同学。本文用 XML 和 注解 两种方式,把一张
user表的增删改查从头到尾跑通,最后再聊聊底层原理。代码全都能直接抄去用 ✅
一. 前言:MyBatis 到底是个啥?
ok,我们先别急着写代码,先搞清楚 MyBatis 在你的项目里到底干了啥。
如果你之前手写过 JDBC,一定被这一坨东西折磨过:注册驱动、建连接、写 PreparedStatement、处理 ResultSet、关连接......又臭又长,还全是样板代码。
MyBatis 就是一个"半自动 ORM 框架",它帮你把这些脏活累活全包了:
- ✅ 连接、预编译、结果集解析 ------ MyBatis 帮你做;
- ❌ SQL 语句 ------ 还是要你自己写(这恰恰是它"灵活、好控制"的原因,想怎么写怎么写)。
一句话:MyBatis = JDBC 的"自动挡"版本,但方向盘还在你手里。
对比一下你就懂了:
| 原生 JDBC | MyBatis | |
|---|---|---|
| 写 SQL | 要写 | 要写 |
| 建连接/关连接 | 手动 | 自动 |
| 参数设值 | 手动 setXxx |
#{} 自动 |
| 结果映射成对象 | 手动 getString |
自动封装成实体类 |
本文学完,你就能用 MyBatis 把一个 user 表的 增(Create)、查(Retrieve)、改(Update)、删(Delete) ------ 也就是 CRUD ------ 完完整整跑起来 🚀
二. 环境准备:Maven 依赖
MyBatis 本身只有一个核心包,再配一个 MySQL 驱动就够;测试用 JUnit,实体类咱们偷懒用 Lombok。
pom.xml 直接贴上:
xml
<dependencies>
<!-- MyBatis 核心 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.9</version>
</dependency>
<!-- MySQL 驱动(版本与本地 MySQL 对应,8.x 用 cj 驱动)-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.31</version>
</dependency>
<!-- Lombok:自动生成 getter/setter/构造器 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
<!-- JUnit 单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
📌 驱动小贴士 :MySQL 8.x 用 com.mysql.cj.jdbc.Driver;5.x 用 com.mysql.jdbc.Driver。版本不匹配会报"驱动类找不到"或"时区"错误,新手十有八九栽在这。
三. 数据库与实体类
3.1 建表 SQL
先整张表,字段就那几个最基础的:
sql
CREATE TABLE user (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(20),
password VARCHAR(20),
age INT,
sex CHAR(1),
email VARCHAR(30)
);
3.2 实体类 User(Lombok 版)
java
package com.itheima.pojo;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
@Data // 自动生成 getter/setter/toString/equals/hashCode
@NoArgsConstructor // 无参构造(MyBatis 反射创建对象需要)
@AllArgsConstructor // 全参构造(测试时快速 new)
public class User {
private Integer id;
private String username;
private String password;
private Integer age;
private Character sex;
private String email;
}
📌 两个坑提前说(后面会反复遇到):
- 实体类字段名尽量和表列名一致;
@NoArgsConstructor超级关键 ------ MyBatis 查询时是用"无参构造 + setter"把列值填进对象的,没有无参构造会直接炸。
四. 核心配置文件 mybatis-config.xml
这是 MyBatis 的"总开关":声明连哪个库、用哪种事务、去哪找 Mapper。
mybatis-config.xml(放在 resources 目录下):
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>
<!-- 引入外部 jdbc.properties,避免把账号密码写死 -->
<properties resource="jdbc.properties"/>
<!-- 别名:包下所有实体类,别名=类名(User 即可代表 com.itheima.pojo.User)-->
<typeAliases>
<package name="com.itheima.pojo"/>
</typeAliases>
<!-- 环境:连哪个库、用什么事务管理、什么数据源 -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/> <!-- 用原生 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>
<!-- 注册 Mapper:写包名可一次引入该包下所有映射文件 -->
<mappers>
<package name="com.itheima.mapper"/>
</mappers>
</configuration>
jdbc.properties(同目录):
properties
# 这些 key 会被上面 ${jdbc.xxx} 引用
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis_study?useSSL=false&serverTimezone=UTC
jdbc.username=root
jdbc.password=123456
📌 三处"名字对应"是新手最容易错的地方,记死了:
namespace必须等于 Mapper 接口全限定名;- 映射标签
id必须等于接口方法名; - SQL 里
#{属性名}必须等于实体/参数对象的属性名。
五. 方式一:XML 映射 CRUD(最经典)
这是 MyBatis 最原汁原味的写法:接口里只声明方法,SQL 全写在外面的 .xml 里。
5.1 Mapper 接口(只声明,不写实现)
java
package com.itheima.mapper;
import com.itheima.pojo.User;
import java.util.List;
public interface UserMapper {
// 查全部
List<User> findAll();
// 按 id 查
User findById(Integer id);
// 增
int insert(User user);
// 改
int update(User user);
// 删
int delete(Integer id);
}
5.2 映射文件 UserMapper.xml(SQL 写这里)
位置:resources/com/itheima/mapper/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 必须等于接口全限定名,MyBatis 靠它把接口和 SQL 绑在一起 -->
<mapper namespace="com.itheima.mapper.UserMapper">
<!-- 查全部:resultType 是返回的单条记录类型(用了别名 User)-->
<select id="findAll" resultType="User">
select * from user
</select>
<!-- 按 id 查:parameterType 是入参类型,#{id} 从方法参数取值 -->
<select id="findById" parameterType="int" resultType="User">
select * from user where id = #{id}
</select>
<!-- 增:#{username} 等取 user 对象的属性 -->
<insert id="insert" parameterType="User">
insert into user(username, password, age, sex, email)
values(#{username}, #{password}, #{age}, #{sex}, #{email})
</insert>
<!-- 改 -->
<update id="update" parameterType="User">
update user set username=#{username}, password=#{password},
age=#{age}, sex=#{sex}, email=#{email} where id=#{id}
</update>
<!-- 删 -->
<delete id="delete" parameterType="int">
delete from user where id = #{id}
</delete>
</mapper>
📌 为什么用 #{id} 而不是拼字符串? #{} 会被 MyBatis 替换成 预编译占位符 ?,再由 JDBC 安全地设参数------既防 SQL 注入,又自动处理类型。这个点在"常见坑"里还会再强调。
六. 方式二:注解 CRUD(更简洁)
不想额外写 XML?把 SQL 直接写在接口方法的注解里,接口和 SQL 合二为一。
java
package com.itheima.mapper;
import com.itheima.pojo.User;
import org.apache.ibatis.annotations.*; // 注意导入 annotations 包
import java.util.List;
public interface UserMapper {
@Select("select * from user")
List<User> findAll();
@Select("select * from user where id = #{id}")
User findById(@Param("id") Integer id);
@Insert("insert into user(username,password,age,sex,email) " +
"values(#{username},#{password},#{age},#{sex},#{email})")
int insert(User user);
@Update("update user set username=#{username},password=#{password} " +
"where id=#{id}")
int update(User user);
@Delete("delete from user where id = #{id}")
int delete(@Param("id") Integer id);
}
📌 划重点:
- 注解里写的是"一条 SQL 字符串",参数用
#{属性名}占位; - 多参数建议加
@Param("名字"),注解里才能对应上; - 增删改返回
int(受影响行数);查询靠方法返回类型决定返回对象还是列表; - XML 和注解二选一即可,不能对同一方法重复定义。
七. 会话工具类:获取 SqlSession
MyBatis 操作数据库前,必须先拿到 SqlSession(Java 与数据库之间的"会话")。把它封装成工具类,避免到处重复写。
java
package com.itheima.utils;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.*;
import java.io.IOException;
import java.io.InputStream;
public class MyBatisUtils {
private static SqlSessionFactory factory;
static {
try {
// 1. 读核心配置文件
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
// 2. 构建工厂(全局唯一,线程安全)
factory = new SqlSessionFactoryBuilder().build(is);
} catch (IOException e) { e.printStackTrace(); }
}
// 3. 拿到会话;true = 自动提交事务(省得每次 commit)
public static SqlSession getSqlSession() {
return factory.openSession(true);
}
}
📌 三个核心对象要分清:
SqlSessionFactoryBuilder:用完即丢,专门建工厂;SqlSessionFactory:全局一个,专门建会话;SqlSession:一次数据库操作一个,用完要关。
openSession(true) 自动提交,openSession() 默认手动提交(必须显式 commit())。
八. 测试类:JUnit 跑通 CRUD
java
public class UserMapperTest {
@Test
public void testFindAll() {
try (SqlSession session = MyBatisUtils.getSqlSession()) {
UserMapper mapper = session.getMapper(UserMapper.class); // 拿到代理对象
mapper.findAll().forEach(System.out::println);
} // try-with-resources 自动 close
}
@Test
public void testInsert() {
try (SqlSession session = MyBatisUtils.getSqlSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
int rows = mapper.insert(
new User(null, "张三", "123", 23, '男', "z@163.com"));
// 若 openSession() 默认手动提交,这里必须 session.commit();
System.out.println(rows > 0 ? "插入成功" : "失败");
}
}
@Test
public void testUpdate() {
try (SqlSession session = MyBatisUtils.getSqlSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
mapper.update(new User(1, "李四", "999", 25, '女', "l@163.com"));
}
}
@Test
public void testDelete() {
try (SqlSession session = MyBatisUtils.getSqlSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
mapper.delete(1);
}
}
}
⚠️ 增删改别忘了提交事务! 这是各路教程都重点提醒的坑。若 openSession() 没传 true,增删改执行后必须调 session.commit() ,否则数据不会真正写入库(程序还不报错,但库里没变化)。本例用 openSession(true) 规避了这点。
九. 原理:接口没实现类,为什么能调用?
很多人第一次用 MyBatis 都会懵:UserMapper 我压根没写实现类,为啥 mapper.findById(1) 能直接跑?
你调用 mapper.findById(1)
→ 实际是动态代理对象
→ 代理读 SQL → 执行 JDBC → 映射结果
session.getMapper(UserMapper.class) 返回的不是你写的实现类,而是 MyBatis 用 JDK 动态代理 在运行时"现场生成"的代理对象。流程是这样的:
- 接口只是契约(定义能做什么),没有方法体;
- 代理对象"截胡"了你的方法调用,进到统一的
invoke()逻辑里; - 该逻辑:读取注解/XML 里对应的 SQL → 用 JDBC 预编译执行 → 把
ResultSet按列名映射成User对象 → 返回给你。
📌 这就是"只写接口 + SQL 就能干活"的真相:脏活(连接、预编译、防注入、结果映射)全被动态代理兜底了。
十. XML vs 注解:到底怎么选?
| 维度 | XML 映射 | 注解 |
|---|---|---|
| SQL 位置 | 独立 .xml 文件 |
写在接口方法上 |
| 简单 CRUD | 可用,但多一个文件 | 更简洁,推荐 |
| 动态 SQL(if/foreach) | 原生支持,清爽 | 需 <script> 包裹,难看 |
| 改 SQL 是否重编译 | 不用(改完重启即可) | 要 |
| 对应关系约束 | namespace=接口全名,id=方法名 | 方法名即 SQL 标识 |
📌 实战惯例 :简单单表用注解快速搞定;涉及多表关联、条件拼接、批量操作时,改用 XML。两者可混用(同一接口的不同方法分别用注解和 XML),但同一方法只能有一种。
十一. 常见坑(踩过才记得住)
- ❌ 事务没提交 :增删改后数据没进库 → 用
openSession(true)或手动commit()。 - ❌ namespace 写错:必须 = Mapper 接口全限定名,否则"绑定语句找不到"。
- ❌ id 与接口方法名不一致:映射标签 id 必须等于方法名。
- ❌ resultType 漏写:查询必须指定返回类型(或 resultMap)。
- ❌ 参数名不匹配 :多参数用
@Param,XML 里#{名字}与之一致;单对象用属性名。 - ❌ SQL 末尾加分号:部分数据库会报语法错,建议不写。
- ❌ 字段名 vs 属性名 :不一致用
resultMap或开驼峰映射。 - ❌ 用
${}拼接值 :有 SQL 注入风险,值参数一律用#{};${}仅用于动态表名/排序列名。
十二. 进阶与 SpringBoot 整合
12.1 多参数与 Map
不想建实体时,可用 Map 或 @Param 传参:
java
// 接口
void addUser2(Map<String, Object> map);
// XML:key 直接当占位名
<insert id="addUser2" parameterType="map">
insert into user(id, username, password) values(#{userId}, #{userName}, #{userPwd})
</insert>
12.2 MyBatisX 插件
IDEA 装 MyBatisX 后,接口方法与 XML 标签之间可点箭头互跳,还能按接口方法名自动生成 XML 骨架,效率直接拉满 🚀
12.3 SpringBoot 整合(你后续会学)
在 SpringBoot 里,不再手写 SqlSessionFactory,而是:
- 加
mybatis-spring-boot-starter依赖; - 在启动类或配置类上用
@MapperScan("com.itheima.mapper")扫描 Mapper 接口; - 每个 Mapper 接口加
@Mapper(或靠扫描),框架自动注入代理对象; - 配置写在
application.yml的mybatis:下,数据源交给 Spring 管理。
📌 本质没变:SpringBoot 只是把"构建 SqlSessionFactory、注册 Mapper、注入代理对象"这些步骤自动化了。底层 CRUD 原理(动态代理 + JDBC)和你本章手写的完全一致。
十三. 面试题速记
- ①
#{}和${}的区别?#{}是预编译占位符(?),安全防注入、自动加类型引号;${}是字符串直接拼接,有注入风险,仅用于动态表名/排序字段。 - ② 为什么 Mapper 接口能直接注入调用? MyBatis 用 JDK 动态代理在运行时为接口生成代理对象,代理里封装了"读 SQL → 执行 JDBC → 映射结果"的全部逻辑,并由框架注册可用。
- ③ 增删改后查不到数据? 多半是事务没提交。手动提交需
commit(),或openSession(true)自动提交。 - ④ XML 里 namespace 和 id 有什么用?
namespace把 XML 绑定到某个 Mapper 接口;id对应接口里的方法名,两者合起来唯一定位一条 SQL。 - ⑤ 注解和 XML 能混用吗? 可以混用在不同方法上,但同一方法只能有一种 SQL 定义,否则启动报错。
十四. 结语
ok,到这儿 MyBatis 的增删改查 CRUD 就完整跑通了 🎉 咱们来捋一下记忆主线:
- 准备 :Maven 依赖(mybatis + mysql驱动 + junit/lombok)→ 建表 → 实体类(
@Data+ 无参构造)。 - 配置 :
mybatis-config.xml管环境/别名/注册 Mapper;SQL 写在 XML 映射或接口注解里。 - CRUD :查用
@Select/<select>配resultType;增删改用@Insert/@Update/@Delete或对应标签,返回受影响行数。 - 执行 :
getMapper拿代理 → 调用方法;增删改务必提交事务。 - 原理 :动态代理让"只写接口+SQL"成为可能;
#{}预编译防注入。
如果对你有帮助的话,不妨点一个免费的赞和收藏 👍 你们的支持就是我最大的动力!有问题欢迎评论区交流,咱们下篇继续"从零开始"系列~
标签 :#mybatis #java #spring boot #数据库 #后端