SELECT id, username, email
FROM users
WHERE id = #{id}
SELECT u.id, u.username, u.email, p.phone_number
FROM users u
LEFT JOIN user_profile p ON u.id = p.user_id
WHERE u.id = #{userId}
<result property="username" column="username"/>
<association property="profile" javaType="UserProfile">
<result property="phone" column="phone"/>
<result property="address" column="address"/>
</association>
4.1 查询语句执行计划分析
理解SQL执行计划就像拿到数据库的"体检报告",能准确诊断查询的性能问题。每个数据库都提供了解析执行计划的工具,MySQL的EXPLAIN、Oracle的EXPLAIN PLAN都是常用选择。
执行计划中的关键指标值得关注。type字段显示查询类型,从最优的const到最差的全表扫描ALL,中间还有range、ref等不同级别。我曾经排查一个慢查询,发现type是ALL,意味着全表扫描。通过添加合适的索引,性能提升了数十倍。
rows字段预估需要扫描的行数,filtered显示过滤比例。这两个数值结合,能判断查询效率。Extra字段的"Using filesort"或"Using temporary"往往是性能警讯,表明需要额外的排序或临时表操作。
实际分析时,我习惯先关注最耗时的操作节点。数据库通常按照执行成本从高到低排列,找到瓶颈点就能针对性优化。有个项目中的统计查询原本需要15秒,通过执行计划发现是在大表上做了全表扫描,优化后降到了1秒内。
4.2 索引优化与查询效率提升
索引是查询性能的加速器,但并非越多越好。合理的索引设计需要平衡查询需求和写入性能。我倾向于根据实际查询模式来设计索引,而不是盲目添加。
最左前缀原则是复合索引设计的核心。索引(a,b,c)可以支持a、a,b、a,b,c的查询,但无法支持b,c或c的单独查询。曾经重构过一个用户查询系统,原本有多个单列索引,合并为合适的复合索引后,索引大小减少了40%,查询速度反而提升了。
覆盖索引能避免回表操作,当索引包含所有查询字段时,性能提升明显。但要注意索引维护的成本,频繁更新的字段不适合创建过多索引。
索引失效的常见场景需要警惕。在索引列上使用函数、进行类型转换、或者使用!=操作符,都可能导致索引失效。LIKE查询以通配符开头时,同样无法使用索引。我遇到过开发者在日期字段上使用DATE_FORMAT函数,导致原本高效的索引变得无效。
定期分析慢查询日志是个好习惯。数据库的慢查询日志记录了执行时间超过阈值的SQL,这是发现性能问题的宝贵资源。设置合理的long_query_time,定期检查并优化这些慢查询。
4.3 缓存机制与查询性能平衡
缓存是提升查询性能的利器,但需要谨慎使用。MyBatis提供了一级缓存和二级缓存机制,各有适用场景。
一级缓存默认开启,作用于SqlSession级别。在同一个会话中,相同的查询会直接返回缓存结果。这在事务性操作中很有用,但也要注意缓存失效的时机。更新操作会清空相关缓存,确保数据一致性。
二级缓存需要显式配置,作用于Mapper级别。它能跨会话共享数据,适合读多写少的场景。配置二级缓存时,序列化机制和缓存策略都需要仔细考虑。我通常只在配置相对稳定的数据上使用二级缓存,比如地区编码、系统参数等。
缓存带来的性能提升很诱人,但缓存一致性问题不容忽视。在分布式环境中,本地缓存可能导致数据不同步。这时候可能需要引入Redis等分布式缓存方案。
缓存的失效策略需要根据业务特点设计。时间过期、LRU淘汰、手动清除都是可选方案。过高缓存命中率不一定是最佳状态,可能意味着缓存数据过于静态,无法反映真实业务变化。
实际项目中,我采用分层缓存策略。热点数据使用本地缓存,普通数据使用分布式缓存,冷数据直接查询数据库。这种组合在保证性能的同时,也控制了缓存复杂度。记得一个电商项目通过合理的缓存设计,QPS从几百提升到了上万。
5.1 复杂业务场景查询设计
企业级应用中的查询往往不是简单的单表操作,而是涉及多维度、多条件的复杂组合。面对这样的需求,我倾向于采用模块化的设计思路,将复杂查询拆解为多个可复用的单元。
动态SQL在这里展现出强大威力。MyBatis的
但动态SQL也不是越复杂越好。曾经见过一个包含二十多个
分页查询在企业级应用中几乎无处不在。除了简单的limit分页,还需要考虑深分页的性能问题。当偏移量很大时,传统的limit offset方式性能会急剧下降。采用基于游标的分页,或者使用覆盖索引优化,都是有效的解决方案。
查询结果的处理同样重要。对于关联查询,我更喜欢使用resultMap进行显式映射,而不是依赖自动映射。虽然配置稍显繁琐,但能确保字段映射的准确性,避免因数据库表结构变更导致的意外问题。
5.2 查询安全性与SQL注入防护
SQL注入是Web应用最常见的安全漏洞之一,而MyBatis的正确使用能有效防范这类风险。核心原则很简单:永远不要拼接用户输入到SQL语句中。
{}与${}的区别每个MyBatis使用者都必须清楚。#{}使用预编译语句,参数会被安全地处理,从根本上杜绝SQL注入。${}直接进行字符串替换,存在严重的安全隐患。我见过有团队因为图方便大量使用${},结果在安全扫描中暴露出多个高危漏洞。
但${}并非完全不能用。在一些必须使用动态表名、列名的场景,比如多租户系统中的分表查询,${}是必要的。这时候就需要严格的输入校验和白名单机制,确保用户输入在可控范围内。
权限控制也是查询安全的重要环节。不同角色的用户应该只能访问其权限范围内的数据。在Mapper层进行权限过滤是个不错的选择,通过统一的拦截器或者基类方法,自动添加权限相关的查询条件。
敏感数据的处理需要特别小心。用户密码、身份证号、手机号等敏感信息,即使被合法查询,也要考虑在日志、监控系统中进行脱敏。曾经有个项目因为将完整SQL记录到日志中,导致用户敏感信息泄露。
定期进行安全审计很有必要。使用自动化安全扫描工具检查代码中的SQL注入风险,同时进行人工代码审查,确保所有查询都遵循安全最佳实践。
5.3 代码规范与可维护性考量
良好的代码规范能让团队协作更顺畅,也大大提升系统的可维护性。在MyBatis使用方面,我们团队积累了一些行之有效的规范。
Mapper接口的命名要体现业务语义。比如UserMapper.findActiveUsersByDepartment就比UserMapper.selectList清晰得多。方法名应该让人一眼就能理解查询的意图和返回结果。
XML配置文件的组织也值得讲究。我习惯按业务模块划分Mapper文件,而不是简单地按数据库表划分。同一个业务概念相关的查询放在一起,维护时更容易找到相关代码。
查询语句的格式化很重要。虽然XML本身不关心格式,但良好的缩进和换行能显著提升可读性。复杂的动态SQL更应该合理分段,添加必要的注释说明业务逻辑。
参数对象的设计往往被忽视。与其在方法中传递一堆分散的参数,不如封装成专门的Query对象。这样不仅使接口更清晰,也便于后续扩展。比如分页查询可以抽象出PageQuery基类,包含页码、页大小等通用参数。
日志记录要适度且安全。调试时可以开启MyBatis的SQL日志,但生产环境要谨慎。记录完整SQL可能暴露敏感信息,通常只记录SQL模板和执行时间就足够了。
代码审查是保证质量的最后一道防线。我们团队要求每个Mapper的变更都要经过至少两人的审查,重点关注SQL性能、安全性和可读性。这个过程虽然花费时间,但避免了很多潜在问题。
<if test="keyword != null and keyword != ''">
AND (name LIKE CONCAT('%', #{keyword}, '%')
OR description LIKE CONCAT('%', #{keyword}, '%'))
</if>
<if test="categoryId != null">
AND category_id = #{categoryId}
</if>
<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 collection="brandIds" item="brandId" open="(" separator="," close=")">
#{brandId}
</foreach>
</if>
AND status = 'ON_SALE'
<when test="sortBy == 'PRICE_ASC'">ORDER BY price ASC</when>
<when test="sortBy == 'PRICE_DESC'">ORDER BY price DESC</when>
<when test="sortBy == 'SALES'">ORDER BY sales_count DESC</when>
<otherwise>ORDER BY create_time DESC</otherwise>
LIMIT #{offset}, #{pageSize}