如何简化jOOQ中Record到DTO的映射?Java/jOOQ有无辅助类?
我编写了一个DAO抽象类,所有DAO均继承自该类,它提供基于通用查询参数模型的典型远程分页功能。该类从多表中获取关联数据,并以List<Record>参数形式传递给具体DAO,开发人员需将这些Record实例转换为DTO实例(单个DTO可能需由多个Record实例构造)。示例代码如下:
public class ProductViewDAO extends AbstractViewDAO<ProductRecord, ProductDTO, Long> { ... @Override protected TableOnConditionStep<Record> getViewJoins() { return Product.PRODUCT .leftJoin(ProductLang.PRODUCT_LANG) .on(ProductLang.PRODUCT_LANG.PRODUCTID .eq(Product.PRODUCT.PRODUCTID)); } @Override protected List<ProductDTO> recordsToView(List<Record> records) { List<ProductDTO> products = new ArrayList<>(); Map<Long, ProductDTO> productMap = new HashMap<>(); for (Record record : records) { Long productId = record.get(Product.PRODUCT.PRODUCTID); ProductDTO productDTO = productMap.get(productId); if (productDTO == null) { ProductRecord productRecord = record.into(new ProductRecord()); productDTO = productRecord.into(new ProductDTO()); List<ProductLangDTO> xLangs = new ArrayList<>(); productDTO.setLangs(xLangs); products.add(productDTO); productMap.put(productId, productDTO); } ProductLangRecord xLangRecord = record.into(new ProductLangRecord()); ProductLangDTO xLang = xLangRecord.into(new ProductLangDTO()); productDTO.getLangs().add(xLang); if (xLang.getLangId().equals(requestContext().getLangId())) { productDTO.setLang(xLang); } } return products; } ... } ... public abstract class AbstractViewDAO<R extends UpdatableRecord<R>, P extends AbstractDTO, T> extends AbstractBaseDAO<R, T> { ... public List<P> query(final QueryParameters queryParameters) throws DataAccessException { ... var result = getViewQuery().where(equal(pk, ids)).fetch(); return recordsToView(result); } }
请问如何简化这一转换过程?Java或jOOQ是否提供辅助类来自动化此类Record到DTO的映射?
一、利用jOOQ自带功能简化映射
直接映射单表数据
jOOQ的Record支持直接通过into(Class)方法转换为DTO,无需先转成对应表的Record子类。比如你代码里的productRecord.into(new ProductDTO())可以简化为record.into(ProductDTO.class),减少中间对象创建。用
fetchGroups()自动分组关联数据
针对一对多的关联场景,jOOQ的Result提供了fetchGroups()方法,可直接按主键分组结果,省去手动维护Map的代码:@Override protected List<ProductDTO> recordsToView(List<Record> records) { // 按ProductID分组,Key为ProductID,Value为对应Record列表 Map<Long, List<Record>> groupedRecords = records.stream() .collect(Collectors.groupingBy(r -> r.get(Product.PRODUCT.PRODUCTID))); return groupedRecords.entrySet().stream().map(entry -> { Long productId = entry.getKey(); List<Record> productRelatedRecords = entry.getValue(); // 取第一条记录映射Product基础信息 ProductDTO dto = productRelatedRecords.get(0).into(ProductDTO.class); // 批量映射关联的语言DTO List<ProductLangDTO> langs = productRelatedRecords.stream() .map(r -> r.into(ProductLangDTO.class)) .collect(Collectors.toList()); dto.setLangs(langs); // 设置当前语言的Lang对象 Long currentLangId = requestContext().getLangId(); dto.setLang(langs.stream() .filter(l -> l.getLangId().equals(currentLangId)) .findFirst() .orElse(null)); return dto; }).collect(Collectors.toList()); }也可以直接用jOOQ原生的
fetchGroups(Field)方法:Map<Long, Result<Record>> grouped = result.fetchGroups(Product.PRODUCT.PRODUCTID);抽象通用分组映射逻辑
如果多个DAO都有类似的一对多映射需求,可以在AbstractViewDAO中封装通用方法,接收分组键、DTO类型、关联列表设置逻辑等参数,减少重复代码。
二、使用MapStruct实现自动化映射
MapStruct是Java bean映射的代码生成工具,能和jOOQ完美配合,自动生成类型安全的映射代码,彻底替代手动转换:
定义Mapper接口
@Mapper(componentModel = "spring") // 若用Spring可指定组件模型,方便注入 public interface ProductMapper { ProductMapper INSTANCE = Mappers.getMapper(ProductMapper.class); // 从Record映射到ProductDTO ProductDTO toProductDTO(Record record); // 从Record映射到ProductLangDTO ProductLangDTO toProductLangDTO(Record record); }在DAO中使用Mapper
@Override protected List<ProductDTO> recordsToView(List<Record> records) { Map<Long, ProductDTO> productMap = new HashMap<>(); for (Record record : records) { Long productId = record.get(Product.PRODUCT.PRODUCTID); // 不存在则创建并初始化 ProductDTO dto = productMap.computeIfAbsent(productId, k -> { ProductDTO newDto = ProductMapper.INSTANCE.toProductDTO(record); newDto.setLangs(new ArrayList<>()); return newDto; }); ProductLangDTO langDto = ProductMapper.INSTANCE.toProductLangDTO(record); dto.getLangs().add(langDto); if (langDto.getLangId().equals(requestContext().getLangId())) { dto.setLang(langDto); } } return new ArrayList<>(productMap.values()); }MapStruct会在编译时生成实现类,性能和手动代码一致,还能避免手动映射的错误。
三、用jOOQ Multiset直接构造嵌套DTO(jOOQ 3.15+)
对于复杂的嵌套DTO场景,可以直接在查询阶段用Multiset构造嵌套集合,完全避免内存中的分组转换:
List<ProductDTO> products = ctx.select( Product.PRODUCT.PRODUCTID, Product.PRODUCT.NAME, // 嵌套查询构造ProductLangDTO集合 multiset( select(ProductLang.PRODUCT_LANG.LANGID, ProductLang.PRODUCT_LANG.NAME) .from(ProductLang.PRODUCT_LANG) .where(ProductLang.PRODUCT_LANG.PRODUCTID.eq(Product.PRODUCT.PRODUCTID)) ).as("langs").convertFrom(r -> r.map(Records.mapping(ProductLangDTO::new))) ) .from(Product.PRODUCT) .fetch(Records.mapping(ProductDTO::new));
这种方式直接从数据库获取结构化的DTO数据,效率更高,代码更简洁。
内容的提问来源于stack exchange,提问作者funkrusher

