如何让QueryDSL+Spring Data JPA投影使用Join而非多表查询?
你遇到的「没有使用Join而是多表查询」其实是QueryDSL的隐式连接(通过WHERE子句关联表),而非显式的JOIN语句。这种隐式连接不仅SQL可读性差,还可能因为默认的内连接逻辑导致你丢失预期的数据——这大概率是你结果行数比预期少的核心原因。
一、用显式Join替代隐式连接
要让QueryDSL生成标准的JOIN语句,你需要在查询构建时主动创建Join关系,而不是直接通过booking.endUser.uuid这种嵌套引用触发自动关联。
举个结合JPAQueryFactory的自定义仓库实现示例:
@Repository public class BookingCustomRepositoryImpl implements BookingCustomRepository { private final JPAQueryFactory queryFactory; private final QBooking booking = QBooking.booking; private final QUser user = QUser.user; public BookingCustomRepositoryImpl(EntityManager em) { this.queryFactory = new JPAQueryFactory(em); } @Override public List<BookingProjectionDTO> findBookingsWithUserUuid(BooleanBuilder predicate) { // 显式创建Join,可按需指定关联类型(比如leftJoin适配User可为null的场景) return queryFactory.select( Projections.bean(BookingProjectionDTO.class, booking.uuid.as("bookingUuid"), user.uuid.as("userUuid") ) ) .from(booking) .join(booking.endUser, user) // 显式关联User表 .where(predicate) .fetch(); } }
二、推荐用DTO做投影而非实体类
你之前直接投影到Booking实体类的做法并不合理:
- 实体类包含大量关联字段和业务逻辑,仅填充部分字段可能导致后续使用时触发懒加载异常(比如调用未投影的
endUser属性) - 用专门的DTO类更清晰,也不会污染实体类的状态
创建一个极简的投影DTO:
public class BookingProjectionDTO { private String bookingUuid; private String userUuid; // Projections.bean要求无参构造器 public BookingProjectionDTO() {} // Getter & Setter public String getBookingUuid() { return bookingUuid; } public void setBookingUuid(String bookingUuid) { this.bookingUuid = bookingUuid; } public String getUserUuid() { return userUuid; } public void setUserUuid(String userUuid) { this.userUuid = userUuid; } }
三、MappingProjection的适配方案
如果你偏好使用MappingProjection,同样需要确保查询中显式声明Join:
public class BookingProjection extends MappingProjection<BookingProjectionDTO> { public BookingProjection(Class<BookingProjectionDTO> type) { super(type, QBooking.booking, QUser.user); } @Override protected BookingProjectionDTO map(Record record) { BookingProjectionDTO dto = new BookingProjectionDTO(); dto.setBookingUuid(record.get(QBooking.booking.uuid)); dto.setUserUuid(record.get(QUser.user.uuid)); return dto; } }
查询时的调用方式:
return queryFactory.select(new BookingProjection(BookingProjectionDTO.class)) .from(booking) .join(booking.endUser, user) .where(predicate) .fetch();
四、为什么之前的方式会生成隐式连接?
当你直接在Projections.bean中引用booking.endUser.uuid时,QueryDSL会自动生成隐式表关联,表现为SQL中的WHERE booking0_.user_id=user12_.id,而非显式的JOIN user user12_ ON booking0_.user_id=user12_.id。这种隐式连接默认是内连接,若存在不符合关联条件的数据(即使你的userId是非空约束,其他复杂关联场景也可能出现),就会导致结果行数减少。
总结
你的优化思路完全正确——通过投影只获取需要的字段,既能减少不必要的表关联(如果不需要某些表的字段,甚至可以完全不关联它们),又能降低数据传输量。调整为显式Join+DTO投影的方式后,就能生成更可控、可读性更高的SQL,解决你当前的行数缺失问题。
内容的提问来源于stack exchange,提问作者vargen_

