在使用MyBatis框架开发数据访问层时,SQL语句的编写位置一直是一个绕不开的话题。相较于把SQL直接写在接口方法上的注解方式,XML Mapper提供了一种将SQL语句与Java代码彻底分离的方案,它把SQL统一放在扩展名为xml的映射文件里,通过命名空间与Mapper接口绑定,实现接口方法到SQL语句的自动映射。这种方式不仅让SQL的维护更加集中,也为复杂动态SQL的编写提供了强大的标签支持,是企业级项目中最主流的选择。

XML Mapper是什么,它和注解方式有何区别
XML Mapper本质上是一个遵循MyBatis DTD约束的XML文件,文件内部定义了SQL语句、参数映射规则和结果映射规则。每一个XML映射文件通过namespace属性与一个Java接口建立唯一对应关系,接口中定义方法,XML中定义同id的SQL语句,程序运行时MyBatis会为接口生成动态代理对象,调用方法时自动执行对应的SQL并完成结果封装。
注解方式则是把SQL直接写在方法上,例如使用@Select、@Update等注解。注解方式适合简单场景,代码量少、直观。但一旦SQL变长、需要拼接动态条件,注解里就要借助<script>标签或者拼接字符串,可读性会迅速下降。而XML方式拥有<if>、<where>、<foreach>等原生标签,动态SQL的编写体验远好于注解。此外,SQL集中在XML文件中,数据库调优时只需查找映射文件即可,不必在成百上千个Java文件里搜索字符串。
两者的核心区别可以概括为:注解适合短小固定的SQL,XML适合复杂多变、需要长期维护的业务SQL。实际项目中常见的做法是简单SQL用注解快速搞定,核心业务SQL统一放入XML,两者并不冲突,可以共存于同一个Mapper接口。
如何创建并配置一个XML Mapper映射文件
以一个典型的Spring Boot加MyBatis项目为例,假设项目部署在Windows服务器上,映射文件通常放在src\main\resources\mapper目录下,文件命名习惯为实体名加Mapper.xml,例如UserMapper.xml。首先需要在配置文件中指定扫描路径,在application.yml中配置mybatis.mapper-locations: classpath:mapper/*.xml,同时在启动类或配置类上加@MapperScan注解扫描接口所在的包。
映射文件的根元素是<mapper>,其namespace属性必须填写对应的Mapper接口全限定名。下面是一个完整的示例,展示了增删改查四类语句的基本写法:
<?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.mapper.UserMapper">
<!-- 根据id查询用户,resultType为实体类全限定名或别名 -->
<select id="selectById" resultType="com.example.entity.User">
SELECT id, username, email, age
FROM t_user
WHERE id = #{id}
</select>
<!-- 新增用户,useGeneratedKeys回填自增主键 -->
<insert id="insertUser" parameterType="com.example.entity.User"
useGeneratedKeys="true" keyProperty="id">
INSERT INTO t_user (username, email, age)
VALUES (#{username}, #{email}, #{age})
</insert>
<!-- 更新用户 -->
<update id="updateUser" parameterType="com.example.entity.User">
UPDATE t_user
SET username = #{username}, email = #{email}, age = #{age}
WHERE id = #{id}
</update>
<!-- 删除用户 -->
<delete id="deleteById" parameterType="long">
DELETE FROM t_user WHERE id = #{id}
</delete>
</mapper>对应的Java接口只需要声明方法,不需要任何实现类:
package com.example.mapper;
import com.example.entity.User;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface UserMapper {
User selectById(Long id);
int insertUser(User user);
int updateUser(User user);
int deleteById(Long id);
}需要注意几个细节。第一,SQL语句的id必须与接口方法名完全一致,包括大小写。第二,#{}是预编译占位符,会被转换为JDBC的PreparedStatement参数,能有效防止SQL注入;而${}是字符串直接拼接,只应用于动态表名、排序字段这类无法预编译的场景,且必须校验来源。第三,如果方法有多个参数,建议使用@Param注解为每个参数命名,否则只能通过arg0、param1这类系统默认名称引用。
动态SQL与高级结果映射的实战用法
动态SQL是XML Mapper最强大的能力。多条件查询时,用户可能只填部分筛选项,使用<where>配合<if>可以自动处理where关键字和多余的and前缀:
<select id="selectByCondition" resultType="com.example.entity.User">
SELECT id, username, email, age
FROM t_user
<where>
<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>
</select>批量操作则依赖<foreach>标签,它可以遍历集合生成in条件或者批量插入的values片段。下面的例子演示批量插入一千条数据时的高效写法,比循环调用单条insert快一个数量级:
<insert id="batchInsert">
INSERT INTO t_user (username, email, age) VALUES
<foreach collection="list" item="u" separator=",">
(#{u.username}, #{u.email}, #{u.age})
</foreach>
</insert>当数据库字段名与实体属性名不一致,或者存在一对一、一对多关联关系时,就需要用到<resultMap>。它先定义列到属性的映射规则,再通过<association>和<collection>处理关联对象。例如订单与用户是一对一、订单与订单明细是一对多:
<resultMap id="orderResultMap" type="com.example.entity.Order">
<id property="id" column="order_id"/>
<result property="orderNo" column="order_no"/>
<association property="user" javaType="com.example.entity.User">
<id property="id" column="user_id"/>
<result property="username" column="username"/>
</association>
<collection property="items" ofType="com.example.entity.OrderItem">
<id property="id" column="item_id"/>
<result property="productName" column="product_name"/>
</collection>
</resultMap>此外还有一些提升可维护性的技巧:用<sql>标签抽取重复的字段列表,用<include>在多处引用;写update语句时配合<set>标签自动剔除末尾逗号,实现只更新非空字段。排查问题时可在配置中开启SQL日志输出,观察实际执行的语句与参数是否正确。掌握这些用法后,XML Mapper足以应对绝大多数持久层开发需求,既保证了SQL的灵活性,又维持了代码结构的清晰整洁。
XML MapperMyBatisSQL映射修改时间:2026-08-31 19:21:05