如何用JOIN而非MULTISET实现jOOQ多层嵌套结构查询映射?
用jOOQ通过JOIN实现复杂嵌套结构映射(替代MULTISET)
你已经用MULTISET实现了Foo -> List
1. 编写JOIN查询语句
先用JOIN关联三个表,查询会返回扁平化结果集——每个Foo对应的每一组Bar+Bazz都会单独占一行。用LEFT JOIN可避免丢失无关联Bar的Foo:
public Result<?> findFlatFooData(String fooName) { return ctx().select(FOO.asterisk(), BAR.asterisk(), BAZZ.asterisk()) .from(FOO) .leftJoin(BAR).on(BAR.FOO_ID.eq(FOO.ID)) .leftJoin(BAZZ).on(BAZZ.ID.eq(BAR.BAZZ_ID)) .where(FOO.FOO_NAME.eq(fooName)) .fetch(); }
2. 反射式映射实现
接下来要把扁平化结果转成嵌套的Foo对象列表,核心逻辑是按Foo的唯一ID分组,为每个Foo收集对应的Bar和Bazz,用反射自动填充字段,减少手动赋值工作量。
封装通用反射映射工具
实现两个工具方法:一个负责将Record数据填充到实体类,另一个处理分组和嵌套关系:
import org.jooq.Record; import java.lang.reflect.Field; import java.util.*; import java.util.stream.Collectors; public class ReflectionMapper { // 将Record数据填充到实体对象 @SuppressWarnings("unchecked") public static <T> T mapRecordToEntity(Record record, Class<T> clazz) throws Exception { T entity = clazz.getDeclaredConstructor().newInstance(); for (Field field : clazz.getDeclaredFields()) { field.setAccessible(true); // 这里假设实体字段名转大写对应数据库列名(如fooName对应FOO_NAME),可根据实际命名规则调整 String columnName = field.getName().toUpperCase(); if (record.containsKey(columnName)) { field.set(entity, record.get(columnName)); } } return entity; } // 将扁平化结果转换为嵌套的Foo列表 public static List<Foo> mapFlatResultToNestedFoos(Result<?> result) throws Exception { // 按Foo的ID分组,同一Foo的所有行归为一组 Map<Object, List<Record>> fooGroup = result.stream() .collect(Collectors.groupingBy(r -> r.get(FOO.ID))); List<Foo> foos = new ArrayList<>(); for (Map.Entry<Object, List<Record>> entry : fooGroup.entrySet()) { List<Record> fooRecords = entry.getValue(); // 取第一行映射Foo基础信息 Foo foo = mapRecordToEntity(fooRecords.get(0), Foo.class); // 收集当前Foo对应的所有Bar和Bazz Set<Object> processedBarIds = new HashSet<>(); // 去重,避免重复添加同一Bar List<Bar> bars = new ArrayList<>(); for (Record record : fooRecords) { Object barId = record.get(BAR.ID); if (barId == null) continue; // 跳过无Bar的行 if (processedBarIds.contains(barId)) continue; Bar bar = mapRecordToEntity(record, Bar.class); Bazz bazz = mapRecordToEntity(record, Bazz.class); bar.setBazz(bazz); bars.add(bar); processedBarIds.add(barId); } foo.setBars(bars); foos.add(foo); } return foos; } }
整合查询与映射
将查询和映射工具结合,直接返回嵌套结构的列表:
public List<Foo> findByFooNameWithJoin(String fooName) throws Exception { Result<?> flatResult = ctx().select(FOO.asterisk(), BAR.asterisk(), BAZZ.asterisk()) .from(FOO) .leftJoin(BAR).on(BAR.FOO_ID.eq(FOO.ID)) .leftJoin(BAZZ).on(BAZZ.ID.eq(BAR.BAZZ_ID)) .where(FOO.FOO_NAME.eq(fooName)) .fetch(); return ReflectionMapper.mapFlatResultToNestedFoos(flatResult); }
3. 注意事项
- 字段与列名映射:工具类默认实体字段名转大写对应数据库列名,若你的命名规则不同(如下划线分隔),需调整
mapRecordToEntity中的列名转换逻辑,比如读取实体类的@Column注解获取真实列名。 - 去重处理:JOIN会生成重复行,必须用Bar的ID去重,避免同一Bar被多次添加到Foo的bars列表。
- 性能权衡:JOIN生成的扁平化结果集数据量通常比MULTISET子查询大,数据量大时需考虑性能差异。jOOQ官方更推荐
MULTISET,因为它在数据库层面是聚合操作,效率更高;但如果业务必须用JOIN,反射方式能大幅减少手动映射代码。 - 反射权限:若实体类字段为private,需保留
field.setAccessible(true);模块化项目还需在module-info.java中开放反射权限给工具类。
内容的提问来源于stack exchange,提问作者nogalo
相关产品推荐
相关产品推荐

