You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何简化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自带功能简化映射

  1. 直接映射单表数据
    jOOQ的Record支持直接通过into(Class)方法转换为DTO,无需先转成对应表的Record子类。比如你代码里的productRecord.into(new ProductDTO())可以简化为record.into(ProductDTO.class),减少中间对象创建。

  2. 用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);
    
  3. 抽象通用分组映射逻辑
    如果多个DAO都有类似的一对多映射需求,可以在AbstractViewDAO中封装通用方法,接收分组键、DTO类型、关联列表设置逻辑等参数,减少重复代码。

二、使用MapStruct实现自动化映射

MapStruct是Java bean映射的代码生成工具,能和jOOQ完美配合,自动生成类型安全的映射代码,彻底替代手动转换:

  1. 定义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);
    }
    
  2. 在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 21:38:11