加入 Maven 依赖:MySQL 驱动 + MyBatis
在 Spring Boot 项目中,使用 MyBatis 操作 MySQL 数据库,首先需要在 pom.xml 文件中引入相关依赖。以下是核心依赖配置:
xml
<!-- MySQL 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MyBatis Spring Boot Starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!-- Spring Boot Test(单元测试需要) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
依赖说明:
mysql-connector-j:MySQL 官方 JDBC 驱动,scope设为runtime,仅在运行时需要。mybatis-spring-boot-starter:MyBatis 官方的 Spring Boot 起步依赖,自动配置SqlSessionFactory、SqlSessionTemplate等核心组件。spring-boot-starter-test:Spring Boot 测试框架,包含 JUnit 5、Mockito 等,用于编写单元测试。
配置数据库连接
yml 配置
在 application.yml 中配置 MySQL 数据源连接信息。XML 方式与注解方式的最大区别在于 必须配置 mapper-locations,指定 XML 映射文件的位置:
yml
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mybatis_test?characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
username: 数据库账号
password: 数据库密码
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
# 配置 mybatis xml 的文件路径,在 resources/mapper 创建所有表的 xml 文件
mapper-locations: classpath:mapper/**Mapper.xml
configuration: # 配置打印 MyBatis日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true #配置驼峰自动转换
准备数据库表
在 MySQL 中执行以下 SQL,创建测试数据库和用户表:
sql
-- 创建数据库
DROP DATABASE IF EXISTS mybatis_test;
CREATE DATABASE mybatis_test DEFAULT CHARACTER SET utf8mb4;
-- 使用数据库
USE mybatis_test;
-- =============================================
-- 表名: user_info
-- 描述: 用户基础信息表,存储系统用户的核心数据
-- 注意事项:
-- 1. 密码应存储加密后的密文,VARCHAR(50)根据加密算法调整
-- 2. username 建议添加唯一索引,保证用户唯一
-- 3. 性别采用 TINYINT 存储,节省空间且含义清晰
-- 4. 默认字符集 utf8mb4,兼容emoji等特殊字符
-- 5. 自动维护 create_time 和 update_time,减少应用层负担
-- =============================================
DROP TABLE IF EXISTS user_info;
CREATE TABLE `user_info` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键ID,自增整数,作为用户唯一物理标识',
`username` VARCHAR(50) NOT NULL COMMENT '用户名,登录凭证,实际使用时应添加唯一索引',
`password` VARCHAR(50) NOT NULL COMMENT '加密后的密码,根据加密算法调整长度(如使用bcrypt建议VARCHAR(255))',
`age` INT DEFAULT NULL COMMENT '年龄,允许空值,便于隐私保护或未知场景',
`gender` TINYINT DEFAULT '0' COMMENT '性别:1-男,2-女,0-未知。使用TINYINT节省存储',
`phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号,格式如13800138000,允许空值',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间,由数据库自动填充',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间,每次修改行时自动更新',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表';
-- 添加测试数据
INSERT INTO user_info (username, `password`, age, gender, phone) VALUES
('admin', 'admin', 18, 1, '18612340001'),
('zhangsan', 'zhangsan', 20, 1, '18612340002'),
('lisi', 'lisi', 22, 2, '18612340003');
创建实体类 UserInfo
java
@Data
public class UserInfo {
private Integer id;
private String username;
private String password;
private Integer age;
private Integer gender;
private String phone;
private Date createTime;
private Date updateTime;
}
XML 方式核心:三层组件
XML 方式实现 CRUD 需要三层组件协同工作:
| 层级 | 文件 | 位置 | 作用 |
|---|---|---|---|
| Mapper 接口 | UserInfoMapperXML.java |
src/main/java/.../mapper/ |
定义方法签名,与 XML 绑定 |
| Mapper XML | UserInfoMapperXML.xml |
src/main/resources/mapper/ |
编写 SQL 语句 |
| 实体类 | UserInfo.java |
src/main/java/.../entity/ |
数据载体(已创建) |
绑定规则:
- Mapper 接口的 全限定类名 = XML 的
namespace - Mapper 接口的 方法名 = XML 中
<select>/<insert>/<update>/<delete>的id - 接口方法 参数类型 = XML 中 SQL 标签的
parameterType(可选,MyBatis 可自动推断) - 接口方法 返回类型 = XML 中 SQL 标签的
resultType/resultMap
创建 Mapper 接口
在 src/main/java/.../mapper/ 路径下创建 UserInfoMapperXML.java:
java
package com.example.mapper;
import com.example.entity.UserInfo;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* UserInfo 的 Mapper 接口(XML 方式)
* ⚠️ 接口方法与 XML 中的 id 必须一一对应
*/
public interface UserInfoMapperXML {
// ========== 查询 ==========
/** 1. 按主键查询 */
UserInfo selectById(Integer id);
/** 2. 查询全部 */
List<UserInfo> selectAll();
/** 3. 多条件查询(按用户名和年龄) */
List<UserInfo> selectByCondition(
@Param("username") String username,
@Param("minAge") Integer minAge);
/** 4. 模糊查询(LIKE) */
List<UserInfo> selectByKeyword(@Param("keyword") String keyword);
/** 5. 排序查询 */
List<UserInfo> selectAllOrdered(
@Param("orderColumn") String orderColumn,
@Param("orderDirection") String orderDirection);
/** 6. 分页查询 */
List<UserInfo> selectByPage(
@Param("offset") Integer offset,
@Param("limit") Integer limit);
/** 7. 聚合查询 - 统计总数 */
Integer countAll();
/** 8. IN 子句批量查询 */
List<UserInfo> selectByIds(@Param("idList") List<Integer> idList);
/** 9. 返回单个字段 */
List<String> selectAllUsernames();
/** 10. 返回 Map */
List<Map<String, Object>> selectAllAsMapList();
// ========== 插入 ==========
/** 11. 插入完整实体对象 */
Integer insertUser(UserInfo userInfo);
/** 12. 插入并回填自增主键 */
Integer insertUserAndReturnId(UserInfo userInfo);
/** 13. 逐字段插入 */
Integer insertUserByFields(
@Param("username") String username,
@Param("password") String password,
@Param("age") Integer age,
@Param("gender") Integer gender,
@Param("phone") String phone);
/** 14. 批量插入 */
Integer batchInsert(@Param("userList") List<UserInfo> userList);
// ========== 更新 ==========
/** 15. 按主键更新全部字段 */
Integer updateById(UserInfo userInfo);
/** 16. 按主键选择性更新 */
Integer updatePassword(
@Param("id") Integer id,
@Param("newPassword") String newPassword);
/** 17. 条件更新 */
Integer updateAgeByCondition(
@Param("username") String username,
@Param("password") String password,
@Param("newAge") Integer newAge);
/** 18. CASE WHEN 批量更新 */
Integer batchUpdateAge(@Param("userList") List<UserInfo> userList);
// ========== 删除 ==========
/** 19. 按主键删除 */
Integer deleteById(Integer id);
/** 20. 条件删除(按用户名) */
Integer deleteByUsername(@Param("username") String username);
/** 21. 多条件删除(按年龄范围) */
Integer deleteByAgeRange(
@Param("minAge") Integer minAge,
@Param("maxAge") Integer maxAge);
/** 22. 批量删除 */
Integer deleteByIds(@Param("idList") List<Integer> idList);
/** 23. 清空表 */
Integer deleteAll();
}
对比注解方式的关键区别:
- XML 方式:接口只有方法声明,不需要
@Select、@Insert等注解,SQL 写在 XML 文件中。 - 注解方式:SQL 直接写在接口方法的注解上。
创建 Mapper XML 文件
在 src/main/resources/mapper/ 路径下创建 UserInfoMapperXML.xml。
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 = Mapper 接口的全限定类名 -->
<mapper namespace="com.example.mapper.UserInfoMapperXML">
<!-- SQL 语句将写在这里 -->
</mapper>
⚠️ 关键约束 :
namespace必须与 Mapper 接口的全限定类名 完全一致,否则启动时报BindingException。
标签速查表
| XML 标签 | 对应 SQL | 对应接口注解 | 用途 |
|---|---|---|---|
<select> |
SELECT | @Select |
查询操作 |
<insert> |
INSERT | @Insert |
插入操作 |
<update> |
UPDATE | @Update |
更新操作 |
<delete> |
DELETE | @Delete |
删除操作 |
<resultMap> |
--- | @Results |
自定义结果映射 |
<sql> |
--- | --- | SQL 片段复用 |
<include> |
--- | --- | 引用 SQL 片段 |
XML 属性速查
| 属性 | 说明 | 示例 |
|---|---|---|
id |
方法名,与接口方法一一对应 | id="selectById" |
resultType |
返回值类型(全限定类名或别名) | resultType="com.example.entity.UserInfo" |
resultMap |
自定义结果映射 ID | resultMap="BaseResultMap" |
parameterType |
参数类型(可选,MyBatis 自动推断) | parameterType="Integer" |
useGeneratedKeys |
是否使用 JDBC 自增主键 | useGeneratedKeys="true" |
keyProperty |
自增主键回填到哪个属性 | keyProperty="id" |
keyColumn |
数据库主键列名 | keyColumn="id" |
flushCache |
执行后是否清空缓存 | flushCache="true" |
statementType |
Statement 类型 | statementType="PREPARED" |
查询 SQL:<select> 标签
1. 按主键查询
xml
<!--
id = 接口方法名
resultType = 返回值类型(全限定类名)
parameterType = 参数类型(可选,这里不写 MyBatis 也会自动推断)
-->
<select id="selectById" resultType="com.example.entity.UserInfo">
SELECT * FROM user_info WHERE id = #{id}
</select>
调用示例:
java
UserInfo user = userInfoMapperXML.selectById(1);
// 执行 SQL:SELECT * FROM user_info WHERE id = 1
// 结果:UserInfo{id=1, username="admin", age=18, ...}
2. 查询全部
xml
<select id="selectAll" resultType="com.example.entity.UserInfo">
SELECT * FROM user_info
</select>
调用示例:
java
List<UserInfo> list = userInfoMapperXML.selectAll();
// SQL:SELECT * FROM user_info
// 结果:[UserInfo{id=1,...}, UserInfo{id=2,...}, UserInfo{id=3,...}]
3. 多条件查询(@Param 多参数)
xml
<!--
@Param 指定的名称直接在 #{} 中使用
parameterType 不需要写,MyBatis 自动处理多参数
-->
<select id="selectByCondition" resultType="com.example.entity.UserInfo">
SELECT * FROM user_info
WHERE username LIKE CONCAT('%', #{username}, '%')
AND age >= #{minAge}
</select>
调用示例:
java
List<UserInfo> users = userInfoMapperXML.selectByCondition("zhang", 18);
// SQL:SELECT * FROM user_info WHERE username LIKE '%zhang%' AND age >= 18
// 结果:[UserInfo{username="zhangsan", age=20, ...}]
4. LIKE 模糊查询
xml
<!-- ✅ 安全写法:用 CONCAT + #{} 防止 SQL 注入 -->
<select id="selectByKeyword" resultType="com.example.entity.UserInfo">
SELECT * FROM user_info WHERE username LIKE CONCAT('%', #{keyword}, '%')
</select>
<!-- ✅ 前缀匹配 -->
<select id="selectByPhonePrefix" resultType="com.example.entity.UserInfo">
SELECT * FROM user_info WHERE phone LIKE CONCAT(#{prefix}, '%')
</select>
调用示例:
java
List<UserInfo> users = userInfoMapperXML.selectByKeyword("admin");
// SQL:WHERE username LIKE '%admin%' → 匹配 "admin"
// ⚠️ 不推荐写法(有 SQL 注入风险):
// SELECT * FROM user_info WHERE username LIKE '%${keyword}%'
5. 排序查询(ORDER BY)
xml
<!-- ⚠️ ORDER BY 的列名和排序方向必须使用 ${},不能用 #{} -->
<select id="selectAllOrdered" resultType="com.example.entity.UserInfo">
SELECT * FROM user_info ORDER BY ${orderColumn} ${orderDirection}
</select>
调用示例:
java
List<UserInfo> users = userInfoMapperXML.selectAllOrdered("age", "DESC");
// SQL:SELECT * FROM user_info ORDER BY age DESC
List<UserInfo> users2 = userInfoMapperXML.selectAllOrdered("create_time", "ASC");
// SQL:SELECT * FROM user_info ORDER BY create_time ASC
⚠️ 必须使用
${},因为 JDBC 的?占位符不能替代 SQL 关键字(列名、排序方向)。必须在业务层对
orderColumn做白名单校验,防止 SQL 注入:
javaprivate static final Set<String> ALLOWED_COLUMNS = Set.of("id", "username", "age", "gender", "create_time"); if (!ALLOWED_COLUMNS.contains(orderColumn)) { throw new IllegalArgumentException("非法的排序列: " + orderColumn); }
6. 分页查询(LIMIT)
xml
<select id="selectByPage" resultType="com.example.entity.UserInfo">
SELECT * FROM user_info LIMIT #{offset}, #{limit}
</select>
调用示例:
java
// 第 1 页,每页 2 条
List<UserInfo> page1 = userInfoMapperXML.selectByPage(0, 2);
// SQL:SELECT * FROM user_info LIMIT 0, 2 → [admin, zhangsan]
// 第 2 页
List<UserInfo> page2 = userInfoMapperXML.selectByPage(2, 2);
// SQL:SELECT * FROM user_info LIMIT 2, 2 → [lisi]
7. 聚合查询(COUNT / MAX / MIN / AVG)
xml
<!-- 统计总数 -->
<select id="countAll" resultType="java.lang.Integer">
SELECT COUNT(*) FROM user_info
</select>
<!-- 按性别统计 -->
<select id="countByGender" resultType="Integer">
SELECT COUNT(*) FROM user_info WHERE gender = #{gender}
</select>
<!-- 最大年龄 -->
<select id="selectMaxAge" resultType="Integer">
SELECT MAX(age) FROM user_info
</select>
<!-- 最小年龄 -->
<select id="selectMinAge" resultType="Integer">
SELECT MIN(age) FROM user_info
</select>
调用示例:
java
Integer total = userInfoMapperXML.countAll(); // → 3
Integer maleCount = userInfoMapperXML.countByGender(1); // → 2
Integer maxAge = userInfoMapperXML.selectMaxAge(); // → 22
8. IN 子句批量查询(<foreach>)
xml
<!--
collection = 集合参数名(对应 @Param 的值或默认 param1)
item = 遍历时的单个元素名称
open = 循环开始前拼接的字符
separator = 分隔符
close = 循环结束后拼接的字符
-->
<select id="selectByIds" resultType="com.example.entity.UserInfo">
SELECT * FROM user_info
WHERE id IN
<foreach item="id" collection="idList" open="(" separator="," close=")">
#{id}
</foreach>
</select>
调用示例:
java
List<Integer> ids = Arrays.asList(1, 3);
List<UserInfo> users = userInfoMapperXML.selectByIds(ids);
// SQL:SELECT * FROM user_info WHERE id IN (1, 3)
// 结果:[UserInfo{id=1}, UserInfo{id=3}]
XML 方式支持
<foreach>不需要<script>包裹,这是与注解方式的重要区别。
9. 返回单个字段值
xml
<!-- resultType 写字段对应的 Java 类型 -->
<select id="selectAllUsernames" resultType="java.lang.String">
SELECT username FROM user_info
</select>
<select id="selectPhoneById" resultType="String">
SELECT phone FROM user_info WHERE id = #{id}
</select>
10. 返回 Map
xml
<!-- 单条记录返回 Map<String, Object>,key 是列名,value 是列值 -->
<select id="selectByIdAsMap" resultType="java.util.HashMap">
SELECT * FROM user_info WHERE id = #{id}
</select>
<!-- 多条记录返回 List<Map<String, Object>> -->
<select id="selectAllAsMapList" resultType="java.util.HashMap">
SELECT * FROM user_info
</select>
调用示例:
java
Map<String, Object> map = userInfoMapperXML.selectByIdAsMap(1);
// → {id=1, username=admin, password=admin, age=18, gender=1, ...}
插入 SQL:<insert> 标签
1. 插入完整实体对象
xml
<!--
parameterType 可省略,MyBatis 自动推断
#{} 中直接写实体类的属性名
-->
<insert id="insertUser" parameterType="com.example.entity.UserInfo">
INSERT INTO user_info (username, password, age, gender, phone)
VALUES (#{username}, #{password}, #{age}, #{gender}, #{phone})
</insert>
调用示例:
java
UserInfo newUser = new UserInfo();
newUser.setUsername("wangwu");
newUser.setPassword("123456");
newUser.setAge(25);
newUser.setGender(1);
newUser.setPhone("18612340004");
Integer rows = userInfoMapperXML.insertUser(newUser);
// SQL:INSERT INTO user_info (username, password, age, gender, phone)
// VALUES ('wangwu', '123456', 25, 1, '18612340004')
// 返回值:1
// ⚠️ newUser.getId() 仍为 null(未回填自增主键)
2. 插入并回填自增主键
xml
<!--
useGeneratedKeys="true" → 使用 JDBC getGeneratedKeys() 获取自增主键
keyProperty="id" → 回填到 UserInfo 对象的 id 属性
keyColumn="id" → 数据库主键列名
-->
<insert id="insertUserAndReturnId"
useGeneratedKeys="true"
keyProperty="id"
keyColumn="id">
INSERT INTO user_info (username, password, age, gender, phone)
VALUES (#{username}, #{password}, #{age}, #{gender}, #{phone})
</insert>
调用示例:
java
UserInfo newUser = new UserInfo();
newUser.setUsername("zhaoliu");
newUser.setPassword("123456");
newUser.setAge(30);
newUser.setGender(1);
newUser.setPhone("18612340005");
userInfoMapperXML.insertUserAndReturnId(newUser);
System.out.println(newUser.getId()); // → 4(数据库自动分配的 ID)
关键 :
useGeneratedKeys只对自增主键有效。如果是手动赋值的主键(如 UUID),不需要此属性。
3. 逐字段插入
xml
<!-- 使用 @Param 指定的名称 -->
<insert id="insertUserByFields">
INSERT INTO user_info (username, password, age, gender, phone)
VALUES (#{username}, #{password}, #{age}, #{gender}, #{phone})
</insert>
调用示例:
java
userInfoMapperXML.insertUserByFields("zhouba", "123456", 35, 1, "18612340007");
// ⚠️ 逐字段传参时无法回填 ID(没有实体对象承载)
4. 批量插入(<foreach>)
xml
<!--
separator="," 每组值之间用逗号分隔
(#{user.username}, ...) 通过 item 前缀访问对象属性
-->
<insert id="batchInsert">
INSERT INTO user_info (username, password, age, gender, phone) VALUES
<foreach item="user" collection="userList" separator=",">
(#{user.username}, #{user.password}, #{user.age}, #{user.gender}, #{user.phone})
</foreach>
</insert>
调用示例:
java
List<UserInfo> users = Arrays.asList(
buildUser("userA", "pwdA", 20, 1, "10001"),
buildUser("userB", "pwdB", 21, 2, "10002"),
buildUser("userC", "pwdC", 22, 1, "10003")
);
Integer rows = userInfoMapperXML.batchInsert(users);
// SQL:INSERT INTO user_info (...) VALUES (...), (...), (...)
// 返回值:3
// ⚠️ 批量插入时 useGeneratedKeys 行为因 JDBC 驱动而异
5. 插入默认值
xml
<!--
age 和 phone 有 DEFAULT NULL,gender 有 DEFAULT 0
create_time / update_time 由数据库自动填充
插入时省略这些字段,数据库使用默认值
-->
<insert id="insertUserMinimal"
useGeneratedKeys="true"
keyProperty="id"
keyColumn="id">
INSERT INTO user_info (username, password)
VALUES (#{username}, #{password})
</insert>
更新 SQL:<update> 标签
1. 按主键更新全部字段
xml
<update id="updateById" parameterType="com.example.entity.UserInfo">
UPDATE user_info SET
username = #{username},
password = #{password},
age = #{age},
gender = #{gender},
phone = #{phone}
WHERE id = #{id}
</update>
调用示例:
java
UserInfo user = userInfoMapperXML.selectById(1);
user.setAge(26);
user.setPhone("18600000000");
Integer rows = userInfoMapperXML.updateById(user);
// SQL:UPDATE user_info SET username='admin', password='admin',
// age=26, gender=1, phone='18600000000' WHERE id=1
// 返回值:1
// update_time 由数据库 ON UPDATE CURRENT_TIMESTAMP 自动更新
2. 按主键选择性更新
xml
<!-- 只更新密码 -->
<update id="updatePassword">
UPDATE user_info SET password = #{newPassword}
WHERE id = #{id}
</update>
<!-- 只更新手机号 -->
<update id="updatePhone">
UPDATE user_info SET phone = #{phone}
WHERE id = #{id}
</update>
调用示例:
java
userInfoMapperXML.updatePassword(1, "newPwd123");
// SQL:UPDATE user_info SET password = 'newPwd123' WHERE id = 1
userInfoMapperXML.updatePhone(2, "18699990000");
// SQL:UPDATE user_info SET phone = '18699990000' WHERE id = 2
3. 条件更新(WHERE 多条件)
xml
<update id="updateAgeByCondition">
UPDATE user_info SET age = #{newAge}
WHERE username = #{username} AND password = #{password}
</update>
调用示例:
java
Integer rows = userInfoMapperXML.updateAgeByCondition("zhangsan", "zhangsan", 30);
// SQL:UPDATE user_info SET age = 30 WHERE username='zhangsan' AND password='zhangsan'
// 返回值:1(匹配到并更新),0(用户名或密码不匹配)
4. CASE WHEN 批量更新
xml
<!--
一条 SQL 更新多行的不同值,性能优于循环逐条更新
原理:CASE id WHEN 1 THEN 25 WHEN 2 THEN 30 END
-->
<update id="batchUpdateAge">
UPDATE user_info SET age =
<foreach item="user" collection="userList" open="CASE id" close="END">
WHEN #{user.id} THEN #{user.age}
</foreach>
WHERE id IN
<foreach item="user" collection="userList" open="(" separator="," close=")">
#{user.id}
</foreach>
</update>
调用示例:
java
List<UserInfo> updates = Arrays.asList(
buildUserWithId(1, 25),
buildUserWithId(2, 30),
buildUserWithId(3, 35)
);
userInfoMapperXML.batchUpdateAge(updates);
// SQL:UPDATE user_info SET age = CASE id
// WHEN 1 THEN 25 WHEN 2 THEN 30 WHEN 3 THEN 35 END
// WHERE id IN (1, 2, 3)
删除 SQL:<delete> 标签
1. 按主键删除
xml
<delete id="deleteById" parameterType="Integer">
DELETE FROM user_info WHERE id = #{id}
</delete>
调用示例:
java
Integer rows = userInfoMapperXML.deleteById(5);
// SQL:DELETE FROM user_info WHERE id = 5
// 返回值:1(删除成功),0(ID 不存在)
2. 条件删除
xml
<!-- 按用户名删除 -->
<delete id="deleteByUsername">
DELETE FROM user_info WHERE username = #{username}
</delete>
<!-- 按年龄范围删除 -->
<delete id="deleteByAgeRange">
DELETE FROM user_info
WHERE age >= #{minAge} AND age <= #{maxAge}
<!-- 注意:XML 中 < 和 <= 需转义,或用 CDATA 包裹 -->
</delete>
<!-- 使用 CDATA 包裹特殊字符(推荐) -->
<delete id="deleteByAgeRangeSafe">
DELETE FROM user_info
WHERE <![CDATA[ age >= #{minAge} AND age <= #{maxAge} ]]>
</delete>
3. 批量删除(IN 子句)
xml
<delete id="deleteByIds">
DELETE FROM user_info
WHERE id IN
<foreach item="id" collection="idList" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
调用示例:
java
List<Integer> ids = Arrays.asList(4, 5, 6);
Integer rows = userInfoMapperXML.deleteByIds(ids);
// SQL:DELETE FROM user_info WHERE id IN (4, 5, 6)
// 返回值:实际删除的行数(如果 id=6 不存在,只删 2 行,返回 2)
4. 清空表
xml
<!-- ⚠️ 危险操作:删除全表数据 -->
<delete id="deleteAll">
DELETE FROM user_info
</delete>
调用示例:
java
Integer rows = userInfoMapperXML.deleteAll();
// SQL:DELETE FROM user_info
// 返回值:删除的总行数
TRUNCATE TABLE是 DDL,MyBatis 的<delete>不能执行。如需 TRUNCATE,必须通过 JDBC 原生方式。
动态 SQL
XML 方式相比注解方式的最大优势是 动态 SQL 支持更完善------不需要 <script> 包裹,直接使用 MyBatis 提供的动态标签。
<if> 条件判断
xml
<!-- 根据传入参数动态拼接 WHERE 条件 -->
<select id="selectByDynamicCondition" resultType="com.example.entity.UserInfo">
SELECT * FROM user_info
WHERE 1=1
<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>
<if test="gender != null">
AND gender = #{gender}
</if>
</select>
调用示例:
java
// 只传 username
List<UserInfo> r1 = mapper.selectByDynamicCondition("zhang", null, null, null);
// SQL:SELECT * FROM user_info WHERE 1=1 AND username LIKE '%zhang%'
// 传 username + age
List<UserInfo> r2 = mapper.selectByDynamicCondition("zhang", 18, 30, null);
// SQL:SELECT * FROM user_info WHERE 1=1 AND username LIKE '%zhang%'
// AND age >= 18 AND age <= 30
<where> 智能去除 AND/OR
xml
<!--
<where> 标签的作用:
1. 如果内部有条件,自动添加 WHERE 关键字
2. 自动去除第一个 AND 或 OR(解决 WHERE 1=1 的冗余问题)
-->
<select id="selectByWhere" resultType="com.example.entity.UserInfo">
SELECT * FROM user_info
<where>
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="minAge != null">
AND age >= #{minAge}
</if>
<if test="gender != null">
AND gender = #{gender}
</if>
</where>
</select>
使用
<where>后不再需要WHERE 1=1,MyBatis 会自动处理首条 AND。
<set> 智能去除逗号
xml
<!--
<set> 标签的作用:
1. 自动添加 SET 关键字
2. 自动去除末尾多余的逗号
-->
<update id="updateSelective">
UPDATE user_info
<set>
<if test="username != null and username != ''">
username = #{username},
</if>
<if test="password != null and password != ''">
password = #{password},
</if>
<if test="age != null">
age = #{age},
</if>
<if test="gender != null">
gender = #{gender},
</if>
<if test="phone != null and phone != ''">
phone = #{phone},
</if>
</set>
WHERE id = #{id}
</update>
调用示例:
java
UserInfo user = new UserInfo();
user.setId(1);
user.setAge(26);
// 只传 age,只更新 age 字段
userInfoMapperXML.updateSelective(user);
// SQL:UPDATE user_info SET age = 26 WHERE id = 1
<foreach> 集合遍历
xml
<!--
collection 支持的参数类型:
- List → list 或 @Param 指定的名称
- Array → array
- Set → collection
- Map → _parameter
item :当前遍历元素
index :当前索引(可选)
open :开头拼接字符
close :结尾拼接字符
separator:元素间分隔符
-->
<select id="selectByMultipleIds" resultType="com.example.entity.UserInfo">
SELECT * FROM user_info WHERE id IN
<foreach item="id" index="index" collection="idList"
open="(" separator="," close=")">
#{id}
</foreach>
</select>
<choose> / <when> / <otherwise> 多分支选择
xml
<!-- 类似 Java 的 switch-case-default -->
<select id="selectByChoose" resultType="com.example.entity.UserInfo">
SELECT * FROM user_info
<where>
<choose>
<when test="username != null and username != ''">
AND username = #{username}
</when>
<when test="phone != null and phone != ''">
AND phone = #{phone}
</when>
<otherwise>
AND gender = 1
</otherwise>
</choose>
</where>
</select>
调用示例:
java
// username 不为空 → 按 username 查
mapper.selectByChoose("admin", null);
// SQL:SELECT * FROM user_info WHERE username = 'admin'
// username 为空,phone 不为空 → 按 phone 查
mapper.selectByChoose(null, "18612340001");
// SQL:SELECT * FROM user_info WHERE phone = '18612340001'
// 都为空 → otherwise 兜底
mapper.selectByChoose(null, null);
// SQL:SELECT * FROM user_info WHERE gender = 1
<trim> 自定义修剪
xml
<!-- <trim> 是 <where> 和 <set> 的底层实现 -->
<!-- 等价于 <where> -->
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="username != null">AND username = #{username}</if>
<if test="age != null">AND age = #{age}</if>
</trim>
<!-- prefixOverrides="AND |OR " → 去除前缀的 AND 或 OR -->
<!-- 等价于 <set> -->
<trim prefix="SET" suffixOverrides=",">
<if test="username != null">username = #{username},</if>
<if test="age != null">age = #{age},</if>
</trim>
<!-- suffixOverrides="," → 去除后缀的逗号 -->
<trim> 属性 |
作用 | 示例 |
|---|---|---|
prefix |
在内容前添加前缀 | prefix="WHERE" |
suffix |
在内容后添加后缀 | suffix=")" |
prefixOverrides |
去除内容前匹配的字符 | `prefixOverrides="AND |
suffixOverrides |
去除内容后匹配的字符 | suffixOverrides="," |
<sql> + <include> SQL 片段复用
xml
<!-- 定义 SQL 片段 -->
<sql id="Base_Column_List">
id, username, password, age, gender, phone, create_time, update_time
</sql>
<sql id="Where_Condition">
<where>
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="minAge != null">
AND age >= #{minAge}
</if>
</where>
</sql>
<!-- 引用 SQL 片段 -->
<select id="selectWithSqlFragment" resultType="com.example.entity.UserInfo">
SELECT
<include refid="Base_Column_List"/>
FROM user_info
<include refid="Where_Condition"/>
</select>
<include>的refid还可以跨 namespace 引用:refid="com.example.mapper.OtherMapper.SomeSQL"。
<bind> 变量绑定
xml
<!--
使用 OGNL 表达式创建变量,解决数据库函数差异问题
例如 MySQL 的 CONCAT 和 Oracle 的 || 语法不同
-->
<select id="selectByBind" resultType="com.example.entity.UserInfo">
<bind name="pattern" value="'%' + keyword + '%'"/>
SELECT * FROM user_info WHERE username LIKE #{pattern}
</select>
<bind>常用于模糊查询,避免在不同数据库之间切换 CONCAT 语法。
高级特性
<resultMap> 自定义结果映射
当数据库列名与 Java 属性名不一致(且未开启驼峰转换),或需要复杂映射时使用:
xml
<!-- 定义 resultMap -->
<resultMap id="BaseResultMap" type="com.example.entity.UserInfo">
<!-- id 标签:主键映射 -->
<id column="id" property="id" jdbcType="INTEGER"/>
<!-- result 标签:普通字段映射 -->
<result column="username" property="username" jdbcType="VARCHAR"/>
<result column="password" property="password" jdbcType="VARCHAR"/>
<result column="age" property="age" jdbcType="INTEGER"/>
<result column="gender" property="gender" jdbcType="TINYINT"/>
<result column="phone" property="phone" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
</resultMap>
<!-- 使用 resultMap(不是 resultType) -->
<select id="selectByIdWithResultMap" resultMap="BaseResultMap">
SELECT * FROM user_info WHERE id = #{id}
</select>
resultMap vs resultType:
| 场景 | 使用 |
|---|---|
| 列名与属性名一致(或已开启驼峰转换) | resultType |
| 列名与属性名不一致,且未开启驼峰转换 | resultMap |
| 需要嵌套对象映射(association / collection) | resultMap |
| 需要类型转换器(typeHandler) | resultMap |
<selectKey> 获取主键(非自增场景)
xml
<!-- 场景:主键由程序生成(如 UUID),插入前获取 -->
<insert id="insertWithUUID">
<!--
order="BEFORE" → 在 INSERT 之前执行
keyProperty="id" → 将结果设置到 UserInfo.id
-->
<selectKey keyProperty="id" resultType="String" order="BEFORE">
SELECT REPLACE(UUID(), '-', '')
</selectKey>
INSERT INTO user_info (id, username, password)
VALUES (#{id}, #{username}, #{password})
</insert>
<!-- 场景:插入后获取自增主键(不依赖 useGeneratedKeys) -->
<insert id="insertWithSelectKey">
<selectKey keyProperty="id" resultType="Integer" order="AFTER">
SELECT LAST_INSERT_ID()
</selectKey>
INSERT INTO user_info (username, password)
VALUES (#{username}, #{password})
</insert>
XML 中的特殊字符处理
xml
<!-- 方式一:使用 XML 转义符 -->
<!--
< → <
> → >
<= → <=
>= → >=
& → &
" → "
' → '
-->
<select id="selectByAgeRange" resultType="com.example.entity.UserInfo">
SELECT * FROM user_info WHERE age >= #{minAge} AND age <= #{maxAge}
</select>
<!-- 方式二:使用 CDATA 包裹(推荐) -->
<select id="selectByAgeRangeCDATA" resultType="com.example.entity.UserInfo">
SELECT * FROM user_info
WHERE <![CDATA[ age >= #{minAge} AND age <= #{maxAge} ]]>
</select>
推荐使用 CDATA,可读性更好,不容易遗漏转义。
参数传递机制
单参数传递
java
// 接口
UserInfo selectById(Integer id);
xml
<!-- 单参数时,{} 中的名称可以任意指定 -->
<select id="selectById" resultType="com.example.entity.UserInfo">
SELECT * FROM user_info WHERE id = #{任意名称}
</select>
<!-- 两种写法都正确 -->
<select id="selectById" resultType="com.example.entity.UserInfo">
SELECT * FROM user_info WHERE id = #{id}
</select>
多参数传递(使用 @Param)
java
// 接口
List<UserInfo> selectByCondition(
@Param("username") String username,
@Param("minAge") Integer minAge);
xml
<!-- @Param 指定的名称直接使用 -->
<select id="selectByCondition" resultType="com.example.entity.UserInfo">
SELECT * FROM user_info
WHERE username = #{username} AND age >= #{minAge}
</select>
未使用 @Param 时的默认名称:
| 参数位置 | 默认名称(从 0 开始) | 默认名称(从 1 开始) |
|---|---|---|
| 第 1 个参数 | arg0 |
param1 |
| 第 2 个参数 | arg1 |
param2 |
| 第 3 个参数 | arg2 |
param3 |
推荐始终使用
@Param明确指定参数名称,提高代码可读性和可维护性。
传递对象参数
java
// 接口
Integer insertUser(UserInfo userInfo);
xml
<!-- 直接使用属性名 -->
<insert id="insertUser">
INSERT INTO user_info (username, password, age)
VALUES (#{username}, #{password}, #{age})
</insert>
java
// 使用 @Param 为对象加前缀
Integer insertUser(@Param("user") UserInfo userInfo);
xml
<!-- 通过前缀访问属性 -->
<insert id="insertUser">
INSERT INTO user_info (username, password, age)
VALUES (#{user.username}, #{user.password}, #{user.age})
</insert>
${} 与 #{} 的区别
详见:\[${}与 {}的区别]
在 XML 中的使用规则与注解方式完全一致:
| 符号 | 机制 | 防注入 | 适用场景 |
|---|---|---|---|
#{} |
JDBC PreparedStatement ? 占位符 |
✅ 安全 | WHERE 值、INSERT 值、UPDATE 值、LIMIT 值 |
${} |
字符串直接拼接 | ❌ 不安全 | ORDER BY 列名、表名、LIKE(需手动防注入) |
xml
<!-- #{} → 预编译占位符 → 安全 -->
<select id="selectById" resultType="com.example.entity.UserInfo">
SELECT * FROM user_info WHERE id = #{id}
</select>
-- 生成:SELECT * FROM user_info WHERE id = ?
<!-- ${} → 直接拼接 → 必须做白名单校验 -->
<select id="selectOrdered" resultType="com.example.entity.UserInfo">
SELECT * FROM user_info ORDER BY ${column} ${direction}
</select>
-- 生成:SELECT * FROM user_info ORDER BY age DESC
XML 方式 vs 注解方式 对比总结⭐
| 维度 | XML 方式 | 注解方式 |
|---|---|---|
| SQL 位置 | 独立 XML 文件 | 注解字符串中 |
| SQL 与代码分离 | ✅ 是 | ❌ 否 |
| 动态 SQL | 原生支持 <if> <where> 等 |
需 <script> 包裹 |
| SQL 可读性 | ✅ 高(长 SQL 不受限制) | ❌ 低(长 SQL 拼接困难) |
| 可维护性 | ✅ SQL 集中管理 | ❌ 散落在各方法上 |
| SQL 调试 | ✅ 可单独查看 XML | ❌ 嵌在注解中难定位 |
| 配置复杂度 | 需配置 mapper-locations |
无需额外配置 |
| 文件数量 | 接口 + XML(两份) | 仅接口(一份) |
| 适用场景 | 复杂 SQL、大量动态条件 | 简单 CRUD |
| 团队分工 | ✅ DBA 可独立维护 XML | ❌ 需要懂 Java |