跳到主要内容

XML 映射器

XML 映射器是 MyBatis 最核心的部分,用于定义 SQL 语句和映射规则。本章将详细介绍 XML 映射文件的各种元素和使用方法。

映射文件结构

一个完整的 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">

<mapper namespace="com.example.mybatis.mapper.UserMapper">

<!-- 缓存配置 -->
<cache/>

<!-- 结果映射 -->
<resultMap id="BaseResultMap" type="User">
<!-- 映射配置 -->
</resultMap>

<!-- SQL 片段 -->
<sql id="Base_Column_List">
id, username, password, email
</sql>

<!-- 查询语句 -->
<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>

<!-- 插入语句 -->
<insert id="insert" parameterType="User">
INSERT INTO user (username, password) VALUES (#{username}, #{password})
</insert>

<!-- 更新语句 -->
<update id="update" parameterType="User">
UPDATE user SET username = #{username} WHERE id = #{id}
</update>

<!-- 删除语句 -->
<delete id="deleteById">
DELETE FROM user WHERE id = #{id}
</delete>
</mapper>

namespace 命名空间

namespace 是映射文件的唯一标识,通常设置为对应 Mapper 接口的全限定名:

<mapper namespace="com.example.mybatis.mapper.UserMapper">

命名空间的作用

  • 绑定 Mapper 接口,实现接口方法与 SQL 语句的映射
  • 隔离不同映射文件中的 SQL 语句 ID
  • 防止 SQL 语句 ID 冲突

select 查询语句

select 元素用于定义查询语句。

基本用法

<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>

属性说明

属性说明
id命名空间中的唯一标识符,对应 Mapper 接口方法名
parameterType参数类型(可省略,MyBatis 可自动推断)
resultType返回结果类型(全限定名或别名)
resultMap引用外部 resultMap(与 resultType 二选一)
flushCache调用后是否清空本地缓存和二级缓存,默认 false
useCache是否使用二级缓存,默认 true
timeout超时时间(秒)
fetchSize获取数量
statementType语句类型:STATEMENT、PREPARED、CALLABLE
resultSetType结果集类型
databaseId数据库厂商标识

查询示例

<!-- 查询单个对象 -->
<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>

<!-- 查询列表 -->
<select id="selectAll" resultType="User">
SELECT * FROM user ORDER BY id DESC
</select>

<!-- 查询数量 -->
<select id="count" resultType="long">
SELECT COUNT(*) FROM user
</select>

<!-- 查询 Map -->
<select id="selectMap" resultType="map">
SELECT id, username FROM user WHERE id = #{id}
</select>

<!-- 使用 resultMap -->
<select id="selectWithOrders" resultMap="UserWithOrdersMap">
SELECT u.*, o.id as order_id, o.order_no
FROM user u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.id = #{id}
</select>

<!-- 分页查询 -->
<select id="selectByPage" resultType="User">
SELECT * FROM user
LIMIT #{offset}, #{pageSize}
</select>

参数传递

<!-- 单个参数 -->
<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>

<!-- 多个参数(使用 @Param 注解) -->
<select id="selectByUsernameAndEmail" resultType="User">
SELECT * FROM user
WHERE username = #{username} AND email = #{email}
</select>

<!-- 对象参数 -->
<select id="selectByCondition" resultType="User">
SELECT * FROM user
WHERE username = #{username}
AND status = #{status}
</select>

<!-- Map 参数 -->
<select id="selectByMap" resultType="User">
SELECT * FROM user
WHERE username = #{username}
AND email = #{email}
</select>

insert 插入语句

insert 元素用于定义插入语句。

基本用法

<insert id="insert" parameterType="User">
INSERT INTO user (username, password, email, phone)
VALUES (#{username}, #{password}, #{email}, #{phone})
</insert>

主键回填

使用 useGeneratedKeys 获取数据库自动生成的主键:

<insert id="insert" parameterType="User" 
useGeneratedKeys="true" keyProperty="id">
INSERT INTO user (username, password, email, phone)
VALUES (#{username}, #{password}, #{email}, #{phone})
</insert>

执行插入后,user.getId() 即可获取生成的主键值。

自定义主键生成

对于不支持自动生成主键的数据库,可以使用 selectKey

<insert id="insert" parameterType="User">
<selectKey keyProperty="id" resultType="long" order="BEFORE">
SELECT NEXT VALUE FOR user_seq
</selectKey>
INSERT INTO user (id, username, password)
VALUES (#{id}, #{username}, #{password})
</insert>
属性说明
keyProperty主键属性名
resultType主键类型
order执行时机:BEFORE(插入前)、AFTER(插入后)
statementType语句类型

MySQL 示例

<insert id="insert" parameterType="User" 
useGeneratedKeys="true" keyProperty="id">
INSERT INTO user (username, password, email, phone, status)
VALUES (#{username}, #{password}, #{email}, #{phone}, #{status})
</insert>

Oracle 示例

<insert id="insert" parameterType="User">
<selectKey keyProperty="id" resultType="long" order="BEFORE">
SELECT user_seq.NEXTVAL FROM DUAL
</selectKey>
INSERT INTO user (id, username, password)
VALUES (#{id}, #{username}, #{password})
</insert>

update 更新语句

update 元素用于定义更新语句。

基本用法

<update id="update" parameterType="User">
UPDATE user
SET username = #{username},
email = #{email},
phone = #{phone}
WHERE id = #{id}
</update>

动态更新

只更新非空字段:

<update id="updateSelective" parameterType="User">
UPDATE user
<set>
<if test="username != null">username = #{username},</if>
<if test="email != null">email = #{email},</if>
<if test="phone != null">phone = #{phone},</if>
<if test="status != null">status = #{status},</if>
</set>
WHERE id = #{id}
</update>

delete 删除语句

delete 元素用于定义删除语句。

基本用法

<delete id="deleteById">
DELETE FROM user WHERE id = #{id}
</delete>

批量删除

<delete id="deleteByIds">
DELETE FROM user WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>

sql SQL 片段

sql 元素用于定义可重用的 SQL 片段。

定义和引用

<!-- 定义 SQL 片段 -->
<sql id="Base_Column_List">
id, username, password, email, phone, status, create_time, update_time
</sql>

<sql id="Example_Where_Clause">
<where>
<if test="username != null">AND username = #{username}</if>
<if test="email != null">AND email = #{email}</if>
<if test="status != null">AND status = #{status}</if>
</where>
</sql>

<!-- 引用 SQL 片段 -->
<select id="selectByCondition" resultType="User">
SELECT <include refid="Base_Column_List"/>
FROM user
<include refid="Example_Where_Clause"/>
</select>

带参数的 SQL 片段

<sql id="table">
${prefix}user
</sql>

<select id="selectById" resultType="User">
SELECT * FROM <include refid="table">
<property name="prefix" value="t_"/>
</include>
WHERE id = #{id}
</select>

resultMap 结果映射

resultMap 是 MyBatis 最强大的特性,用于复杂的结果集映射。

基本映射

<resultMap id="BaseResultMap" type="User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="password" property="password"/>
<result column="email" property="email"/>
<result column="phone" property="phone"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
</resultMap>

属性说明

属性说明
id当前命名空间中的唯一标识
type映射的 Java 类型
autoMapping是否启用自动映射
extends继承其他 resultMap

id 和 result 元素

属性说明
propertyJava Bean 属性名
column数据库列名
javaTypeJava 类型
jdbcTypeJDBC 类型
typeHandler类型处理器

id 元素用于标识主键字段,MyBatis 会使用主键来提高性能。

constructor 构造器映射

当类没有无参构造器时,使用构造器映射:

<resultMap id="ConstructorResultMap" type="User">
<constructor>
<idArg column="id" javaType="long"/>
<arg column="username" javaType="string"/>
<arg column="email" javaType="string"/>
</constructor>
</resultMap>

association 关联映射(一对一)

<resultMap id="UserWithOrderMap" type="User">
<id column="id" property="id"/>
<result column="username" property="username"/>

<!-- 一对一关联 -->
<association property="order" javaType="Order">
<id column="order_id" property="id"/>
<result column="order_no" property="orderNo"/>
<result column="amount" property="amount"/>
</association>
</resultMap>

嵌套查询

<resultMap id="UserWithOrderMap" type="User">
<id column="id" property="id"/>
<result column="username" property="username"/>

<!-- 嵌套查询(延迟加载) -->
<association property="order"
column="id"
select="com.example.mybatis.mapper.OrderMapper.selectByUserId"/>
</resultMap>

collection 集合映射(一对多)

<resultMap id="UserWithOrdersMap" type="User">
<id column="id" property="id"/>
<result column="username" property="username"/>

<!-- 一对多关联 -->
<collection property="orders" ofType="Order">
<id column="order_id" property="id"/>
<result column="order_no" property="orderNo"/>
<result column="amount" property="amount"/>
</collection>
</resultMap>

嵌套查询

<resultMap id="UserWithOrdersMap" type="User">
<id column="id" property="id"/>
<result column="username" property="username"/>

<!-- 嵌套查询 -->
<collection property="orders"
column="id"
ofType="Order"
select="com.example.mybatis.mapper.OrderMapper.selectListByUserId"/>
</resultMap>

discriminator 鉴别器

根据结果值选择不同的 resultMap:

<resultMap id="VehicleResultMap" type="Vehicle">
<id column="id" property="id"/>
<result column="name" property="name"/>
<discriminator javaType="int" column="vehicle_type">
<case value="1" resultMap="CarResultMap"/>
<case value="2" resultMap="TruckResultMap"/>
</discriminator>
</resultMap>

<resultMap id="CarResultMap" type="Car" extends="VehicleResultMap">
<result column="door_count" property="doorCount"/>
</resultMap>

<resultMap id="TruckResultMap" type="Truck" extends="VehicleResultMap">
<result column="load_capacity" property="loadCapacity"/>
</resultMap>

参数处理

参数占位符

MyBatis 提供两种参数占位符:

占位符说明
#{}预编译参数,安全,防止 SQL 注入
${}字符串替换,不安全,用于动态表名、列名
<!-- 使用 #{}(推荐) -->
<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>

<!-- 使用 ${}(动态表名、列名) -->
<select id="selectByColumn" resultType="User">
SELECT * FROM user WHERE ${columnName} = #{value}
</select>

<select id="selectFromTable" resultType="User">
SELECT * FROM ${tableName} WHERE id = #{id}
</select>

参数类型

<!-- 基本类型 -->
<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>

<!-- 字符串 -->
<select id="selectByUsername" resultType="User">
SELECT * FROM user WHERE username = #{username}
</select>

<!-- 对象 -->
<insert id="insert" parameterType="User">
INSERT INTO user (username, password) VALUES (#{username}, #{password})
</insert>

<!-- Map -->
<select id="selectByMap" resultType="User">
SELECT * FROM user WHERE username = #{username} AND email = #{email}
</select>

<!-- List -->
<select id="selectByIds" resultType="User">
SELECT * FROM user WHERE id IN
<foreach collection="list" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>

<!-- 数组 -->
<select id="selectByIdArray" resultType="User">
SELECT * FROM user WHERE id IN
<foreach collection="array" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>

内置参数

MyBatis 提供了两个内置参数:

<select id="selectByCondition" resultType="User">
SELECT * FROM user
<where>
<if test="_parameter != null">
AND username = #{username}
</if>
</where>
</select>

<select id="selectByDatabaseId" resultType="User">
<if test="_databaseId == 'mysql'">
SELECT * FROM user LIMIT 10
</if>
<if test="_databaseId == 'oracle'">
SELECT * FROM user WHERE ROWNUM &lt;= 10
</if>
</select>
参数说明
_parameter单个参数时为该参数,多个参数时为 Map
_databaseId当前数据库厂商标识

高级查询方法

游标查询(Cursor)

当处理大量数据时,使用游标可以实现数据的惰性加载,避免一次性将所有数据加载到内存中。

Mapper 接口定义

public interface UserMapper {

/**
* 使用游标查询
* Cursor 实现了 Closeable 接口,使用后需要关闭
*/
Cursor<User> selectAllCursor();

/**
* 带条件的游标查询
*/
Cursor<User> selectCursorByStatus(@Param("status") Integer status);
}

XML 映射

<select id="selectAllCursor" resultType="User">
SELECT * FROM user ORDER BY id
</select>

<select id="selectCursorByStatus" resultType="User">
SELECT * FROM user WHERE status = #{status} ORDER BY id
</select>

使用示例

@Test
void testCursorQuery() {
SqlSession session = SqlSessionUtil.getSqlSession();

try (Cursor<User> cursor = session.selectCursor(
"com.example.mybatis.mapper.UserMapper.selectAllCursor")) {

// 游标实现了 Iterable 接口,可以使用 for-each 遍历
for (User user : cursor) {
// 逐条处理数据
System.out.println(user);

// 处理完成后数据会被释放,不会占用内存
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
session.close();
}
}

// 使用 Mapper 接口
@Test
void testCursorWithMapper() {
SqlSession session = SqlSessionUtil.getSqlSession();
UserMapper userMapper = session.getMapper(UserMapper.class);

try (Cursor<User> cursor = userMapper.selectCursorByStatus(1)) {
// 使用流式处理
cursor.forEach(user -> {
// 处理每条数据
System.out.println(user.getUsername());
});
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
session.close();
}
}

游标与 List 的区别

特性CursorList
数据加载惰性加载,逐条获取一次性加载全部
内存占用低,适合大数据量高,可能 OOM
遍历次数只能遍历一次可多次遍历
使用场景大数据量处理小数据量、需要随机访问

注意:游标必须在事务中使用,并且需要在同一个 SqlSession 中完成遍历。

@Test
void testCursorInTransaction() {
SqlSession session = SqlSessionUtil.getSqlSession();

try {
// 开启事务(游标需要在事务中使用)
session.getConnection().setAutoCommit(false);

try (Cursor<User> cursor = session.selectCursor("selectAllCursor")) {
cursor.forEach(System.out::println);
}

session.commit();
} catch (Exception e) {
session.rollback();
throw new RuntimeException(e);
} finally {
session.close();
}
}

结果处理器(ResultHandler)

ResultHandler 允许自定义每行结果的处理逻辑,适用于需要对结果进行特殊处理的场景。

ResultHandler 接口

package org.apache.ibatis.session;

public interface ResultHandler<T> {
void handleResult(ResultContext<? extends T> context);
}

ResultContext 接口

public interface ResultContext<T> {
T getResultObject(); // 获取当前结果对象
int getResultCount(); // 获取已处理的结果数量
boolean isStopped(); // 是否已停止
void stop(); // 停止加载更多结果
}

自定义 ResultHandler

// 示例1:收集结果到自定义集合
public class UserCollectHandler implements ResultHandler<User> {

private final Set<String> usernames = new HashSet<>();

@Override
public void handleResult(ResultContext<? extends User> context) {
User user = context.getResultObject();
usernames.add(user.getUsername());
}

public Set<String> getUsernames() {
return usernames;
}
}

// 示例2:条件处理
public class ActiveUserHandler implements ResultHandler<User> {

private final List<User> activeUsers = new ArrayList<>();

@Override
public void handleResult(ResultContext<? extends User> context) {
User user = context.getResultObject();

// 只收集活跃用户
if (user.getStatus() == 1) {
activeUsers.add(user);
}

// 达到100条后停止
if (context.getResultCount() >= 100) {
context.stop();
}
}

public List<User> getActiveUsers() {
return activeUsers;
}
}

// 示例3:统计处理
public class UserStatisticsHandler implements ResultHandler<User> {

private int totalCount = 0;
private int activeCount = 0;
private final Map<Integer, Integer> statusDistribution = new HashMap<>();

@Override
public void handleResult(ResultContext<? extends User> context) {
User user = context.getResultObject();
totalCount++;

if (user.getStatus() == 1) {
activeCount++;
}

statusDistribution.merge(user.getStatus(), 1, Integer::sum);
}

public void printStatistics() {
System.out.println("总用户数: " + totalCount);
System.out.println("活跃用户数: " + activeCount);
System.out.println("状态分布: " + statusDistribution);
}
}

使用 ResultHandler

@Test
void testResultHandler() {
SqlSession session = SqlSessionUtil.getSqlSession();

// 创建处理器
UserCollectHandler handler = new UserCollectHandler();

// 执行查询并使用处理器
session.select("com.example.mybatis.mapper.UserMapper.selectAll", handler);

// 获取处理结果
Set<String> usernames = handler.getUsernames();
System.out.println("用户名集合: " + usernames);

session.close();
}

@Test
void testStatisticsHandler() {
SqlSession session = SqlSessionUtil.getSqlSession();

UserStatisticsHandler handler = new UserStatisticsHandler();
session.select("selectAll", handler);
handler.printStatistics();

session.close();
}

使用 Lambda 表达式

@Test
void testResultHandlerLambda() {
SqlSession session = SqlSessionUtil.getSqlSession();

List<String> emails = new ArrayList<>();

// 使用 Lambda 表达式
session.select("selectAll", (ResultHandler<User>) context -> {
User user = context.getResultObject();
if (user.getEmail() != null) {
emails.add(user.getEmail());
}
});

System.out.println("邮箱列表: " + emails);
session.close();
}

RowBounds 分页

MyBatis 提供了 RowBounds 类进行简单的内存分页:

@Test
void testRowBounds() {
SqlSession session = SqlSessionUtil.getSqlSession();
UserMapper mapper = session.getMapper(UserMapper.class);

// 跳过前10条,获取10条(内存分页)
int offset = 10;
int limit = 10;
RowBounds rowBounds = new RowBounds(offset, limit);

List<User> users = session.selectList(
"com.example.mybatis.mapper.UserMapper.selectAll",
null,
rowBounds
);

users.forEach(System.out::println);
session.close();
}

注意RowBounds 是内存分页,会先查询所有数据再截取,大数据量时性能较差。生产环境建议使用数据库物理分页(如 PageHelper)。

selectMap 方法

selectMap 方法可以将结果集转换为 Map,指定某个属性作为 key:

@Test
void testSelectMap() {
SqlSession session = SqlSessionUtil.getSqlSession();

// 以 id 为 key,User 对象为 value
Map<Long, User> userMap = session.selectMap(
"com.example.mybatis.mapper.UserMapper.selectAll",
"id" // 作为 key 的属性名
);

User user = userMap.get(1L);
System.out.println(user);

session.close();
}

// Mapper 接口中使用 @MapKey 注解
public interface UserMapper {

@MapKey("id")
@Select("SELECT * FROM user")
Map<Long, User> selectAllMap();
}

ResultHandler 注意事项

使用 ResultHandler 时需要注意以下限制:

  1. 缓存问题:使用带 ResultHandler 参数的方法时,收到的数据不会被缓存

  2. 结果映射:使用高级结果映射(如关联查询)时,可能收到尚未完整填充的对象

  3. 事务要求ResultHandler 需要在事务中执行

// 正确用法:在事务中使用
@Test
void testResultHandlerWithTransaction() {
SqlSession session = SqlSessionUtil.getSqlSession();

try {
List<User> result = new ArrayList<>();

session.select("selectAll", (ResultHandler<User>) context -> {
result.add(context.getResultObject());
});

session.commit();
} catch (Exception e) {
session.rollback();
} finally {
session.close();
}
}

完整示例

实体类

@Data
public class User {
private Long id;
private String username;
private String password;
private String email;
private String phone;
private Integer status;
private LocalDateTime createTime;
private LocalDateTime updateTime;

// 一对一关联
private Order latestOrder;

// 一对多关联
private List<Order> orders;
}

Mapper 接口

public interface UserMapper {
User selectById(Long id);
List<User> selectAll();
List<User> selectByCondition(UserQuery query);
int insert(User user);
int insertBatch(List<User> users);
int update(User user);
int deleteById(Long id);
int deleteByIds(List<Long> ids);
User selectWithOrders(Long id);
}

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">

<mapper namespace="com.example.mybatis.mapper.UserMapper">

<resultMap id="BaseResultMap" type="User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="password" property="password"/>
<result column="email" property="email"/>
<result column="phone" property="phone"/>
<result column="status" property="status"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
</resultMap>

<resultMap id="UserWithOrdersMap" type="User" extends="BaseResultMap">
<collection property="orders" ofType="Order">
<id column="order_id" property="id"/>
<result column="order_no" property="orderNo"/>
<result column="amount" property="amount"/>
</collection>
</resultMap>

<sql id="Base_Column_List">
id, username, password, email, phone, status, create_time, update_time
</sql>

<select id="selectById" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM user
WHERE id = #{id}
</select>

<select id="selectAll" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM user
ORDER BY id DESC
</select>

<select id="selectByCondition" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM user
<where>
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="email != null and email != ''">
AND email = #{email}
</if>
<if test="status != null">
AND status = #{status}
</if>
</where>
ORDER BY id DESC
</select>

<insert id="insert" parameterType="User"
useGeneratedKeys="true" keyProperty="id">
INSERT INTO user (username, password, email, phone, status)
VALUES (#{username}, #{password}, #{email}, #{phone}, #{status})
</insert>

<insert id="insertBatch" parameterType="list">
INSERT INTO user (username, password, email, phone, status)
VALUES
<foreach collection="list" item="user" separator=",">
(#{user.username}, #{user.password}, #{user.email}, #{user.phone}, #{user.status})
</foreach>
</insert>

<update id="update" parameterType="User">
UPDATE user
<set>
<if test="username != null">username = #{username},</if>
<if test="email != null">email = #{email},</if>
<if test="phone != null">phone = #{phone},</if>
<if test="status != null">status = #{status},</if>
</set>
WHERE id = #{id}
</update>

<delete id="deleteById">
DELETE FROM user WHERE id = #{id}
</delete>

<delete id="deleteByIds">
DELETE FROM user WHERE id IN
<foreach collection="list" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>

<select id="selectWithOrders" resultMap="UserWithOrdersMap">
SELECT u.*, o.id as order_id, o.order_no, o.amount
FROM user u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.id = #{id}
</select>

</mapper>

小结

本章详细介绍了 XML 映射器的各种元素:

  • select/insert/update/delete:CRUD 操作语句
  • sql:可重用的 SQL 片段
  • resultMap:复杂结果映射
  • 参数处理:# 和 $ 的区别

下一章将介绍注解开发方式。