第32篇:MyBatis进阶:动态SQL+分页+缓存,优化数据库操作
在Java后端开发中MyBatis作为一款优秀的持久层框架凭借其灵活的SQL定制能力和高效的数据库交互性能成为了开发人员的首选工具。但在实际项目开发中单纯的基础CRUD操作远不能满足业务需求我们需要借助动态SQL、分页查询、缓存机制等进阶特性进一步优化数据库操作提升系统性能和开发效率。本文将从核心进阶特性入手结合实战案例详细解析MyBatis优化技巧同时补充注解开发和MyBatis-Plus入门知识帮助大家全面掌握MyBatis进阶用法。一、动态SQL灵活拼接SQL适配复杂业务场景动态SQL是MyBatis的核心亮点之一它允许我们根据不同的业务条件动态拼接SQL语句避免了编写大量重复、冗余的SQL代码。MyBatis提供了多种动态SQL标签常用的有if、where、foreach、choose等下面结合实战案例逐一讲解。1. if标签单条件判断按需拼接SQLif标签是最基础的动态SQL标签用于根据条件判断是否拼接某段SQL语句。常用于多条件查询、动态更新等场景核心属性是test用于指定判断条件使用OGNL表达式。实战案例根据用户姓名和年龄查询用户信息姓名和年龄为可选条件若参数为空则不参与查询。!-- UserMapper.xml -- mapper namespacecom.example.mapper.UserMapper select idselectUserByCondition resultTypecom.example.entity.User SELECT id, username, age, email FROM t_user WHERE 11 !-- 判断姓名是否为空不为空则拼接姓名查询条件 -- if testusername ! null and username ! AND username LIKE CONCAT(%, #{username}, %) /if !-- 判断年龄是否为空不为空则拼接年龄查询条件 -- if testage ! null AND age #{age} /if /select /mapper注意使用if标签时通常会在WHERE后添加“11”作为基础条件避免因所有if条件都不满足导致拼接出“WHERE”后无内容的语法错误。2. where标签自动处理多余的AND/OR简化条件拼接where标签可以替代手动添加的“11”它会自动判断内部拼接的SQL片段若有内容则自动添加WHERE关键字同时去除开头多余的AND或OR避免SQL语法错误。改造上述案例使用where标签优化select idselectUserByCondition resultTypecom.example.entity.User SELECT id, username, age, email FROM t_user where if testusername ! null and username ! AND username LIKE CONCAT(%, #{username}, %) /if if testage ! null AND age #{age} /if /where /select此时若两个条件都为空SQL语句会自动变为“SELECT id, username, age, email FROM t_user”无多余的WHERE关键字若有一个条件满足会自动去除条件前的AND拼接正确的SQL。3. foreach标签遍历集合实现批量操作foreach标签用于遍历集合List、Set、Map等常用于批量查询、批量插入、批量删除等场景。核心属性包括collection指定要遍历的集合、item遍历的元素别名、index索引别名可选、open拼接的开头字符、close拼接的结尾字符、separator元素之间的分隔符。实战案例1批量查询用户根据用户ID集合select idselectUserByIds resultTypecom.example.entity.User SELECT id, username, age, email FROM t_user where id IN foreach collectionids itemid open( close) separator, #{id} /foreach /where /select对应的Mapper接口方法ListUser selectUserByIds(Param(ids) ListLong ids);实战案例2批量插入用户insert idbatchInsertUser INSERT INTO t_user (username, age, email) VALUES foreach collectionusers itemuser separator, (#{user.username}, #{user.age}, #{user.email}) /foreach /insert对应的Mapper接口方法int batchInsertUser(Param(users) ListUser users);4. choose标签多条件分支类似Java中的switch-casechoose标签用于实现多条件分支判断它包含when和otherwise子标签when标签用于指定满足的条件otherwise标签用于指定所有when条件都不满足时的默认操作。常用于“优先按某条件查询若不满足则按另一条件查询”的场景。实战案例优先按用户ID查询若ID为空则按姓名查询若姓名也为空则查询所有用户select idselectUserByChoose resultTypecom.example.entity.User SELECT id, username, age, email FROM t_user where choose when testid ! null AND id #{id} /when when testusername ! null and username ! AND username LIKE CONCAT(%, #{username}, %) /when otherwise AND 11 /otherwise /choose /where /select二、分页查询高效处理大量数据提升查询性能在实际项目中数据库中的数据量往往较大若一次性查询所有数据会导致内存占用过高、查询速度变慢甚至引发系统异常。此时分页查询就显得尤为重要。MyBatis实现分页的方式主要有两种原生RowBounds和PageHelper插件其中PageHelper插件因使用简单、功能强大成为主流选择。1. 原生RowBounds简单分页无需修改SQLRowBounds是MyBatis提供的原生分页方式它通过在查询时指定起始行和查询条数实现分页效果无需修改SQL语句。但需要注意的是RowBounds是基于内存分页即先查询所有数据再在内存中截取数据量较大时性能较差仅适用于小数据量场景。实战案例使用RowBounds实现分页查询用户列表第一步编写Mapper接口和XML查询语句无需添加分页条件// Mapper接口 ListUser selectAllUser();二步在Service层使用RowBounds实现分页Service public class UserServiceImpl implements UserService { Autowired private UserMapper userMapper; Override public ListUser selectUserByPage(int pageNum, int pageSize) { // pageNum页码pageSize每页条数起始行 (页码-1)*每页条数 int offset (pageNum - 1) * pageSize; RowBounds rowBounds new RowBounds(offset, pageSize); // 使用SqlSession调用查询方法传入RowBounds return userMapper.selectAllUser(rowBounds); } }2. PageHelper插件物理分页高效适配大量数据PageHelper是一款基于MyBatis的分页插件它采用物理分页即通过在SQL语句后添加LIMIT子句直接从数据库中查询分页数据性能远优于原生RowBounds同时支持多种数据库MySQL、Oracle、SQL Server等使用简单、配置方便。实战案例使用PageHelper实现分页查询第一步引入PageHelper依赖Mavendependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.6/version /dependency第二步配置PageHelperSpring Boot项目可省略默认配置即可如需自定义可在application.yml中配置pagehelper: helper-dialect: mysql # 指定数据库方言 reasonable: true # 分页合理化页码小于1时查第一页大于总页数时查最后一页 support-methods-arguments: true # 支持通过Mapper接口参数传递分页参数第三步在Service层使用PageHelper实现分页Service public class UserServiceImpl implements UserService { Autowired private UserMapper userMapper; Override public PageInfoUser selectUserByPage(int pageNum, int pageSize) { // 开启分页紧跟在该方法后的第一个查询会自动分页 PageHelper.startPage(pageNum, pageSize); // 查询所有用户会自动添加分页条件 ListUser userList userMapper.selectAllUser(); // 封装分页结果包含总条数、总页数、当前页数据等信息 PageInfoUser pageInfo new PageInfo(userList); return pageInfo; } }PageInfo封装了丰富的分页信息常用属性有total总记录数、pages总页数、list当前页数据、pageNum当前页码、pageSize每页条数等可直接返回给前端使用。三、MyBatis缓存机制减少数据库查询提升响应速度缓存是提升系统性能的重要手段MyBatis提供了一级缓存和二级缓存通过缓存查询结果避免重复查询数据库从而减少数据库压力提升系统响应速度。1. 一级缓存SqlSession级别的缓存默认开启一级缓存也称为本地缓存是SqlSession级别的缓存每个SqlSession都有自己的一级缓存不同SqlSession之间的缓存相互独立。一级缓存的工作机制是当通过SqlSession执行查询操作时MyBatis会先从一级缓存中查询若存在则直接返回结果若不存在则查询数据库并将结果存入一级缓存。当执行增、删、改操作或关闭SqlSession时一级缓存会被清空。实战案例验证一级缓存效果Test public void testFirstLevelCache() { // 获取第一个SqlSession try (SqlSession session1 MyBatisUtil.getSqlSession()) { UserMapper mapper1 session1.getMapper(UserMapper.class); // 第一次查询从数据库获取数据并存入一级缓存 User user1 mapper1.selectUserById(1L); System.out.println(user1); // 第二次查询同一SqlSession直接从一级缓存获取数据不查询数据库 User user2 mapper1.selectUserById(1L); System.out.println(user2 user1); // 输出true说明是同一个对象 // 执行更新操作清空一级缓存 User user new User(); user.setId(1L); user.setUsername(test_cache); mapper1.updateUser(user); session1.commit(); // 第三次查询缓存已清空重新从数据库获取数据 User user3 mapper1.selectUserById(1L); System.out.println(user3 user1); // 输出false } // 获取第二个SqlSession与第一个SqlSession的缓存相互独立 try (SqlSession session2 MyBatisUtil.getSqlSession()) { UserMapper mapper2 session2.getMapper(UserMapper.class); // 第四次查询新的SqlSession从数据库获取数据 User user4 mapper2.selectUserById(1L); System.out.println(user4 user1); // 输出false } }注意一级缓存默认开启无需额外配置若需要禁用一级缓存可在查询标签中添加flushCachetrue属性。2. 二级缓存Mapper级别的缓存需手动开启二级缓存也称为全局缓存是Mapper级别的缓存多个SqlSession可以共享同一个Mapper的二级缓存。二级缓存的工作机制是当SqlSession执行查询操作并提交事务后查询结果会存入二级缓存其他SqlSession查询相同的语句时会先从二级缓存中查询若存在则直接返回结果无需查询数据库。二级缓存的作用范围比一级缓存更广适用于查询频率高、数据变更频率低的场景。注意执行增、删、改操作时会自动清空当前Mapper的二级缓存确保缓存数据与数据库数据一致若需要禁用某条查询的二级缓存可在查询标签中添加useCachefalse属性。四、MyBatis注解开发替代XML简化开发流程MyBatis不仅支持XML配置开发还提供了注解开发方式通过在Mapper接口方法上添加注解直接指定SQL语句无需编写XML文件简化了开发流程适用于简单的CRUD操作场景。常用的注解有Select、Insert、Update、Delete、Results等。1. 基础CRUD注解使用Mapper public interface UserMapper { // 查询根据ID查询用户 Select(SELECT id, username, age, email FROM t_user WHERE id #{id}) User selectUserById(Long id); // 插入新增用户 Insert(INSERT INTO t_user (username, age, email) VALUES (#{username}, #{age}, #{email})) Options(useGeneratedKeys true, keyProperty id) // 自动生成主键并赋值给实体类的id属性 int insertUser(User user); // 更新修改用户信息 Update(UPDATE t_user SET username #{username}, age #{age}, email #{email} WHERE id #{id}) int updateUser(User user); // 删除根据ID删除用户 Delete(DELETE FROM t_user WHERE id #{id}) int deleteUserById(Long id); // 多条件查询结合动态SQL注解 Select({ script, SELECT id, username, age, email FROM t_user, where, if testusername ! null and username ! \\, AND username LIKE CONCAT(%, #{username}, %), /if, if testage ! null, AND age #{age}, /if, /where, /script }) ListUser selectUserByCondition(Param(username) String username, Param(age) Integer age); }注意使用注解开发动态SQL时需要用script标签包裹SQL语句内部标签用法与XML一致对于复杂的SQL语句建议还是使用XML开发可读性更高。2. 结果映射注解Results当实体类属性名与数据库表字段名不一致时可使用Results注解实现结果映射替代XML中的resultMap标签。Select(SELECT id, user_name, user_age, user_email FROM t_user WHERE id #{id}) Results({ Result(column id, property id), Result(column user_name, property username), // 表字段user_name映射到实体类username Result(column user_age, property age), // 表字段user_age映射到实体类age Result(column user_email, property email) // 表字段user_email映射到实体类email }) User selectUserById(Long id);五、MyBatis-Plus入门简化CRUD提升开发效率MyBatis-Plus简称MP是基于MyBatis的增强工具在MyBatis的基础上做了无侵入增强提供了通用的CRUD方法、分页查询、条件构造器等功能无需编写基础CRUD的SQL语句极大地提升了开发效率。1. 快速入门配置与基础使用第一步引入MyBatis-Plus依赖Mavendependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency第二步配置数据库连接application.yml与MyBatis配置一致spring: datasource: url: jdbc:mysql://localhost:3306/mybatis_demo?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneUTC username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true # 开启驼峰命名自动映射 mapper-locations: classpath:mapper/*.xml # 若有自定义XML指定路径第三步编写实体类使用注解指定表名和主键Data TableName(t_user) // 指定对应的数据库表名 public class User { TableId(type IdType.AUTO) // 指定主键自增策略 private Long id; private String username; private Integer age; private String email; // 无参、有参构造方法可省略使用Data注解自动生成getter/setter、toString等方法 }2. 核心亮点条件构造器QueryWrapperMyBatis-Plus提供了QueryWrapper条件构造器用于动态拼接查询条件替代传统的动态SQL使用更加简洁。实战案例根据姓名模糊查询、年龄大于指定值的用户Override public ListUser selectUserByCondition(String username, Integer age) { // 创建条件构造器 QueryWrapperUser queryWrapper new QueryWrapper(); // 姓名模糊查询 queryWrapper.like(StringUtils.isNotBlank(username), username, username); // 年龄大于指定值 queryWrapper.gt(age ! null, age, age); // 执行查询 return userMapper.selectList(queryWrapper); }六、MyBatis优化技巧总结结合以上进阶特性总结MyBatis优化数据库操作的核心技巧帮助大家在实际项目中提升系统性能和开发效率合理使用动态SQL根据业务场景选择合适的动态SQL标签if、where、foreach等避免编写冗余SQL同时注意SQL拼接的语法正确性。优先使用物理分页对于大量数据的分页查询优先使用PageHelper插件或MyBatis-Plus的分页功能避免使用原生RowBounds的内存分页提升查询性能。合理配置缓存一级缓存默认开启无需额外配置二级缓存适用于查询频率高、数据变更频率低的场景开启后可减少数据库查询次数但需注意缓存的清空策略确保数据一致性。注解与XML结合使用简单的CRUD操作使用注解开发简化流程复杂的SQL语句如多表关联查询、复杂动态SQL使用XML开发提升可读性和维护性。使用MyBatis-Plus简化开发对于基础CRUD操作优先使用MyBatis-Plus的通用方法和条件构造器减少重复代码提升开发效率同时可结合MyBatis的原生特性实现复杂业务需求。优化SQL语句避免使用SELECT *只查询需要的字段合理使用索引提升查询速度减少多表关联查询的复杂度必要时可使用分步查询。七、总结本文详细讲解了MyBatis的进阶特性包括动态SQL、分页查询、缓存机制同时补充了注解开发和MyBatis-Plus入门知识并结合实战案例进行了演示。这些进阶特性是优化数据库操作、提升系统性能和开发效率的关键希望大家在实际项目中灵活运用结合业务场景选择合适的技术方案。MyBatis的学习是一个循序渐进的过程基础CRUD是入门进阶特性是提升而真正的精通需要结合实际项目不断总结和优化。后续我将继续分享MyBatis的高级用法如多表关联查询、存储过程调用等欢迎大家持续关注