文章目录
面向 Java 新手,带你跳过繁琐配置,直达数据增删改查核心!
在 Java 企业级应用开发中,数据持久化是绕不开的环节。你可能听过 JDBC、JPA (Hibernate) 等技术,它们各有优劣。传统的 JDBC 代码量大、重复多,写起来很“累”;像 JPA 这样的全自动 ORM (Object-Relational Mapping) 框架,虽然自动化程度高,但在处理复杂、定制化的 SQL 查询时,有时会显得不够灵活,甚至让你感觉“失去了对数据库的控制”。
这时候,MyBatis 就像一位“懂你”的伙伴出现了!它是一个优秀的持久层框架,相比 JDBC,它极大地简化了数据库操作的代码;相比 JPA,它让你能够完全掌控 SQL 语句。这意味着你既能享受到框架带来的便利,又能充分发挥数据库的性能,写出高性能的定制化 SQL。
本指南就是为你——零基础的Java开发者——量身打造的MyBatis快速入门手册。我们将一起:
- 🚀 快速搭建一个MyBatis开发环境。
- ⚙️ 理解并编写MyBatis的核心配置文件。
- 📝 学会如何定义**数据操作接口(Mapper)**和对应的 SQL 映射文件。
- 🎯 通过完整的代码示例,亲手实现最常用的 CRUD(创建、读取、更新、删除)操作。
- ⚠️ 梳理初学者常见的高频问题和避坑指南,让你少走弯路。
准备好了吗?让我们马上开始MyBatis的探索之旅,十分钟内掌握企业级开发中的数据操作核心!
1. 为什么选择 MyBatis?
MyBatis 为何受到众多企业和开发者的青睐?它的魅力在于以下几点:
- SQL 掌控力:这是MyBatis最突出的优点。你可以直接在 XML 文件或注解中编写原生的 SQL 语句。MyBatis 负责帮你把 Java 方法和 SQL 绑定起来,自动处理输入参数和查询结果到 Java 对象的映射。
- 告别繁琐 JDBC:MyBatis 封装了大量 JDBC 的模板代码,比如打开/关闭连接、创建 Statement、处理 ResultSet 等,让你专注于业务逻辑和 SQL 本身。
- 灵活映射:无论是简单的属性映射,还是复杂的联合查询结果,MyBatis 提供了强大的
<resultMap>
等配置,轻松应对各种数据结构的映射需求。 - 易集成:与 Spring、Spring Boot 等主流 Java 框架无缝集成,在企业级项目中应用起来非常方便。
- 半自动化 ORM:它不像 JPA 那样完全隐藏 SQL,而是介于 JDBC 和全自动 ORM 之间,为你提供了一个平衡点。
总结一下:如果你需要编写高性能、定制化的 SQL,同时又希望摆脱 JDBC 的繁琐,那么MyBatis绝对是你的不二之选!
2. 环境准备:项目搭建与依赖引入
“千里之行,始于足下”。要使用 MyBatis,我们得先搭建一个基础的 Java 项目环境。
2.1 开发环境基础
- 确保你已经安装了 Java Development Kit (JDK),推荐使用 JDK 8 或更高版本。
- 推荐使用 Maven 或 Gradle 作为项目管理工具,它们能帮助我们轻松管理项目依赖。本指南以 Maven 为例。
- 准备一个数据库,例如 MySQL。安装并启动 MySQL 服务。
2.2 创建 Maven 项目
打开你的集成开发环境 (IDE),如 IntelliJ IDEA 或 Eclipse,创建一个新的 Maven 项目。
2.3 引入 MyBatis 及数据库驱动依赖
在创建好的 Maven 项目的 pom.xml
文件中,添加 MyBatis 核心库和你的数据库驱动(这里以 MySQL 为例)的依赖。
<?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.example</groupId>
<artifactId>mybatis-quickstart</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- MyBatis 核心依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version> <!-- 使用最新稳定版本,建议查看 Maven Central -->
</dependency>
<!-- MySQL 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version> <!-- 根据你的 MySQL 版本选择 -->
</dependency>
<!-- 日志依赖(可选,便于查看 MyBatis 生成的 SQL) -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.36</version> <!-- 推荐使用 SLF4J -->
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.36</version>
<scope>test</scope> <!-- 日志通常在测试或开发环境使用 -->
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
<scope>test</scope>
</dependency>
<!-- 单元测试依赖 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
小提示:将上面的代码复制到你的 pom.xml
文件中,然后刷新 Maven 项目(IDE 会提示)。依赖就会自动下载到你的本地仓库。
2.4 项目结构
一个标准的 MyBatis standalone 项目结构如下,请参照创建对应的包和文件夹:
myBatis-quickstart/
├── src/
│ ├── main/
│ │ ├── java/ # 存放你的 Java 源代码
│ │ │ └── com/example/mybatis/
│ │ │ ├── pojo/ # 存放实体类 (POJO)
│ │ │ │ └── User.java
│ │ │ └── mapper/ # 存放 Mapper 接口
│ │ │ └── UserMapper.java
│ │ └── resources/ # 存放配置文件和 Mapper XML
│ │ ├── mybatis-config.xml # MyBatis 全局配置文件
│ │ ├── mapper/ # 存放 Mapper XML 文件
│ │ │ └── UserMapper.xml
│ │ └── log4j.properties # 日志配置文件 (可选)
│ └── test/ # 存放单元测试代码 (推荐)
│ └── java/
│ └── com/example/mybatis/
│ └── MyBatisTest.java
└── pom.xml # Maven 配置文件
请注意:resources
文件夹下的内容在打包时会被复制到 classpath 的根目录。
3. MyBatis 核心配置 (mybatis-config.xml
)
mybatis-config.xml
是 MyBatis 的全局配置文件,它告诉 MyBatis 如何连接数据库、如何加载 Mapper 等。这个文件通常放在 src/main/resources
目录下。
3.1 配置文件骨架
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- environments:配置不同的数据库环境,default 指定当前使用哪个环境 -->
<environments default="development">
<!-- environment:定义一个具体的环境 -->
<environment id="development">
<!-- transactionManager:事务管理器,JDBC 表示使用 JDBC 事务 -->
<transactionManager type="JDBC"/>
<!-- dataSource:数据源,POOLED 表示使用 MyBatis 内置的连接池 -->
<dataSource type="POOLED">
<!-- property:配置数据库连接信息 -->
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<!-- TODO: 将 your_database, your_username, your_password 替换为你自己的数据库信息 -->
<property name="url" value="jdbc:mysql://localhost:3306/your_database?useSSL=false&serverTimezone=UTC"/>
<property name="username" value="your_username"/>
<property name="password" value="your_password"/>
</dataSource>
</environment>
<!-- 实际项目中可能还有 production 等环境 -->
<!--
<environment id="production">
...
</environment>
-->
</environments>
<!-- mappers:注册你的 Mapper 文件或接口 -->
<mappers>
<!-- resource:通过 classpath 路径加载 Mapper XML 文件 -->
<mapper resource="mapper/UserMapper.xml"/>
<!-- 或者:package:扫描指定包下的所有 Mapper 接口(接口名需与 XML 文件名对应,XML 在相同包结构下) -->
<!-- <package name="com.example.mybatis.mapper"/> -->
</mappers>
<!-- 其他常用配置(可选) -->
<settings>
<!-- 配置 MyBatis 日志实现,方便调试 -->
<setting name="logImpl" value="LOG4J"/>
<!-- 开启驼峰命名自动映射:数据库列名 user_name 自动映射到 Java 属性 userName -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<!-- typeAliases:为 POJO 类配置别名,方便在 XML 中引用 -->
<typeAliases>
<!-- package:扫描指定包下的所有类,类名即为别名(首字母大小写不敏感) -->
<package name="com.example.mybatis.pojo"/>
<!-- 或者:type:为单个类指定别名 -->
<!-- <typeAlias type="com.example.mybatis.pojo.User" alias="User"/> -->
</typeAliases>
</configuration>
重要!:请将文件中的 your_database
, your_username
, your_password
替换为你实际的数据库连接信息。
3.2 核心配置项解释
<environments>
:可以配置多个数据库连接环境,default
属性指定当前激活的环境 ID。<environment>
:定义一个具体的环境配置,id
是唯一的标识。<transactionManager>
:配置事务管理方式,JDBC
表示使用原生的 JDBC 事务管理(需要手动提交/回滚),MANAGED
表示由外部容器管理(如 Spring)。<dataSource>
:配置数据源,通常使用连接池 (POOLED
) 以提高性能。内部通过<property>
配置连接数据库所需的参数。
<mappers>
:**非常重要!**在这里注册你的 Mapper 文件。MyBatis 需要知道去哪里找到你的 SQL 映射。resource
方式适合少量 Mapper,package
方式适合大量 Mapper,MyBatis 会自动扫描指定包。<settings>
:配置 MyBatis 的全局行为,比如日志输出、缓存设置、驼峰命名自动映射等。开启mapUnderscoreToCamelCase
对提高开发效率非常有帮助。<typeAliases>
:给你的 Java 类(尤其是 POJO)起个“昵称”,在 Mapper XML 中就可以直接用昵称代替完整的类路径了,让 XML 更简洁。使用package
扫描更方便。
4. 创建实体类 (POJO) 和 Mapper 接口/XML
MyBatis 的核心是 Mapper,它连接你的 Java 代码和 SQL 语句。通常,一个 Mapper 对应数据库的一张表。
4.1 实体类 (POJO)
首先,创建一个 Java Bean 来映射数据库表的行数据。假设我们有一个 users
表,包含 id
, username
, age
字段。
数据库表结构:
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
age INT
);
User.java
:
package com.example.mybatis.pojo;
import java.io.Serializable;
// User 实体类,映射数据库 users 表
public class User implements Serializable {
private Long id; // 对应数据库的 id 字段
private String username; // 对应数据库的 username 字段
private Integer age; // 对应数据库的 age 字段
// TODO: 根据实际需求添加更多属性
// 必须提供无参构造函数(MyBatis 需要通过它创建对象)
public User() {
}
// 常用构造函数
public User(String username, Integer age) {
this.username = username;
this.age = age;
}
// Getters 和 Setters 方法(MyBatis 需要通过它们访问属性)
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
// 为了方便打印查看,重写 toString 方法
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", age=" + age +
'}';
}
}
4.2 Mapper 接口
创建一个 Java 接口,声明你想要执行的数据库操作方法。
UserMapper.java
:
package com.example.mybatis.mapper;
import com.example.mybatis.pojo.User;
import org.apache.ibatis.annotations.Param; // 当方法有多个参数时,建议使用 @Param 注解
import java.util.List;
// UserMapper 接口:定义了针对 User 对象的数据库操作方法
public interface UserMapper {
/**
* 根据用户 ID 查询用户信息
* @param id 用户 ID
* @return 对应的 User 对象,如果不存在则返回 null
*/
User selectUserById(Long id);
/**
* 查询所有用户信息
* @return 所有 User 对象的列表
*/
List<User> selectAllUsers();
/**
* 插入一个新用户
* @param user 待插入的 User 对象
* @return 插入的记录数
*/
int insertUser(User user);
/**
* 更新用户信息
* @param user 待更新的 User 对象 (通常包含 id 和需要修改的属性)
* @return 更新的记录数
*/
int updateUser(User user);
/**
* 根据用户 ID 删除用户
* @param id 待删除用户的 ID
* @return 删除的记录数
*/
int deleteUserById(Long id);
/**
* 根据用户名模糊查询用户列表
* @param usernameKeyword 用户名关键字
* @return 符合条件的 User 对象列表
*/
List<User> selectUsersByUsername(@Param("usernameKeyword") String usernameKeyword);
// TODO: 你可以在这里继续添加其他需要的数据库操作方法
}
4.3 Mapper XML 文件
创建与 Mapper 接口同名的 XML 文件 (UserMapper.xml
),通常放在 src/main/resources/mapper/
目录下,用来编写具体的 SQL 语句。
UserMapper.xml
:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace 必须与你的 Mapper 接口全限定名(包名+类名)完全一致 -->
<mapper namespace="com.example.mybatis.mapper.UserMapper">
<!-- 定义一个结果集映射,将数据库列名映射到 POJO 属性名 -->
<!-- id: 映射的唯一标识,type: 映射到的 POJO 类型 -->
<resultMap id="userResultMap" type="com.example.mybatis.pojo.User">
<!-- id: 主键列映射 -->
<!-- column: 数据库列名, property: POJO 属性名 -->
<id column="id" property="id"/>
<!-- result: 普通列映射 -->
<result column="username" property="username"/>
<result column="age" property="age"/>
<!-- TODO: 如果 User 类有更多属性需要映射,在这里添加对应的 <result> 标签 -->
</resultMap>
<!-- 根据ID查询用户 -->
<!-- id: 必须与 Mapper 接口中的方法名 selectUserById 完全一致 -->
<!-- resultType: 指定查询结果映射到的单行 POJO 类型 -->
<!-- #{} 是参数占位符,防止 SQL 注入 -->
<select id="selectUserById" resultType="com.example.mybatis.pojo.User">
SELECT id, username, age
FROM users
WHERE id = #{id}
</select>
<!-- 查询所有用户 -->
<select id="selectAllUsers" resultType="com.example.mybatis.pojo.User">
SELECT id, username, age
FROM users
</select>
<!-- 插入一个新用户 -->
<!-- parameterType: 指定输入参数的类型 -->
<!-- useGeneratedKeys="true" keyProperty="id": 用于获取数据库自增主键的值,并设置到传入的 User 对象的 id 属性上 -->
<insert id="insertUser" parameterType="com.example.mybatis.pojo.User" useGeneratedKeys="true" keyProperty="id">
INSERT INTO users (username, age)
VALUES (#{username}, #{age})
</insert>
<!-- 更新用户信息 -->
<update id="updateUser" parameterType="com.example.mybatis.pojo.User">
UPDATE users
SET username = #{username}, age = #{age}
WHERE id = #{id}
</update>
<!-- 根据ID删除用户 -->
<delete id="deleteUserById" parameterType="Long">
DELETE FROM users
WHERE id = #{id}
</delete>
<!-- 根据用户名模糊查询 -->
<!-- resultMap: 引用上面定义好的结果集映射 -->
<!-- #{usernameKeyword}: 这里的参数名需要和 Mapper 接口中 @Param("usernameKeyword") 的值一致 -->
<select id="selectUsersByUsername" resultMap="userResultMap">
SELECT id, username, age
FROM users
WHERE username LIKE CONCAT('%', #{usernameKeyword}, '%')
</select>
<!-- TODO: 根据你的 Mapper 接口继续添加对应的 SQL 映射 -->
</mapper>
4.4 接口与 XML 的“联姻”
MyBatis 如何知道 UserMapper
接口的 selectUserById(Long id)
方法对应 UserMapper.xml
中的哪个 <select>
标签呢?
关键在于两点:
- XML 文件中的
<mapper namespace="...">
的值必须与 Mapper 接口的**全限定名(包名+类名)**完全一致。 - XML 文件中每个 SQL 标签(
<select>
,<insert>
,<update>
,<delete>
)的id
属性值必须与 Mapper 接口中对应的方法名完全一致。
MyBatis 通过 namespace + id
来找到并执行对应的 SQL 语句。
重要概念解释:
#{...}
:参数占位符。MyBatis 会将其替换为对应的参数值,并在底层使用 PreparedStatement 设置参数。这是推荐的方式,可以有效防止 SQL 注入。${...}
:字符串替换。MyBatis 会直接将值拼接到 SQL 字符串中。存在 SQL 注入风险,通常只用于替换表名、列名等无法使用参数占位符的场景。小白阶段请慎用!resultType
:指定查询结果单行数据映射到的 Java 类型。resultMap
:用于定义复杂的列到属性映射规则,或者处理关联查询、继承等场景。当数据库列名和 POJO 属性名不一致,且未开启mapUnderscoreToCamelCase
时,也需要用到它。
5. 执行 CRUD 操作:让代码跑起来!
现在配置和 Mapper 都准备好了,是时候让 MyBatis 连接数据库并执行操作了!
5.1 核心 API 对象
SqlSessionFactory
:顾名思义,它是创建SqlSession
的工厂。一个应用通常只需要一个SqlSessionFactory
。它是线程安全的。SqlSession
:MyBatis 工作单元,包含了所有执行 SQL 的方法。它不是线程安全的,所以每个线程都应该有自己的SqlSession
实例。用完后必须关闭。
5.2 构建 SqlSessionFactory 工具类
我们创建一个简单的工具类来获取 SqlSessionFactory
。
MyBatisUtil.java
:
package com.example.mybatis;
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;
// MyBatis 工具类,用于获取 SqlSessionFactory
public class MyBatisUtil {
// SqlSessionFactory 是线程安全的,应用程序生命周期内只需要一个
private static SqlSessionFactory sqlSessionFactory;
static {
// 类加载时执行,初始化 SqlSessionFactory
try {
// 指定 mybatis-config.xml 配置文件的路径 (相对 classpath)
String resource = "mybatis-config.xml";
// 读取配置文件为 InputStream
InputStream inputStream = Resources.getResourceAsStream(resource);
// 使用 SqlSessionFactoryBuilder 构建 SqlSessionFactory
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// TODO: 可以根据需要加载不同的 environment (比如 "production")
// sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream, "production");
} catch (IOException e) {
e.printStackTrace();
// 如果初始化失败,可以抛出运行时异常或记录日志
throw new ExceptionInInitializerError("初始化 SqlSessionFactory 失败: " + e.getMessage());
}
}
/**
* 获取 SqlSessionFactory 实例
* @return SqlSessionFactory
*/
public static SqlSessionFactory getSqlSessionFactory() {
return sqlSessionFactory;
}
// TODO: 可以根据需要提供获取 SqlSession 的便捷方法
}
5.3 编写测试代码执行 CRUD
现在,我们编写一个简单的 Main 方法或者 JUnit 测试来执行我们定义的 CRUD 操作。JUnit 测试更规范,这里使用 JUnit 示例。
src/test/java/com/example/mybatis/MyBatisTest.java
:
package com.example.mybatis;
import com.example.mybatis.mapper.UserMapper;
import com.example.mybatis.pojo.User;
import org.apache.ibatis.session.SqlSession;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.*; // JUnit 断言库
// MyBatis 测试类
public class MyBatisTest {
private SqlSession sqlSession; // 工作单元
private UserMapper userMapper; // Mapper 接口代理对象
@Before // 在每个测试方法执行前运行
public void setUp() throws Exception {
// 1. 获取 SqlSession (默认不自动提交事务)
sqlSession = MyBatisUtil.getSqlSessionFactory().openSession();
// 如果希望自动提交,可以使用 openSession(true);
// 2. 获取 Mapper 接口的代理对象
userMapper = sqlSession.getMapper(UserMapper.class);
System.out.println("\n--- 开始测试 ---");
}
@After // 在每个测试方法执行后运行
public void tearDown() throws Exception {
// 3. 关闭 SqlSession
if (sqlSession != null) {
// 测试完成后,通常回滚事务,避免测试数据污染数据库
// 实际业务代码中需要根据情况 commit 或 rollback
sqlSession.rollback(); // 测试结束回滚
// sqlSession.commit(); // 如果希望测试修改数据库,则提交
sqlSession.close();
sqlSession = null; // 避免资源泄露
}
System.out.println("--- 测试结束 ---\n");
}
@Test // 标记这是一个测试方法
public void testInsertUser() {
System.out.println("--- 测试 Insert ---");
User newUser = new User("test_insert", 30);
int count = userMapper.insertUser(newUser);
System.out.println("插入用户: " + newUser);
System.out.println("插入记录数: " + count);
// 断言插入成功
assertEquals(1, count);
// 断言自增主键已设置到对象上
assertNotNull(newUser.getId());
System.out.println("插入成功,新用户 ID: " + newUser.getId());
// 提交事务,使插入生效 (在 @After 中回滚,这里只是演示提交)
// sqlSession.commit();
}
@Test
public void testSelectUserById() {
System.out.println("--- 测试 Select By ID ---");
// TODO: 确保数据库中存在 ID 为 1 的用户,或者先插入一个用户
// 为了测试稳定,我们可以先插入一个用户,再查询
User tempUser = new User("temp_select", 28);
userMapper.insertUser(tempUser); // 插入临时用户
sqlSession.commit(); // 提交插入事务,否则查不到
Long userIdToQuery = tempUser.getId(); // 获取插入的临时用户 ID
System.out.println("准备查询用户 ID: " + userIdToQuery);
User user = userMapper.selectUserById(userIdToQuery);
System.out.println("查询结果: " + user);
// 断言查询结果不为 null
assertNotNull(user);
// 断言查询到的用户 ID 正确
assertEquals(userIdToQuery, user.getId());
// 清理临时数据 (可选,因为 @After 会回滚)
// userMapper.deleteUserById(userIdToQuery);
// sqlSession.commit();
}
@Test
public void testSelectAllUsers() {
System.out.println("--- 测试 Select All ---");
List<User> userList = userMapper.selectAllUsers();
System.out.println("查询所有用户结果:");
userList.forEach(System.out::println);
// 断言查询结果不为 null
assertNotNull(userList);
// 断言列表大小,取决于你的数据库有多少用户
// assertTrue(userList.size() > 0);
}
@Test
public void testUpdateUser() {
System.out.println("--- 测试 Update ---");
// TODO: 确保数据库中存在一个用户用于更新,或者先插入一个
User tempUser = new User("temp_update", 22);
userMapper.insertUser(tempUser); // 插入临时用户
sqlSession.commit(); // 提交插入
Long userIdToUpdate = tempUser.getId();
System.out.println("准备更新用户 ID: " + userIdToUpdate);
// 查询出用户,然后修改属性进行更新
User userToUpdate = userMapper.selectUserById(userIdToUpdate);
assertNotNull(userToUpdate); // 确保用户存在
System.out.println("更新前用户信息: " + userToUpdate);
userToUpdate.setAge(userToUpdate.getAge() + 1); // 年龄加一
userToUpdate.setUsername(userToUpdate.getUsername() + "_updated"); // 用户名加后缀
int count = userMapper.updateUser(userToUpdate);
System.out.println("更新记录数: " + count);
assertEquals(1, count); // 断言更新成功
// 提交更新事务 (或者依赖 @After 的回滚)
// sqlSession.commit();
// 重新查询,验证更新是否生效
User updatedUser = userMapper.selectUserById(userIdToUpdate);
System.out.println("更新后用户信息: " + updatedUser);
assertEquals(userToUpdate.getAge(), updatedUser.getAge());
assertEquals(userToUpdate.getUsername(), updatedUser.getUsername());
}
@Test
public void testDeleteUserById() {
System.out.println("--- 测试 Delete ---");
// TODO: 确保数据库中存在一个用户用于删除,或者先插入一个
User tempUser = new User("temp_delete", 99);
userMapper.insertUser(tempUser); // 插入临时用户
sqlSession.commit(); // 提交插入
Long userIdToDelete = tempUser.getId();
System.out.println("准备删除用户 ID: " + userIdToDelete);
// 先确认用户存在
User userToDelete = userMapper.selectUserById(userIdToDelete);
assertNotNull(userToDelete);
int count = userMapper.deleteUserById(userIdToDelete);
System.out.println("删除记录数: " + count);
assertEquals(1, count); // 断言删除成功
// 提交删除事务 (或者依赖 @After 的回滚)
// sqlSession.commit();
// 尝试再次查询,验证用户是否已被删除
User deletedUser = userMapper.selectUserById(userIdToDelete);
System.out.println("删除后查询结果: " + deletedUser);
assertNull(deletedUser); // 断言用户不存在
}
@Test
public void testSelectUsersByUsernameKeyword() {
System.out.println("--- 测试 Select By Username Keyword ---");
// TODO: 确保数据库中有包含 "test" 关键字的用户
// 插入一些测试数据
userMapper.insertUser(new User("test_keyword1", 10));
userMapper.insertUser(new User("another_test", 20));
userMapper.insertUser(new User("no_match", 30));
sqlSession.commit(); // 提交插入
List<User> users = userMapper.selectUsersByUsername("test");
System.out.println("根据关键字 'test' 查询结果:");
users.forEach(System.out::println);
// 断言查询结果不为 null
assertNotNull(users);
// 断言至少找到 2 个用户
assertTrue(users.size() >= 2);
// 再次查询,测试找不到的情况
List<User> noMatchUsers = userMapper.selectUsersByUsername("nonexistent");
System.out.println("根据关键字 'nonexistent' 查询结果:");
noMatchUsers.forEach(System.out::println);
assertNotNull(noMatchUsers);
assertTrue(noMatchUsers.isEmpty()); // 断言列表为空
}
// TODO: 可以添加更多测试方法来验证你的 Mapper 方法
}
动手试试! 运行 MyBatisTest
类中的测试方法 (testInsertUser
, testSelectUserById
等)。如果一切配置正确,你应该能在控制台看到 MyBais 执行的 SQL 和结果。这会给你极大的成就感!
6. 动态 SQL 基础:构建灵活的查询
实际开发中,查询条件往往是动态变化的。MyBatis 提供了强大的动态 SQL 功能,让你在 XML 中像写代码一样构建灵活的 SQL 语句,避免在 Java 代码中进行丑陋的字符串拼接。
常用的动态 SQL 标签有:<if>
, <where>
, <set>
, <foreach>
, <choose>
, <when>
, <otherwise>
, <trim>
等。
6.1 <if>
和 <where>
示例
假设我们要实现一个根据用户名(模糊查询)和/或年龄来查询用户的接口。
Mapper 接口方法不变:
List<User> selectUsersByCriteria(
@Param("username") String username,
@Param("age") Integer age);
Mapper XML 中对应的 SQL:
<select id="selectUsersByCriteria" resultType="com.example.mybatis.pojo.User">
SELECT id, username, age
FROM users
<!-- <where> 标签智能处理 WHERE 子句 -->
<!-- 它会在内部有条件成立时自动加上 WHERE 关键字 -->
<!-- 并且会智能移除第一个条件前的 AND 或 OR -->
<where>
<!-- <if test="..."> 条件判断,test 中的内容是 OGNL 表达式 -->
<if test="username != null and username != ''">
username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="age != null">
AND age = #{age}
</if>
<!-- 如果既有 username 又有 age,最终生成的 SQL 类似:
SELECT ... FROM users WHERE username LIKE ... AND age = ...
<where> 标签自动移除了 "username LIKE ..." 前的 WHERE,以及 "AND age = #{age}" 前的 "AND" (如果 age 是第一个条件的话)。
但因为上面 username 先判断,所以 age 前面的 AND 会保留下来,<where> 只会处理第一个条件前面的多余 AND/OR。
更常见的用法是:
<where>
<if test="..."> username LIKE ... </if>
<if test="..."> AND age = ... </if>
</where>
这样 <where> 会确保只有条件存在时才加 WHERE,并处理第一个条件前的多余 AND。
-->
</where>
</select>
OGNL 表达式:在 <if test="...">
等标签中,你可以使用 OGNL (Object-Graph Navigation Language) 表达式来访问方法参数。如果参数是 POJO,可以直接写属性名,如 test="user.age != null"
;如果是多个参数且使用了 @Param
,则使用 @Param
指定的名称,如 test="username != null"
;如果是单个基本类型参数,可以直接使用参数名或者 _parameter
。判断字符串是否非空,通常用 != null and != ''
。
6.2 <foreach>
示例
查询指定 ID 列表的用户是一个常见需求。<foreach>
标签非常适合处理集合参数(List, Array, Set)或 Map。
Mapper 接口方法:
List<User> selectUsersByIds(@Param("ids") List<Long> ids);
Mapper XML 中对应的 SQL:
<select id="selectUsersByIds" resultType="com.example.mybatis.pojo.User">
SELECT id, username, age
FROM users
WHERE id IN
<!-- 遍历集合参数 ids -->
<!-- collection: 指定要遍历的集合参数名 (对应 @Param("ids")) -->
<!-- item: 遍历过程中当前元素的变量名 -->
<!-- open: 遍历开始前插入的字符串 -->
<!-- separator: 每次遍历元素之间插入的字符串 -->
<!-- close: 遍历结束后插入的字符串 -->
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id} <!-- 使用 #{} 获取当前元素的值 -->
</foreach>
</select>
如果传入 ids
是 [1L, 5L, 10L]
,MyBatis 最终生成的 SQL 类似:
SELECT id, username, age FROM users WHERE id IN ( ? , ? , ? )
MyBatis 会将 1, 5, 10
作为预编译参数设置进去。这比手动拼接 (1,5,10)
要安全得多。
更多动态 SQL 标签: 还有 <set>
(用于 UPDATE 语句,自动移除末尾逗号), <choose>/<when>/<otherwise>
(多分支判断) 等,都非常实用。建议查阅官方文档深入学习。
7. 集成 Spring/Spring Boot (进阶了解)
在真实的企业级开发中,MyBatis 通常与 Spring 或 Spring Boot 框架一起使用。Spring 提供了强大的依赖注入和事务管理功能,可以极大地简化 MyBatis 的配置和使用。
如果你正在学习 Spring Boot,你会发现集成 MyBatis 变得异常简单。只需要引入 mybatis-spring-boot-starter
依赖,在 application.properties
(或 application.yml
) 中配置数据源和 MyBatis 相关的路径,再在 Spring Boot 启动类上添加 @MapperScan
注解扫描你的 Mapper 接口包,MyBatis 就会自动配置好 SqlSessionFactory
和 SqlSession
,并将你的 Mapper 接口注册为 Spring Bean,可以直接通过 @Autowired
注入使用了。
# application.properties 示例
spring.datasource.url=jdbc:mysql://localhost:3306/your_database?useSSL=false&serverTimezone=UTC
spring.datasource.username=your_username
spring.datasource.password=your_password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
mybatis.mapper-locations=classpath*:mapper/*Mapper.xml # Mapper XML 位置
mybatis.type-aliases-package=com.example.mybatis.pojo # POJO 包
mybatis.configuration.map-underscore-to-camel-case=true # 开启驼峰映射
// Spring Boot 启动类示例
package com.example.myapp;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.example.mybatis.mapper") // 扫描你的 Mapper 接口所在的包
public class MyAppApplication {
public static void main(String[] args) {
SpringApplication.run(MyAppApplication.class, args);
}
}
在 Service 类中直接注入 Mapper 接口:
// Service 层示例
package com.example.myapp.service.impl;
import com.example.mybatis.mapper.UserMapper;
import com.example.mybatis.pojo.User;
import com.example.myapp.service.UserService; // 假设你有 UserService 接口
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; // Spring 事务注解
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
// 直接注入 Mapper 接口的代理对象
@Autowired
private UserMapper userMapper;
@Override
@Transactional // 开启 Spring 事务管理
public int createUser(User user) {
return userMapper.insertUser(user);
}
@Override
public User getUserById(Long id) {
return userMapper.selectUserById(id);
}
// ... 其他方法类似
}
通过 Spring Boot 集成,你可以不再关心 SqlSessionFactory
和 SqlSession
的创建和管理,事务也由 @Transactional
注解搞定,开发效率大幅提升!这部分内容更深入,适合你在掌握 MyBatis 基础后进一步学习。
8. 初学者避坑指南 (高频问题解答)
作为新手,你很可能会遇到一些常见问题,别担心,这里帮你整理好了,遇到问题先来看看这里!
- 问题1:
resources
下的配置文件或 Mapper XML 找不到?- 原因:路径不对,或者 Maven/Gradle 没有将
resources
目录下的文件复制到target/classes
(即 classpath)。 - 解决方案:
- 检查
mybatis-config.xml
在src/main/resources
下。 - 检查
mapper/UserMapper.xml
在src/main/resources/mapper
下。 - 检查
mybatis-config.xml
中<mapper resource="mapper/UserMapper.xml"/>
的路径是否正确。 - 如果是 Maven 项目,确认
pom.xml
中没有阻止资源文件打包的配置。通常src/main/resources
会自动打包。 - 清理并重新构建项目 (
mvn clean package
)。
- 检查
- 原因:路径不对,或者 Maven/Gradle 没有将
- 问题2:
namespace
或id
匹配错误,报Bound statement not found
异常?- 原因:MyBatis 通过
namespace + id
找到对应的 SQL 映射,任何一个不匹配都会导致找不到。 - 解决方案:
- 检查 Mapper XML 文件中
<mapper namespace="...">
的值是否完全等于 Mapper 接口的全限定名。 - 检查 Mapper XML 中 SQL 标签的
id
属性值是否完全等于 Mapper 接口中对应的方法名。
- 检查 Mapper XML 文件中
- 原因:MyBatis 通过
- 问题3:参数传不进去,或者参数映射错误?
- 原因:参数类型与 XML 中声明的
parameterType
不匹配,或者多参数方法没有使用@Param
注解,导致 MyBatis 不知道如何取值。 - 解决方案:
- 如果只有一个参数,XML 中可以使用
parameterType
指定类型,在 SQL 中直接用#{参数名}
或#{_parameter}
访问。 - 如果参数是 POJO,XML 中
parameterType
指向 POJO 类,SQL 中使用#{属性名}
。 - 强烈建议多参数方法都使用
@Param("参数别名")
注解,然后在 XML 中使用#{参数别名}
访问。例如selectUserById(@Param("userId") Long id)
对应 XML 中的WHERE id = #{userId}
。 - 检查
#{}
和${}
是否用错。大多数情况下应该使用#{}
。
- 如果只有一个参数,XML 中可以使用
- 原因:参数类型与 XML 中声明的
- 问题4:数据库连接失败?
- 原因:
mybatis-config.xml
中的数据库连接信息(url, username, password, driver)错误,或者数据库服务未启动,防火墙阻止连接等。 - 解决方案:
- 仔细检查
mybatis-config.xml
中的数据库连接配置。 - 确认数据库服务正在运行,并且监听了正确的端口。
- 确保 JDBC 驱动 (
mysql-connector-java
) 版本与你的数据库兼容。 - 如果是远程数据库,检查网络连接和防火墙设置。
serverTimezone=UTC
和useSSL=false
参数对于 MySQL 8+ 常见,通常需要加上。
- 仔细检查
- 原因:
- 问题5:插入后自增主键回填不到对象上?
- 原因:插入标签
<insert>
没有正确配置useGeneratedKeys="true"
和keyProperty="你的主键属性名"
。 - 解决方案:确保插入标签中添加了
useGeneratedKeys="true" keyProperty="id"
(假设你的主键属性名是id
)。
- 原因:插入标签
- 问题6:查询结果映射错误,部分属性为 null?
- 原因:数据库列名与 POJO 属性名不一致,且没有使用
<resultMap>
或开启mapUnderscoreToCamelCase
。 - 解决方案:
- 推荐开启
<setting name="mapUnderscoreToCamelCase" value="true"/>
来自动映射user_name
到userName
。 - 如果列名和属性名差异较大,或者需要处理复杂映射,定义并使用
<resultMap>
进行显式映射。
- 推荐开启
- 原因:数据库列名与 POJO 属性名不一致,且没有使用
温馨提示:遇到问题时,仔细查看控制台输出的错误日志。MyBatis 的日志通常会给出非常详细的错误信息,结合这些信息去排查问题会事半功倍!如果开启了日志 (<setting name="logImpl" value="LOG4J"/>
并配置了 log4j.properties),还能看到 MyBatis 实际执行的 SQL 语句,这对于调试非常有帮助。
你的 MyBatis 之旅刚刚开始!
恭喜你!通过本指南的学习和实践,你已经掌握了 MyBatis 的基础知识和核心用法,能够独立搭建环境、配置、编写 Mapper 并执行基本的数据库 CRUD 操作。这为你进一步深入学习和在企业级项目中应用 MyBatis 打下了坚实的基础。
核心知识点再回顾:
- 定位:MyBatis 是一个灵活的持久层框架,让你掌控 SQL。
- 配置:
mybatis-config.xml
是全局指挥中心。 - 映射:Mapper 接口定义操作,Mapper XML 编写 SQL 并负责映射 (
#{}
,${}
)。 - API:
SqlSessionFactory
(工厂) 和SqlSession
(工作单元)。 - CRUD:掌握
<select>
,<insert>
,<update>
,<delete>
标签的基本用法。 - 动态 SQL:
<if>
,<where>
,<foreach>
等标签让你写出更灵活的 SQL。 - 集成:与 Spring/Spring Boot 集成是企业级开发的常见模式。
- 避坑:注意
namespace
/id
匹配、参数传递、#{}
vs${}
、事务管理等。
接下来,你可以:
- 巩固基础:多动手实践,尝试实现更复杂的查询,比如带分页、排序的查询。
- 深入官方文档:MyBatis 官方文档 是最权威的资料,了解更多高级特性,如缓存、插件、类型处理器等。
- 学习进阶技术:研究 MyBatis 的二级缓存如何配置和使用,学习如何使用 PageHelper 等流行分页插件。
- 结合 Spring Boot 实践:搭建一个 Spring Boot + MyBatis 项目,体验更便捷的开发模式。
- 探索 MyBatis Generator:了解代码生成器,提高开发效率。
掌握 MyBatis 这样的持久层框架是 Java 后端开发者的必备技能。不断学习和实践,你一定能在数据操作领域游刃有余!
如果你觉得这篇指南对你有帮助,请毫不犹豫地:
- 点赞👍:让更多的人看到这篇指南!
- 收藏⭐:方便以后随时查阅!
- 分享↗️:帮助更多正在学习 MyBatis 的朋友!
也欢迎在评论区留下你的问题或学习心得,我们一起交流进步!