作为Java生态中最主流的ORM框架之一,MyBatis在实际企业开发中扮演着重要角色。当基础CRUD操作无法满足复杂业务需求时,动态SQL和关联查询就成为开发者必须掌握的进阶技能。这两个特性分别解决了SQL语句动态生成和对象关系映射这两大核心痛点。
我在电商系统开发中曾遇到这样的场景:商品列表页需要根据多达12个筛选条件动态生成查询语句,同时每个商品需要关联查询所属店铺和SKU列表。如果没有动态SQL,就需要写几十个几乎重复的Mapper方法;而缺乏关联查询支持,则会导致N+1查询问题严重拖慢系统响应。正是这些实际痛点让我深刻认识到这两个特性的价值。
MyBatis的动态SQL本质上是基于OGNL表达式和XML标签实现的模板引擎。其核心原理可以概括为:在XML映射文件解析阶段,MyBatis会构建一个包含完整解析环境的上下文,动态标签根据传入参数的条件值决定是否包含其中的SQL片段。
以最常用的<if>标签为例:
xml复制<select id="findActiveBlogWithTitleLike" resultType="Blog">
SELECT * FROM BLOG
WHERE state = 'ACTIVE'
<if test="title != null">
AND title like #{title}
</if>
</select>
当调用该方法时,MyBatis会:
test属性中的OGNL表达式<choose>/<when>/<otherwise>组合实现了类似Java中的switch-case逻辑:
xml复制<select id="findActiveBlogLike" resultType="Blog">
SELECT * FROM BLOG WHERE state = 'ACTIVE'
<choose>
<when test="title != null">
AND title like #{title}
</when>
<when test="author != null and author.name != null">
AND author_name like #{author.name}
</when>
<otherwise>
AND featured = 1
</otherwise>
</choose>
</select>
<foreach>在处理IN查询时尤为实用:
xml复制<select id="selectPostIn" resultType="domain.blog.Post">
SELECT * FROM POST P
WHERE ID in
<foreach item="item" index="index" collection="list"
open="(" separator="," close=")">
#{item}
</foreach>
</select>
注意:collection属性支持List、Array和Map三种集合类型,使用时需要确保参数类型匹配
<set>标签智能处理UPDATE语句中的逗号问题:
xml复制<update id="updateAuthorIfNecessary">
update Author
<set>
<if test="username != null">username=#{username},</if>
<if test="password != null">password=#{password},</if>
<if test="email != null">email=#{email}</if>
</set>
where id=#{id}
</update>
即使最后一个条件不满足,MyBatis也会自动去除多余的逗号。
避免过度动态化:动态SQL虽然灵活,但过度使用会导致SQL难以维护。建议将稳定不变的查询条件放在动态标签外部。
参数预处理:对于复杂的动态查询,建议在Service层预先处理参数,减少Mapper层的判断逻辑。
索引友好性:确保生成的动态SQL能利用到数据库索引。可以通过MyBatis的日志输出检查最终执行的SQL。
批量操作优化:使用<foreach>进行批量插入时,建议设置batch执行器:
java复制SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
BlogMapper mapper = session.getMapper(BlogMapper.class);
for (Blog blog : blogs) {
mapper.insertBlog(blog);
}
session.commit();
} finally {
session.close();
}
假设我们有一个订单(Order)和用户(User)的一对一关系:
java复制public class Order {
private Integer id;
private String orderNo;
private User user; // 关联用户对象
// getters & setters
}
xml复制<resultMap id="orderWithUserResultMap" type="Order">
<id property="id" column="id"/>
<result property="orderNo" column="order_no"/>
<association property="user" javaType="User">
<id property="id" column="user_id"/>
<result property="username" column="username"/>
<result property="email" column="email"/>
</association>
</resultMap>
<select id="selectOrderWithUser" resultMap="orderWithUserResultMap">
SELECT
o.id, o.order_no,
u.id as user_id, u.username, u.email
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE o.id = #{id}
</select>
xml复制<resultMap id="orderResultMap" type="Order">
<id property="id" column="id"/>
<result property="orderNo" column="order_no"/>
<association property="user" column="user_id"
javaType="User" select="selectUser"/>
</resultMap>
<select id="selectOrder" resultMap="orderResultMap">
SELECT id, order_no, user_id FROM orders WHERE id = #{id}
</select>
<select id="selectUser" resultType="User">
SELECT id, username, email FROM users WHERE id = #{id}
</select>
关键区别:嵌套结果通过单次JOIN查询实现,而嵌套查询需要额外SQL查询。在大数据量场景下,JOIN方式通常性能更好。
考虑博客(Blog)和文章(Post)的一对多关系:
java复制public class Blog {
private Integer id;
private String title;
private List<Post> posts; // 关联文章列表
// getters & setters
}
xml复制<resultMap id="blogWithPostsResultMap" type="Blog">
<id property="id" column="id"/>
<result property="title" column="title"/>
<collection property="posts" ofType="Post">
<id property="id" column="post_id"/>
<result property="subject" column="subject"/>
<result property="body" column="body"/>
</collection>
</resultMap>
<select id="selectBlogWithPosts" resultMap="blogWithPostsResultMap">
SELECT
b.id, b.title,
p.id as post_id, p.subject, p.body
FROM blogs b
LEFT JOIN posts p ON b.id = p.blog_id
WHERE b.id = #{id}
</select>
对于大型数据集,可以采用分步查询避免JOIN带来的性能问题:
xml复制<resultMap id="blogResultMap" type="Blog">
<id property="id" column="id"/>
<result property="title" column="title"/>
<collection property="posts" column="id"
ofType="Post" select="selectPostsForBlog"/>
</resultMap>
<select id="selectBlog" resultMap="blogResultMap">
SELECT id, title FROM blogs WHERE id = #{id}
</select>
<select id="selectPostsForBlog" resultType="Post">
SELECT id, subject, body FROM posts WHERE blog_id = #{id}
</select>
N+1查询问题:在嵌套查询方式中,如果主查询返回N条记录,关联查询会执行N次。解决方案:
@BatchSize注解批量加载列名冲突:在多表JOIN时,不同表的同名列会导致映射错误。必须使用as明确指定列别名:
sql复制SELECT
u.id as user_id,
u.name as user_name,
d.id as dept_id,
d.name as dept_name
FROM ...
@JsonIgnore注解断开循环在实际项目中,我们经常需要同时使用这两种技术。例如实现一个动态的商品搜索功能:
xml复制<select id="searchProducts" resultMap="productWithCategoryAndSkus">
SELECT
p.id, p.name, p.price,
c.id as category_id, c.name as category_name
FROM products p
LEFT JOIN categories c ON p.category_id = c.id
<where>
<if test="name != null">
p.name like CONCAT('%', #{name}, '%')
</if>
<if test="minPrice != null">
AND p.price >= #{minPrice}
</if>
<if test="categoryId != null">
AND p.category_id = #{categoryId}
</if>
</where>
ORDER BY
<choose>
<when test="sortBy == 'price'">p.price</when>
<when test="sortBy == 'sales'">p.sales_volume</when>
<otherwise>p.create_time</otherwise>
</choose>
</select>
<resultMap id="productWithCategoryAndSkus" type="Product">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="price" column="price"/>
<association property="category" javaType="Category">
<id property="id" column="category_id"/>
<result property="name" column="category_name"/>
</association>
<collection property="skus" column="id"
ofType="Sku" select="selectSkusByProductId"/>
</resultMap>
通过<resultMap>的extends属性可以实现映射配置的复用:
xml复制<resultMap id="baseOrderResultMap" type="Order">
<id property="id" column="id"/>
<result property="orderNo" column="order_no"/>
<result property="createTime" column="create_time"/>
</resultMap>
<resultMap id="orderWithUserResultMap" extends="baseOrderResultMap" type="Order">
<association property="user" javaType="User">
<id property="id" column="user_id"/>
<result property="username" column="username"/>
</association>
</resultMap>
对于Java枚举类型,MyBatis提供了两种处理方式:
自定义处理方式示例:
java复制public class StatusEnumTypeHandler extends BaseTypeHandler<StatusEnum> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i,
StatusEnum parameter, JdbcType jdbcType) {
ps.setString(i, parameter.getCode());
}
// 其他方法实现...
}
在配置中注册:
xml复制<typeHandlers>
<typeHandler handler="com.example.StatusEnumTypeHandler"
javaType="com.example.StatusEnum"/>
</typeHandlers>
对于多层嵌套的对象结构,可以使用嵌套的association和collection:
xml复制<resultMap id="detailedOrderResultMap" type="Order">
<id property="id" column="order_id"/>
<association property="user" javaType="User">
<id property="id" column="user_id"/>
<association property="department" javaType="Department">
<id property="id" column="dept_id"/>
</association>
</association>
<collection property="items" ofType="OrderItem">
<id property="id" column="item_id"/>
<association property="product" javaType="Product">
<id property="id" column="product_id"/>
</association>
</collection>
</resultMap>
MyBatis的二级缓存可以跨Session共享,但使用不当会导致严重问题:
xml复制<mapper namespace="com.example.BlogMapper">
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
</mapper>
缓存问题排查清单:
<cache-ref>在mybatis-config.xml中全局配置:
xml复制<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
对于特定查询可以覆盖全局设置:
xml复制<resultMap id="blogResultMap" type="Blog">
<collection property="posts" column="id"
ofType="Post" select="selectPostsForBlog"
fetchType="lazy"/>
</resultMap>
java复制PageHelper.startPage(1, 10);
List<Blog> blogs = blogMapper.selectAll();
PageInfo<Blog> pageInfo = new PageInfo<>(blogs);
sql复制SELECT * FROM table
WHERE id > #{lastId}
ORDER BY id ASC
LIMIT #{pageSize}
COUNT(*)的优化方案:sql复制-- 快速估算行数(MySQL)
EXPLAIN SELECT * FROM table WHERE condition;
properties复制# log4j配置
log4j.logger.org.mybatis=DEBUG
使用MyBatis-Plus的PerformanceInterceptor分析SQL性能
关键监控指标:
我在实际项目中最常遇到的性能问题往往源于不合理的关联查询设计。一个经验法则是:对于高频访问的查询,宁可多写几个专门的DTO和Mapper方法,也不要为了代码复用而设计过于复杂的通用查询。曾经因为一个"万能查询"方法导致系统在流量高峰时数据库连接耗尽,这个教训让我深刻认识到合理设计查询的重要性。