Spring Data JPA同请求查询3个子类返回按日期排序的Page分页对象
实现方案
你当前使用@MappedSuperclass的继承模式不会为父类生成独立数据表,三个子类对应三张完全独立的表,JPA默认无法直接跨表做统一排序分页,可根据项目实际情况选择以下两种实现路径:
方案一:调整JPA继承策略(推荐,适合新项目/可调整实体结构的场景)
JPA原生支持多态查询,只需要把父类的@MappedSuperclass替换为实体继承注解,即可直接实现跨表的统一分页查询,无需手写复杂SQL。
- 修改父类实体注解,选择
TABLE_PER_CLASS继承策略(该策略下每个子类表独立存储父类+自身字段,和你现有表结构完全兼容,不需要调整表字段):
//lombok annotations @Entity @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) public class Parent { @Id // 注意:不要用IDENTITY自增主键,会导致三张表主键重复,建议改用雪花ID等全局唯一主键生成策略 @GeneratedValue(generator = "snowflake") @GenericGenerator(name = "snowflake", strategy = "com.example.SnowflakeIdGenerator") private Long id; private String city; // 原字段名City首字母大写不符合小驼峰规范,调整后和返回结构的city字段匹配 private Date date; }
- 给三个子类加上
@Entity注解,原有字段逻辑无需修改:
//lombok annotations @Entity public class Child1 extends Parent { private Obj1 obj1; } //lombok annotations @Entity public class Child2 extends Parent { private Obj1 obj2; } //lombok annotations @Entity public class Child3 extends Parent { private Obj1 obj3; }
- 新增父类通用Repository,直接调用内置的分页查询方法即可:
public interface ParentRepository extends JpaRepository<Parent, Long> {}
- 业务层直接调用查询,传入排序分页参数即可得到符合要求的
Page<Parent>结果,JPA会自动生成UNION ALL语句合并三张表的数据、按date排序后分页,返回的实体序列化为JSON时会自动带上子类独有的obj1/obj2/obj3字段,完全匹配你给出的返回结构:
@Service public class ParentService { @Autowired private ParentRepository parentRepository; public Page<Parent> queryPage(int pageNum, int pageSize) { Pageable pageable = PageRequest.of(pageNum, pageSize, Sort.by(Direction.ASC, "date")); return parentRepository.findAll(pageable); } }
方案二:手动实现跨表分页(适合老项目/无法调整现有实体映射的场景)
如果不能修改现有@MappedSuperclass的实体结构,可以通过原生SQL+批量查询的方式手动实现分页,避免全表拉取到内存排序的性能问题:
- 业务层实现逻辑:先统计三张表总数据量,再通过原生SQL
UNION ALL排序分页取出当前页的ID和对应实体类型,最后分类型批量查询实体按顺序组装即可,示例代码如下:
@Service public class ParentQueryService { @Autowired private Child1Repository child1Repository; @Autowired private Child2Repository child2Repository; @Autowired private Child3Repository child3Repository; @PersistenceContext private EntityManager entityManager; public Page<Parent> getMergedPage(int pageNum, int pageSize) { Sort sort = Sort.by(Sort.Direction.ASC, "date"); Pageable pageable = PageRequest.of(pageNum, pageSize, sort); // 统计总记录数 long total = child1Repository.count() + child2Repository.count() + child3Repository.count(); if (total == 0) { return Page.empty(pageable); } // 原生SQL查询当前页的ID和实体类型,表名字段名替换成你实际数据库的名称 String unionSql = """ (SELECT id, date, 'child1' AS type FROM child1) UNION ALL (SELECT id, date, 'child2' AS type FROM child2) UNION ALL (SELECT id, date, 'child3' AS type FROM child3) ORDER BY date ASC LIMIT ? OFFSET ? """; int offset = (int) pageable.getOffset(); List<?> idRows = entityManager.createNativeQuery(unionSql) .setParameter(1, pageSize) .setParameter(2, offset) .getResultList(); // 按类型分组收集ID List<Long> c1Ids = new ArrayList<>(), c2Ids = new ArrayList<>(), c3Ids = new ArrayList<>(); for (Object row : idRows) { Object[] cols = (Object[]) row; Long id = ((Number) cols[0]).longValue(); String type = (String) cols[2]; switch (type) { case "child1" -> c1Ids.add(id); case "child2" -> c2Ids.add(id); case "child3" -> c3Ids.add(id); } } // 批量查询对应实体 Map<String, Map<Long, Parent>> entityMap = Map.of( "child1", new HashMap<>(), "child2", new HashMap<>(), "child3", new HashMap<>() ); if (!c1Ids.isEmpty()) { child1Repository.findAllById(c1Ids).forEach(e -> entityMap.get("child1").put(e.getId(), e)); } if (!c2Ids.isEmpty()) { child2Repository.findAllById(c2Ids).forEach(e -> entityMap.get("child2").put(e.getId(), e)); } if (!c3Ids.isEmpty()) { child3Repository.findAllById(c3Ids).forEach(e -> entityMap.get("child3").put(e.getId(), e)); } // 按排序顺序组装结果 List<Parent> content = new ArrayList<>(); for (Object row : idRows) { Object[] cols = (Object[]) row; Long id = ((Number) cols[0]).longValue(); String type = (String) cols[2]; content.add(entityMap.get(type).get(id)); } return new PageImpl<>(content, pageable, total); } }
注意事项
- 方案一必须保证三个子表主键全局唯一,禁止使用数据库自增ID,避免不同表出现相同ID导致数据映射错误。
- 方案二的原生SQL需要和你实际数据库的表名、字段名保持一致,如果需要多字段排序,直接修改
ORDER BY后的条件即可。 - 你原有父类中
City字段名首字母大写,不符合Java编码规范,会导致JSON序列化后字段名首字母大写,和你给出的返回示例不符,建议调整为小驼峰city。
内容的提问来源于stack exchange,提问作者xp test
相关产品推荐
相关产品推荐

