在实际业务开发中,我们经常遇到需要根据不同条件拼接 SQL 语句的情况。比如电商平台的商品筛选功能,用户可能选择按价格、品牌、分类等多个维度进行组合查询。传统 JDBC 开发需要手动拼接大量 if-else 语句,既难以维护又容易出错。
MyBatis 的动态 SQL 特性完美解决了这个问题。通过 XML 配置文件中提供的条件标签,我们可以优雅地构建灵活多变的 SQL 语句。这种声明式的写法不仅提高了开发效率,还大大增强了代码的可读性。
xml复制<select id="searchProducts" resultType="Product">
SELECT * FROM products
WHERE status = 'ACTIVE'
<if test="minPrice != null">
AND price >= #{minPrice}
</if>
<if test="maxPrice != null">
AND price <= #{maxPrice}
</if>
<if test="brandIds != null and brandIds.size() > 0">
AND brand_id IN
<foreach item="id" collection="brandIds" open="(" separator="," close=")">
#{id}
</foreach>
</if>
</select>
注意:test 表达式使用的是 OGNL 语法,可以调用参数的属性和方法。对于集合判断,建议使用 size() 方法而非直接判断 null,避免空集合导致的逻辑错误。
当需要实现类似 Java 中 switch-case 的逻辑时,
xml复制<select id="getUserList" resultType="User">
SELECT * FROM users
WHERE
<choose>
<when test="role == 'admin'">
role_level >= 9
</when>
<when test="role == 'vip'">
role_level BETWEEN 5 AND 8
</when>
<otherwise>
role_level < 5
</otherwise>
</choose>
ORDER BY create_time DESC
</select>
xml复制<select id="findActiveBlogs" resultType="Blog">
SELECT * FROM blog
<where>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null">
AND author = #{author}
</if>
</where>
</select>
即使所有条件都不满足,生成的 SQL 也不会出现语法错误。这是 MyBatis 动态 SQL 最实用的特性之一。
在 UPDATE 语句中,
xml复制<update id="updateUser">
UPDATE users
<set>
<if test="username != null">username=#{username},</if>
<if test="email != null">email=#{email},</if>
<if test="password != null">password=#{password},</if>
</set>
WHERE id=#{id}
</update>
避免过度动态化:虽然动态 SQL 很强大,但过度使用会导致 SQL 解析开销增加。对于固定条件,建议直接写在静态 SQL 中。
合理使用
xml复制<insert id="insertSelective">
INSERT INTO users
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">id,</if>
<if test="username != null">username,</if>
<if test="email != null">email,</if>
</trim>
<trim prefix="VALUES (" suffix=")" suffixOverrides=",">
<if test="id != null">#{id},</if>
<if test="username != null">#{username},</if>
<if test="email != null">#{email},</if>
</trim>
</insert>
xml复制<insert id="batchInsert">
INSERT INTO products (name, price) VALUES
<foreach item="item" collection="list" separator=",">
(#{item.name}, #{item.price})
</foreach>
</insert>
实际开发中建议每批 500-1000 条记录,避免单次 SQL 过长导致数据库性能问题。
关联查询是 ORM 框架的核心功能之一,MyBatis 提供了两种实现方式:
xml复制<resultMap id="blogWithAuthorMap" type="Blog">
<id property="id" column="id"/>
<result property="title" column="title"/>
<association property="author" column="author_id" javaType="Author"
select="selectAuthorById"/>
</resultMap>
<select id="selectBlogWithAuthor" resultMap="blogWithAuthorMap">
SELECT * FROM blog WHERE id = #{id}
</select>
<select id="selectAuthorById" resultType="Author">
SELECT * FROM author WHERE id = #{id}
</select>
优缺点分析:
xml复制<resultMap id="blogWithAuthorResultMap" type="Blog">
<id property="id" column="blog_id"/>
<result property="title" column="blog_title"/>
<association property="author" javaType="Author">
<id property="id" column="author_id"/>
<result property="name" column="author_name"/>
<result property="email" column="author_email"/>
</association>
</resultMap>
<select id="selectBlogWithAuthorJoin" resultMap="blogWithAuthorResultMap">
SELECT
b.id as blog_id,
b.title as blog_title,
a.id as author_id,
a.name as author_name,
a.email as author_email
FROM blog b LEFT JOIN author a ON b.author_id = a.id
WHERE b.id = #{id}
</select>
性能优化建议:
xml复制<resultMap id="authorWithPostsMap" type="Author">
<id property="id" column="id"/>
<result property="name" column="name"/>
<collection property="posts" column="id" ofType="Post"
select="selectPostsByAuthorId"/>
</resultMap>
<select id="selectAuthorWithPosts" resultMap="authorWithPostsMap">
SELECT * FROM author WHERE id = #{id}
</select>
<select id="selectPostsByAuthorId" resultType="Post">
SELECT * FROM post WHERE author_id = #{id}
</select>
xml复制<resultMap id="authorWithPostsResultMap" type="Author">
<id property="id" column="author_id"/>
<result property="name" column="author_name"/>
<collection property="posts" ofType="Post">
<id property="id" column="post_id"/>
<result property="title" column="post_title"/>
<result property="content" column="post_content"/>
</collection>
</resultMap>
<select id="selectAuthorWithPostsJoin" resultMap="authorWithPostsResultMap">
SELECT
a.id as author_id,
a.name as author_name,
p.id as post_id,
p.title as post_title,
p.content as post_content
FROM author a LEFT JOIN post p ON a.id = p.author_id
WHERE a.id = #{id}
</select>
当查询结果需要根据某个字段值映射到不同子类时,可以使用鉴别器:
xml复制<resultMap id="vehicleResultMap" type="Vehicle">
<id property="id" column="id"/>
<result property="brand" column="brand"/>
<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 property="doorCount" column="door_count"/>
</resultMap>
<resultMap id="truckResultMap" type="Truck" extends="vehicleResultMap">
<result property="boxSize" column="box_size"/>
</resultMap>
在 mybatis-config.xml 中配置延迟加载:
xml复制<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
也可以在特定关联上单独配置:
xml复制<association property="author" column="author_id" javaType="Author"
select="selectAuthorById" fetchType="lazy"/>
条件判断中的空字符串问题:
xml复制<!-- 错误示例 -->
<if test="name != ''">
AND name = #{name}
</if>
<!-- 正确做法 -->
<if test="name != null and name != ''">
AND name = #{name}
</if>
集合判断的完整写法:
xml复制<!-- 更安全的集合判断 -->
<if test="ids != null and !ids.isEmpty()">
AND id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</if>
N+1 查询问题的解决方案:
java复制// 在 Service 层实现批量查询
List<Blog> blogs = blogMapper.selectBlogsByIds(ids);
List<Integer> authorIds = blogs.stream().map(Blog::getAuthorId).distinct().collect(Collectors.toList());
Map<Integer, Author> authorMap = authorMapper.selectAuthorsByIds(authorIds)
.stream().collect(Collectors.toMap(Author::getId, Function.identity()));
blogs.forEach(blog -> blog.setAuthor(authorMap.get(blog.getAuthorId())));
联表查询的索引优化:
多层嵌套集合的处理:
xml复制<resultMap id="departmentWithEmployeesMap" type="Department">
<id property="id" column="dept_id"/>
<result property="name" column="dept_name"/>
<collection property="employees" ofType="Employee">
<id property="id" column="emp_id"/>
<result property="name" column="emp_name"/>
<collection property="skills" ofType="Skill">
<id property="id" column="skill_id"/>
<result property="name" column="skill_name"/>
</collection>
</collection>
</resultMap>
自定义类型处理器应用:
对于数据库中的 JSON 字段或特殊格式数据,可以创建自定义 TypeHandler:
java复制@MappedTypes(List.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class StringListTypeHandler extends BaseTypeHandler<List<String>> {
// 实现类型转换逻辑
}
保持 XML 的可读性:
安全注意事项:
领域模型设计建议:
微服务环境下的调整:
SQL 监控指标:
常用优化手段:
在实际项目中,我发现动态 SQL 的复杂度往往随着业务发展而增长。一个实用的建议是:当动态 SQL 变得难以维护时,可以考虑使用 MyBatis 提供的 Provider 注解方式,在 Java 代码中动态构建 SQL,这样可以利用 Java 语言的强大表达能力,同时获得更好的类型安全支持。