一、前言:为什么需要 MyBatis?
如果你写过 JDBC,一定体会过这种痛苦:
java
// JDBC 六步曲,每个接口都要写一遍
Class.forName("com.mysql.cj.jdbc.Driver");
Connection conn = DriverManager.getConnection(url, user, pwd);
PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM t_user WHERE id = ?");
pstmt.setLong(1, id);
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
User user = new User();
user.setId(rs.getLong("id"));
user.setUsername(rs.getString("username"));
// ... 十几个字段手动映射
}
rs.close(); pstmt.close(); conn.close();
MyBatis 解决的核心问题:
| JDBC 痛点 | MyBatis 方案 | 效果 |
|---|---|---|
| 大量重复代码 | Mapper 接口 + 注解/XML | 代码量减少 70% |
| SQL 与 Java 混杂 | XML 分离或注解简洁 | 维护性提升 |
| 结果集手动映射 | 自动映射到对象 | 不再写 getString |
| 动态 SQL 拼接 | <if>、<where> 标签 |
条件查询不再头疼 |
| 连接管理繁琐 | Spring 集成自动管理 | 专注业务逻辑 |
MyBatis 定位: 半自动 ORM,SQL 自己写(可控),结果映射自动做(省心)。
二、环境准备
2.1 需要安装
| 软件 | 版本 | 检查命令 |
|---|---|---|
| JDK | 17+ | java -version |
| Maven | 3.8+ | mvn -v |
| MySQL | 8.0+ | mysql --version |
| IDEA | 2023+ | - |
| 2.2 创建 Spring Boot 项目 |
-
选择:
- Project: Maven
- Language: Java
- Spring Boot: 3.2.0
- Dependencies: Spring Web, MySQL Driver, MyBatis Framework
-
点击 Generate,下载解压后用 IDEA 打开
2.3 项目结构预览
plain
mybatis-demo/
├── pom.xml
└── src/
└── main/
├── java/com/example/demo/
│ ├── DemoApplication.java
│ ├── entity/ ← 实体类
│ │ └── User.java
│ ├── mapper/ ← Mapper 接口
│ │ └── UserMapper.java
│ ├── service/ ← 业务层
│ │ └── UserService.java
│ └── controller/ ← 控制器
│ └── UserController.java
└── resources/
├── application.yml ← 配置文件
└── mapper/ ← XML 映射文件
└── UserMapper.xml
三、创建数据库
3.1 执行建库脚本
sql
-- 创建数据库
CREATE DATABASE IF NOT EXISTS mybatis_demo
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE mybatis_demo;
-- 创建用户表
CREATE TABLE t_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID',
username VARCHAR(50) NOT NULL COMMENT '用户名',
email VARCHAR(100) COMMENT '邮箱',
age INT COMMENT '年龄',
phone VARCHAR(20) COMMENT '手机号',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted TINYINT DEFAULT 0 COMMENT '逻辑删除:0-正常 1-删除'
) COMMENT '用户表';
-- 插入测试数据
INSERT INTO t_user (username, email, age, phone) VALUES
('张三', 'zhangsan@example.com', 25, '13800138001'),
('李四', 'lisi@example.com', 30, '13800138002'),
('王五', 'wangwu@example.com', 28, '13800138003'),
('赵六', 'zhaoliu@example.com', 35, '13800138004'),
('孙七', 'sunqi@example.com', 22, '13800138005');
-- 验证数据
SELECT * FROM t_user;
四、完整代码
4.1 pom.xml(完整依赖)
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
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>mybatis-demo</artifactId>
<version>1.0.0</version>
<name>mybatis-demo</name>
<description>MyBatis Demo Project</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis Spring Boot Starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 参数校验 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
4.2 application.yml(完整配置)
yaml
# 服务器端口
server:
port: 8080
# 数据源配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/mybatis_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: your_password # 修改为你的密码
driver-class-name: com.mysql.cj.jdbc.Driver
# MyBatis 配置
mybatis:
# XML 映射文件位置
mapper-locations: classpath:mapper/*.xml
# 实体类别名包
type-aliases-package: com.example.demo.entity
configuration:
# 自动驼峰转换:create_time → createTime
map-underscore-to-camel-case: true
# 打印 SQL 到控制台
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 缓存开启
cache-enabled: true
# 日志级别
logging:
level:
com.example.demo.mapper: debug # 打印 Mapper SQL
4.3 实体类 User.java
java
package com.example.demo.entity;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 用户实体类
*
* 对应数据库表 t_user
* 字段名与表字段一一对应,驼峰命名自动转换
*/
@Data
public class User {
/** 用户ID */
private Long id;
/** 用户名 */
private String username;
/** 邮箱 */
private String email;
/** 年龄 */
private Integer age;
/** 手机号 */
private String phone;
/** 创建时间 */
private LocalDateTime createTime;
/** 更新时间 */
private LocalDateTime updateTime;
/** 逻辑删除:0-正常 1-删除 */
private Integer deleted;
}
4.4 统一响应结果 Result.java
java
package com.example.demo.common;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 统一响应结果
*/
@Data
public class Result<T> {
private Integer code;
private String message;
private T data;
private String timestamp;
private Result() {
this.timestamp = LocalDateTime.now().toString();
}
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("成功");
result.setData(data);
return result;
}
public static <T> Result<T> success() {
return success(null);
}
public static <T> Result<T> error(Integer code, String message) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
return result;
}
}
4.5 分页结果 PageResult.java
java
package com.example.demo.common;
import lombok.Data;
import java.util.List;
/**
* 分页结果
*/
@Data
public class PageResult<T> {
private Long total;
private List<T> list;
private Integer pageNum;
private Integer pageSize;
private Integer totalPages;
public static <T> PageResult<T> of(Long total, List<T> list, Integer pageNum, Integer pageSize) {
PageResult<T> result = new PageResult<>();
result.setTotal(total);
result.setList(list);
result.setPageNum(pageNum);
result.setPageSize(pageSize);
result.setTotalPages((int) Math.ceil((double) total / pageSize));
return result;
}
}
4.6 业务异常 BusinessException.java
java
package com.example.demo.exception;
import lombok.Getter;
@Getter
public class BusinessException extends RuntimeException {
private final Integer code;
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
}
}
4.7 全局异常处理器 GlobalExceptionHandler.java
java
package com.example.demo.handler;
import com.example.demo.common.Result;
import com.example.demo.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import jakarta.servlet.http.HttpServletRequest;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e, HttpServletRequest request) {
// log.warn("业务异常 [{}] - {}", request.getRequestURI(), e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e, HttpServletRequest request) {
// log.error("系统异常 [{}]", request.getRequestURI(), e);
return Result.error(500, "系统繁忙,请稍后重试");
}
}
4.8 Mapper 接口 UserMapper.java
java
package com.example.demo.mapper;
import com.example.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 用户 Mapper
*
* @Mapper 标记为 MyBatis Mapper,Spring 自动扫描并生成代理对象
*/
@Mapper
public interface UserMapper {
/**
* 根据 ID 查询
*/
User selectById(Long id);
/**
* 查询所有
*/
List<User> selectAll();
/**
* 条件查询
* @param username 用户名(模糊查询)
* @param minAge 最小年龄
* @param maxAge 最大年龄
*/
List<User> selectByCondition(@Param("username") String username,
@Param("minAge") Integer minAge,
@Param("maxAge") Integer maxAge);
/**
* 分页查询
* @param offset 偏移量
* @param pageSize 每页条数
*/
List<User> selectByPage(@Param("offset") Integer offset,
@Param("pageSize") Integer pageSize);
/**
* 统计总数
*/
long count();
/**
* 插入
*/
int insert(User user);
/**
* 更新(动态 SQL)
*/
int update(User user);
/**
* 逻辑删除
*/
int deleteById(Long id);
}
4.9 resources/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">
<mapper namespace="com.example.demo.mapper.UserMapper">
<!--
结果映射
将数据库字段名映射到实体类属性名
开启 map-underscore-to-camel-case 后,大部分可自动映射
这里显式定义是为了演示
-->
<resultMap id="BaseResultMap" type="com.example.demo.entity.User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="email" property="email"/>
<result column="age" property="age"/>
<result column="phone" property="phone"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
<result column="deleted" property="deleted"/>
</resultMap>
<!-- 基础列,避免重复书写 -->
<sql id="Base_Column_List">
id, username, email, age, phone, create_time, update_time, deleted
</sql>
<!-- 根据 ID 查询 -->
<select id="selectById" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM t_user
WHERE id = #{id} AND deleted = 0
</select>
<!-- 查询所有 -->
<select id="selectAll" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM t_user
WHERE deleted = 0
ORDER BY create_time DESC
</select>
<!--
条件查询(动态 SQL)
使用 <where> 标签自动处理 WHERE 和 AND
如果所有条件都不满足,不会生成 WHERE 子句
-->
<select id="selectByCondition" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM t_user
<where>
deleted = 0
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="minAge != null">
AND age >= #{minAge}
</if>
<if test="maxAge != null">
AND age <= #{maxAge}
</if>
</where>
ORDER BY create_time DESC
</select>
<!-- 分页查询 -->
<select id="selectByPage" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM t_user
WHERE deleted = 0
ORDER BY create_time DESC
LIMIT #{offset}, #{pageSize}
</select>
<!-- 统计总数 -->
<select id="count" resultType="long">
SELECT COUNT(*) FROM t_user WHERE deleted = 0
</select>
<!--
插入
useGeneratedKeys: 使用数据库自增主键
keyProperty: 将生成的主键赋值给实体类的 id 属性
-->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO t_user (username, email, age, phone)
VALUES (#{username}, #{email}, #{age}, #{phone})
</insert>
<!--
更新(动态 SQL)
<set> 标签自动处理 SET 和逗号
只更新传入的非 null 字段
-->
<update id="update">
UPDATE t_user
<set>
<if test="username != null">username = #{username},</if>
<if test="email != null">email = #{email},</if>
<if test="age != null">age = #{age},</if>
<if test="phone != null">phone = #{phone},</if>
</set>
WHERE id = #{id} AND deleted = 0
</update>
<!-- 逻辑删除 -->
<update id="deleteById">
UPDATE t_user SET deleted = 1 WHERE id = #{id}
</update>
</mapper>
4.10 Service 层 UserService.java
java
package com.example.demo.service;
import com.example.demo.common.PageResult;
import com.example.demo.common.Result;
import com.example.demo.entity.User;
import com.example.demo.exception.BusinessException;
import com.example.demo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
/**
* 根据 ID 查询
*/
public User getById(Long id) {
if (id == null || id <= 0) {
throw new BusinessException(400, "用户ID必须大于0");
}
User user = userMapper.selectById(id);
if (user == null) {
throw new BusinessException(404, "用户不存在");
}
return user;
}
/**
* 查询所有
*/
public List<User> findAll() {
return userMapper.selectAll();
}
/**
* 条件查询
*/
public List<User> findByCondition(String username, Integer minAge, Integer maxAge) {
return userMapper.selectByCondition(username, minAge, maxAge);
}
/**
* 分页查询
*/
public PageResult<User> findByPage(Integer pageNum, Integer pageSize) {
if (pageNum == null || pageNum < 1) pageNum = 1;
if (pageSize == null || pageSize < 1) pageSize = 10;
int offset = (pageNum - 1) * pageSize;
List<User> list = userMapper.selectByPage(offset, pageSize);
long total = userMapper.count();
return PageResult.of(total, list, pageNum, pageSize);
}
/**
* 新增(事务)
*/
@Transactional
public User create(User user) {
if (user == null) {
throw new BusinessException(400, "用户信息不能为空");
}
if (user.getUsername() == null || user.getUsername().trim().isEmpty()) {
throw new BusinessException(400, "用户名不能为空");
}
userMapper.insert(user);
return user;
}
/**
* 更新(事务)
*/
@Transactional
public User update(User user) {
if (user == null || user.getId() == null) {
throw new BusinessException(400, "用户ID不能为空");
}
int rows = userMapper.update(user);
if (rows == 0) {
throw new BusinessException(404, "用户不存在或已被删除");
}
return user;
}
/**
* 删除(事务)
*/
@Transactional
public void delete(Long id) {
if (id == null || id <= 0) {
throw new BusinessException(400, "用户ID必须大于0");
}
int rows = userMapper.deleteById(id);
if (rows == 0) {
throw new BusinessException(404, "用户不存在");
}
}
}
4.11 Controller 层 UserController.java
java
package com.example.demo.controller;
import com.example.demo.common.PageResult;
import com.example.demo.common.Result;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 用户控制器
*/
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
/**
* 根据 ID 查询
* GET /api/users/1
*/
@GetMapping("/{id}")
public Result<User> getById(@PathVariable Long id) {
return Result.success(userService.getById(id));
}
/**
* 查询所有
* GET /api/users
*/
@GetMapping
public Result<List<User>> findAll() {
return Result.success(userService.findAll());
}
/**
* 条件查询
* GET /api/users/search?username=张&minAge=20&maxAge=30
*/
@GetMapping("/search")
public Result<List<User>> search(
@RequestParam(required = false) String username,
@RequestParam(required = false) Integer minAge,
@RequestParam(required = false) Integer maxAge) {
return Result.success(userService.findByCondition(username, minAge, maxAge));
}
/**
* 分页查询
* GET /api/users/page?pageNum=1&pageSize=2
*/
@GetMapping("/page")
public Result<PageResult<User>> page(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
return Result.success(userService.findByPage(pageNum, pageSize));
}
/**
* 新增
* POST /api/users
*/
@PostMapping
public Result<User> create(@RequestBody User user) {
return Result.success(userService.create(user));
}
/**
* 更新
* PUT /api/users
*/
@PutMapping
public Result<User> update(@RequestBody User user) {
return Result.success(userService.update(user));
}
/**
* 删除
* DELETE /api/users/1
*/
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
userService.delete(id);
return Result.success();
}
}
4.12 启动类 DemoApplication.java
java
package com.example.demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.example.demo.mapper") // 添加这行
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
System.out.println("=== MyBatis Demo 启动成功 ===");
System.out.println("访问地址: http://localhost:8080");
}
}
五、运行与测试
5.1 启动项目
在 IDEA 中右键 DemoApplication.java → Run,或命令行:
bash
mvn spring-boot:run
看到控制台输出 SQL 即表示成功:
plain
==> Preparing: SELECT id, username, email, age, phone, create_time, update_time, deleted FROM t_user WHERE deleted = 0 ORDER BY create_time DESC
==> Parameters:
<== Columns: id, username, email, age, phone, create_time, update_time, deleted
<== Row: 1, 张三, zhangsan@example.com, 25, 13800138001, 2026-06-15 10:00:00, 2026-06-15 10:00:00, 0
5.2 接口测试
bash
# 1. 查询所有用户
curl http://localhost:8080/api/users
# 2. 根据 ID 查询
curl http://localhost:8080/api/users/1
# 3. 条件查询(模糊匹配用户名,年龄范围)
curl "http://localhost:8080/api/users/search?username=张&minAge=20&maxAge=30"
# 4. 分页查询
curl "http://localhost:8080/api/users/page?pageNum=1&pageSize=2"
# 5. 新增用户
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"username":"周八","email":"zhouba@example.com","age":40,"phone":"13800138006"}'
# 6. 更新用户(只传需要改的字段)
curl -X PUT http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"id":1,"username":"张三(已修改)","age":26}'
# 7. 删除用户(逻辑删除)
curl -X DELETE http://localhost:8080/api/users/1
# 8. 验证删除后查不到
curl http://localhost:8080/api/users/1
# 返回:{"code":404,"message":"用户不存在",...}
六、注解方式对比(快速参考)
上面的 Demo 使用 XML 方式,简单场景也可用注解:
java
@Mapper
public interface UserMapperAnnotation {
@Select("SELECT * FROM t_user WHERE id = #{id} AND deleted = 0")
User selectById(Long id);
@Select("SELECT * FROM t_user WHERE deleted = 0")
List<User> selectAll();
@Insert("INSERT INTO t_user (username, email, age, phone) VALUES (#{username}, #{email}, #{age}, #{phone})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);
@Update("UPDATE t_user SET deleted = 1 WHERE id = #{id}")
int deleteById(Long id);
}
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 注解 | 简单 CRUD | 简洁,一眼看到 SQL | 复杂 SQL 难维护 |
| XML | 复杂查询、动态 SQL | 语法高亮,便于管理 | 需要维护两个文件 |
七、核心知识点总结
| 知识点 | 要点 |
|---|---|
#{} vs ${} |
#{} 预编译防注入;${} 直接拼接,用于动态表名/列名 |
resultMap |
自定义字段与属性映射关系 |
<where> |
自动处理 WHERE 和 AND,避免语法错误 |
<set> |
自动处理 SET 和逗号,动态更新 |
<if> |
条件判断,test 属性写 OGNL 表达式 |
useGeneratedKeys |
获取数据库自增主键 |
@Transactional |
声明式事务,方法异常自动回滚 |
八、常见错误排查
| 错误 | 原因 | 解决 |
|---|---|---|
Invalid bound statement |
Mapper 接口与 XML 命名空间不匹配 | 检查 namespace |
Property 'sqlSessionFactory' not found |
数据源配置错误 | 检查 application.yml |
| SQL 不打印 | 日志配置未开启 | 添加 log-impl 配置 |
| 返回 null | 字段名与属性名不匹配 | 开启驼峰转换或写 resultMap |
九、面试题自检
| 问题 | 答案 |
|---|---|
| MyBatis 与 Hibernate 区别? | MyBatis SQL 可控,Hibernate 自动生成 |
#{} 与 ${} 区别? |
#{} 预编译防注入,${} 直接拼接 |
| 动态 SQL 标签有哪些? | <if>、<where>、<set>、<foreach>、<choose> |
| 一对多怎么查? | <resultMap> + <collection> |
| 延迟加载怎么配? | fetchType="lazy" + 代理对象 |