SpringBoot项目里,用JPAQueryFactory写动态查询,比MyBatis XML香在哪?

SpringBoot项目里,用JPAQueryFactory写动态查询,比MyBatis XML香在哪?

SpringBoot项目中JPAQueryFactory动态查询的实战优势

1. 类型安全:告别运行时SQL错误的噩梦

在传统MyBatis XML中,我们经常遇到这样的场景:修改了Java实体类字段名后,忘记同步XML中的SQL语句,直到运行时才抛出Column 'xxx' not found异常。而JPAQueryFactory通过元模型(Q-class)在编译期就能发现问题。

// 编译期就能发现拼写错误 QUser user = QUser.user; queryFactory.selectFrom(user) .where(user.usernmae.eq("admin")) // 这里会直接编译报错 .fetch();

类型安全带来的三大实际收益:

  1. IDE智能提示:输入user.后自动补全所有字段
  2. 重构友好:重命名实体字段时,所有查询代码自动更新
  3. 参数类型检查:避免把字符串误传给数字字段

对比MyBatis XML的典型问题:

<!-- 运行时才会暴露问题 --> <select id="findUser"> SELECT * FROM user WHERE username = #{usernmae} <!-- 拼写错误 --> </select>

2. 链式API:让动态查询像搭积木一样简单

处理后台管理系统的复杂筛选条件时,MyBatis需要这样写:

<select id="searchUsers" resultType="User"> SELECT * FROM user <where> <if test="name != null"> AND name LIKE #{name} </if> <if test="status != null"> AND status = #{status} </if> <!-- 更多条件判断 --> </where> </select>

而JPAQueryFactory的Java链式API更加直观:

public List<User> searchUsers(String name, UserStatus status) { return queryFactory.selectFrom(user) .where(name != null ? user.name.contains(name) : null) .where(status != null ? user.status.eq(status) : null) // 更多条件直接链式添加 .fetch(); }

复杂条件组合时,可以使用BooleanBuilder

BooleanBuilder builder = new BooleanBuilder(); if (region != null) { builder.and(user.region.eq(region)); } if (startDate != null && endDate != null) { builder.and(user.createTime.between(startDate, endDate)); } // 最终查询 queryFactory.selectFrom(user) .where(builder) .fetch();

3. 联表查询:比MyBatis更优雅的对象导航

处理用户和订单的一对多关系时,MyBatis需要手动配置<collection>

<resultMap id="userWithOrders" type="User"> <id property="id" column="id"/> <collection property="orders" ofType="Order"> <id property="id" column="order_id"/> </collection> </resultMap> <select id="findUserWithOrders" resultMap="userWithOrders"> SELECT u.*, o.id as order_id FROM user u LEFT JOIN order o ON u.id = o.user_id WHERE u.id = #{userId} </select>

而JPAQueryFactory直接利用实体关联关系:

List<User> users = queryFactory.selectFrom(user) .leftJoin(user.orders, order).fetchJoin() // 自动处理一对多 .where(user.id.eq(userId)) .fetch();

对于需要自定义结果的联表查询,可以使用DTO投影:

queryFactory.select( Projections.constructor( UserOrderDTO.class, user.id, user.name, order.id, order.totalAmount )) .from(user) .leftJoin(user.orders, order) .fetch();

4. 分页与排序:一行代码搞定复杂需求

MyBatis实现分页通常需要插件配合:

<select id="findUsers" resultType="User"> SELECT * FROM user ORDER BY ${sortField} ${sortOrder} LIMIT #{offset}, #{pageSize} </select>

而JPAQueryFactory的分页更加类型安全:

// 简单分页 Page<User> page = queryFactory.selectFrom(user) .orderBy(user.createTime.desc()) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetchResults(); // 动态排序 OrderSpecifier<?>[] orders = new OrderSpecifier[]{ pageable.getSort().getOrderFor("name").isAscending() ? user.name.asc() : user.name.desc() }; queryFactory.selectFrom(user) .orderBy(orders) .fetch();

5. 高级特性:MyBatis难以企及的能力

类型安全的子查询

// 查询订单金额高于平均值的用户 List<User> users = queryFactory.selectFrom(user) .where(user.id.in( JPAExpressions.select(order.user.id) .from(order) .groupBy(order.user.id) .having(order.amount.avg().lt(order.amount)) )) .fetch();

批量更新与删除

// 批量更新 long updatedCount = queryFactory.update(user) .set(user.status, UserStatus.INACTIVE) .where(user.lastLoginTime.lt(LocalDateTime.now().minusYears(1))) .execute(); // 批量删除 long deletedCount = queryFactory.delete(order) .where(order.createTime.lt(LocalDateTime.now().minusYears(3))) .execute();

自定义SQL函数支持

// 使用数据库特定函数 queryFactory.select( Expressions.stringTemplate( "DATE_FORMAT({0}, '%Y-%m')", user.createTime ).as("month"), user.count() ) .from(user) .groupBy(Expressions.stringTemplate("DATE_FORMAT({0}, '%Y-%m')", user.createTime)) .fetch();

6. 从MyBatis迁移的实战建议

混合使用策略

  1. 新功能直接使用JPAQueryFactory
  2. 复杂报表查询暂时保留MyBatis
  3. 逐步重写高频查询

性能优化技巧

// 启用二级缓存 @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) @Entity public class User { ... } // 查询时使用缓存 queryFactory.selectFrom(user) .setHint("org.hibernate.cacheable", true) .where(...) .fetch();

常见问题解决方案

  1. N+1查询问题:使用fetchJoin()
  2. 大结果集处理:使用流式查询
  3. 复杂统计查询:结合GroupBy使用
// 流式处理大数据量 try (Stream<User> userStream = queryFactory.selectFrom(user) .where(...) .stream()) { userStream.forEach(...); }

在Spring Data JPA生态中,JPAQueryFactory与Repository的完美配合:

public interface UserRepository extends JpaRepository<User, Long>, QuerydslPredicateExecutor<User> { // 自定义查询方法 default List<User> findActiveUsers() { return findAll(QUser.user.status.eq(Status.ACTIVE)); } }

7. 实际项目中的架构设计

分层架构建议

com.example.project ├── domain # 实体类 ├── repository # JPA Repository ├── query # 查询封装类 │ └── UserQuery.java └── service

查询封装示例

public class UserQuery { private String name; private LocalDateTime startDate; private LocalDateTime endDate; public Predicate toPredicate() { QUser user = QUser.user; BooleanBuilder builder = new BooleanBuilder(); if (StringUtils.isNotBlank(name)) { builder.and(user.name.containsIgnoreCase(name)); } if (startDate != null && endDate != null) { builder.and(user.createTime.between(startDate, endDate)); } return builder; } } // 服务层使用 public Page<User> searchUsers(UserQuery query, Pageable pageable) { return userRepository.findAll(query.toPredicate(), pageable); }

监控与调优

  1. 开启SQL日志:spring.jpa.show-sql=true
  2. 使用Hibernate统计:
spring.jpa.properties.hibernate.generate_statistics=true
  1. 定期分析慢查询