jpa查询返回自定义对象、返回指定VO、POJO

本文探讨了在JPA查询中如何避免不优雅的解决方案,如Map结果、SQL构造等,提出通过分析源码配置DefaultConversionService来实现自定义对象到VO的转换,提供了一个EntityToVoGenericConverter示例,以提升查询性能并保持代码简洁。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

jpa查询返回自定义对象、返回指定VO、POJO

jpa查询返回自定义对象、返回指定VO、POJO,JPA查询前会做大量处理,还有线程通知的操作。若并发大,处理性能直线下降。但是jpa就因为做了大量处理,对多数据库兼容极好,操作方便。

有时候你想查询某个表,不想要某个字段内容太长时;或要返回非entity的对象时,需要自定义。

这时候你就会先百度、google一下,找到如下方案:

  • 1、使用session.createQuery自定义返回Map结果,撸编写一大串代码,jpa就是为了简化代码编写,背道而驰了。
  • 2、在查询的sql中直接使用新建构造对象的: select new top.lingkang.lalanote.vo.FolderVo(e.id,e.name) from FolderEntity e 不优雅,一点也不优雅,你还要维护返回值的构造函数
  • 3、JpaRepository中查询结果使用数组的:List<Object[]> query();,后期维护可能存在变动、也不优雅
  • 4、JpaRepository中查询返回结果是一个接口类的,查询pojo、vo写一堆接口层层转换也不优雅

基于以上种种,我立马分析源码看看怎么配置优雅地返回自定义结果,于是有了这篇文章,包含了我对java的一些理解。花了好几个小时,帮我点点赞吧!

转自:https://lingkang.top/archives/jpa-cha-xun-fan-hui-zi-ding-yi-dui-xiang

1、查询返回某个VO

public interface FolderRepository extends JpaRepository<FolderEntity, String> {
    @Query("select e from FolderEntity e")
    public List<FolderVo> get();
}

你要查询某个VO时,这样写会报错:类型转换失败

No converter found capable of converting from type [top.lingkang.lalanote.entity.FolderEntity] to type [top.lingkang.lalanote.vo.FolderVo]

它抛出异常的地方是这里GenericConversionService.handleConverterNotFound,上一级调用是convert
在这里插入图片描述

通过异常栈发现处理返回结果转换的类是ResultProcessor.processResult
再往上也没啥看头了,我打个断点看ResultProcessor.ProjectingConverter.convert
在这里插入图片描述

发现private final ConversionService conversionService;DefaultConversionService默认转换服务,DefaultConversionService默认的转换服务是spring-core所有。

jpa的结果处理:ResultProcessor.ChainingConverter.and 如下

  return intermediate == null || targetType.isInstance(intermediate) ? intermediate
    : converter.convert(intermediate);

2、源码分析

我们的返回结果是FolderVo,不是表映射实体类FolderEntity 所以targetType.isInstance(intermediate)结果是false,它进入了spring的默认结果转换:DefaultConversionService,其中DefaultConversionService是继承GenericConversionService的。其中的转换处理方法是:

	@Nullable
	protected GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) {
		ConverterCacheKey key = new ConverterCacheKey(sourceType, targetType);
		GenericConverter converter = this.converterCache.get(key);
		if (converter != null) {
			return (converter != NO_MATCH ? converter : null);
		}

		// 默认的转换中找不到 FolderEntity 结果转 FolderVo
		converter = this.converters.find(sourceType, targetType);
		if (converter == null) {
			converter = getDefaultConverter(sourceType, targetType);
		}

		if (converter != null) {
			this.converterCache.put(key, converter);
			return converter;
		}

		this.converterCache.put(key, NO_MATCH);
		return null;
	}

我断点找了一两个小时发现jpa的ResultProcessor是不开放配置的,初始化时已经是固定了,底层执行类也是私有的,无法进行直接配置。
那么只能从默认的转换服务下手,即配置GenericConversionService.converters,在converters添加上我们需要的类转换:FolderEntityFolderVo,而且它有对应的添加方法:

private final Converters converters = new Converters();


public void addConverter(Converter<?, ?> converter)

看了一遍也没能发现配置GenericConversionService的入口,上面提到过,ResultProcessor数据转换调用的是DefaultConversionService查看了源码发现给我们开放了这个实例:

	/**
	 * Return a shared default {@code ConversionService} instance,
	 * lazily building it once needed.
	 * <p><b>NOTE:</b> We highly recommend constructing individual
	 * {@code ConversionService} instances for customization purposes.
	 * This accessor is only meant as a fallback for code paths which
	 * need simple type coercion but cannot access a longer-lived
	 * {@code ConversionService} instance any other way.
	 * @return the shared {@code ConversionService} instance (never {@code null})
	 * @since 4.3.5
	 */
	public static ConversionService getSharedInstance() {
		DefaultConversionService cs = sharedInstance;
		if (cs == null) {
			synchronized (DefaultConversionService.class) {
				cs = sharedInstance;
				if (cs == null) {
					cs = new DefaultConversionService();
					sharedInstance = cs;
				}
			}
		}
		return cs;
	}

注释中说明了这是一个公共共享的转换服务,我们可以直接拿到操作它,往里面添加我们的转换器:
FolderEntityFolderVo

3、定义结果解析,entity转pojo、vo等

首先定义一个转换器:

import cn.hutool.core.bean.BeanUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.GenericConverter;
import top.lingkang.lalanote.entity.FolderEntity;
import top.lingkang.lalanote.vo.FolderVo;

import java.util.HashSet;
import java.util.Set;

/**
 * @author lingkang
 * Created by 2023/8/12
 */
@Slf4j
public class EntityToVoGenericConverter implements GenericConverter {

    // 不必担心性能问题,底层使用了cache存储处理
    @Override
    public Set<ConvertiblePair> getConvertibleTypes() {
        Set<ConvertiblePair> convertiblePairs = new HashSet<>();
        // 其他转换类可以直接在此添加(这样写是定向)
        // convertiblePairs.add(new ConvertiblePair(FolderEntity.class, FolderVo.class));
        // 或者写成这样,这样会匹配所有对象进行转换(推荐)不必担心性能问题,底层使用了cache存储处理
        convertiblePairs.add(new ConvertiblePair(Object.class, Object.class));
        return convertiblePairs;
    }

    @Override
    public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
        // System.out.println(source);
        try {
            // 直接创建结果对象
            Object instance = targetType.getType().getDeclaredConstructor().newInstance();
            // hutool-core中的bean复制:FolderEntity 复制属性到 FolderVo
            BeanUtil.copyProperties(source, instance);
            // 返回结果: FolderVo
            return instance;
        } catch (Exception e) {
            log.error("无法解析的映射!", e);
            throw new RuntimeException(e);
        }
    }
}

添加一个springboot启动后追加初始化设置

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.stereotype.Component;

/**
 * @author lingkang
 * Created by 2023/8/12
 */
@Component
@Order(1) //如果多个自定义的 ApplicationRunner  ,用来标明执行的顺序
public class StartRunAfterInit implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) throws Exception {
        DefaultConversionService sharedInstance = (DefaultConversionService) DefaultConversionService.getSharedInstance();
        // 加入我们的解析 FolderEntity → FolderVo
        sharedInstance.addConverter(new EntityToVoGenericConverter());
    }
}

再执行一次查询,能根据解析返回结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yEEerySS-1691861436064)(/upload/2023/08/image-1691854158520.png)]
在这里插入图片描述

断点也能找到我定义的处理类: EntityToVoGenericConverter
在这里插入图片描述

4、只查询某几个字段

有时候,你不可能把所有的字段都查询出来,只查询其中的两个、或两个以上。(查一个字段可以直接类型返回) 总不能分两次查询、使用数组接收、map接收?这样不优雅,按照上面的配置,我们可以这样:

public interface FolderRepository extends JpaRepository<FolderEntity, String> {
    // 1、底层逻辑是将表所有字段查询出来,通过上面定义的映射映射到VO
    // 2、不推荐直接这样写,因为表中有大字段时,会将结果返回到应用。再映射到vo
    // 我们本身不需要大字段,而且为了提升查询性能、迁移等,最好一个个字段进行查询
    // 即下面的那种查询方法:一个个要的字段进行 as 映射到vo,既提升性能又拿到想要的字段数据
    @Query("select e from FolderEntity e")
    public List<FolderVo> get();

    // 一定要加上 as XXX 否则将无法解析到 AbstractJpaQuery.TupleConverter.TupleBackedMap 中的key值
    // 像下面的 e.createTime 将无法被解析到
    @Query("select e.id as id,e.name as name,e.createTime from FolderEntity e")
    List<FolderVo> getIdAndName();
}

EntityToVoGenericConverter中修改如下

    // 不必担心性能问题,底层使用了cache存储处理
    @Override
    public Set<ConvertiblePair> getConvertibleTypes() {
        Set<ConvertiblePair> convertiblePairs = new HashSet<>();
        // 其他转换类可以直接在此添加(这样写是定向)
        // convertiblePairs.add(new ConvertiblePair(FolderEntity.class, FolderVo.class));
        // 或者写成这样,这样会匹配所有对象进行转换(推荐)
        convertiblePairs.add(new ConvertiblePair(Object.class, Object.class));
        // 用于解析 JpaQueryFactory.TupleConverter.TupleBackedMap(这样写是定向)
        // select e.id as id,e.name as name,e.createTime from FolderEntity e
        convertiblePairs.add(new ConvertiblePair(Map.class, Object.class));
        return convertiblePairs;
    }

执行调用

    @GetMapping("/t2")
    @ResponseBody
    public Object t2() {
        return folderRepository.getIdAndName();
    }

返回结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KcFQOBu5-1691861436065)(/upload/2023/08/image-1691859971475.png)]
可以看到,e.createTime未曾 as createTime导致无法映射到返回的实体类

5、实体类展示

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;

/**
 * @author lingkang
 * created by 2023/7/27
 */
@Data
@Entity
@Table(name = "n_folder")
public class FolderEntity extends BaseEntity {
    @Id
    @Column(name = "id")
    private String id;

    @Column(name = "parent_id")
    private String parentId;

    @Column(name = "name")
    private String name;

    @Column(name = "attr")
    private String attr;

    @Column(name = "type")
    private Integer type;
}
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
 * @author lingkang
 * created by 2023/8/3
 */
@Data
public class FolderVo implements Serializable {
    private String id;
    private String parentId;
    private String name;
    private String attr;
    private Integer type;
    private Date createTime;
    private Date updateTime;
}
<think>好的,我现在需要帮助用户了解如何在Spring Data JPA中使用SqlResultSetMapping来映射自定义SQL查询结果。首先,我得回忆一下相关的知识,并结合提供的引用内容来整理出一个清晰的回答。 用户的问题是关于SqlResultSetMapping的示例和用法,所以我需要先确定这个注解的作用。根据引用[1]和[2],SqlResultSetMapping用于结果集映射,特别是将查询结果映射到非实体对象VO(值对象)。此外,引用[3]和[4]提到,使用这个注解可以将原生SQL查询的结果映射到自定义POJO或者DTO,而引用[5]则列出了几种返回自定义DTO的方式,其中SqlResultSetMapping是其中之一。 接下来,我需要整理出使用SqlResultSetMapping的具体步骤。首先,用户需要定义一个自定义的DTO类,比如GroupVO,这个类需要有对应的构造方法和字段。然后,在实体类上使用@SqlResultSetMapping注解来定义映射规则,这里可能需要用到@Entity和@Table注解,但要注意DTO是否应该作为实体。不过根据引用[4],投影可能更简单,但用户明确询问的是SqlResultSetMapping,所以需要强调如何在实体类上配置。 然后,在Repository中编写自定义查询方法,使用@Query注解并设置nativeQuery为true,同时指定resultSetMapping属性为之前定义的映射名称。最后,在Service中调用这个方法,并验证返回的数据是否正确。 需要注意的几点是:确保字段别名与目标类属性一致,使用全参数构造方法,以及在实体类上配置映射可能带来的侵入性。此外,引用[2]提到优先使用JPQL,但用户的情况可能需要原生SQL,因此要提醒字段映射的谨慎处理。 现在需要将这些步骤详细地分点说明,并给出代码示例。同时,在回答最后生成相关问题,如其他映射方式、性能比较、常见错误等,以帮助用户进一步了解。</think>以下是在Spring Data JPA中使用$@SqlResultSetMapping$映射自定义SQL查询结果的完整实现流程: ### 一、核心步骤说明 1. **定义DTO/VO类** 创建需要接收查询结果的Java类(非实体类),需包含全参构造方法: ```java public class GroupVO { private String groupName; private Integer memberCount; public GroupVO(String groupName, Integer memberCount) { this.groupName = groupName; this.memberCount = memberCount; } // getters... } ``` 2. **配置结果集映射** 在任意实体类上通过$@SqlResultSetMapping$声明映射规则[^1][^3]: ```java @Entity @Table(name = "t_group") @SqlResultSetMappings({ @SqlResultSetMapping( name = "GroupVOMapping", classes = @ConstructorResult( targetClass = GroupVO.class, columns = { @ColumnResult(name = "group_name", type = String.class), @ColumnResult(name = "count", type = Integer.class) } ) ) }) public class GroupEntity { /* 实体类字段... */ } ``` 3. **编写原生SQL查询** 在Repository接口中使用$@Query$定义查询指定映射: ```java public interface GroupRepository extends JpaRepository<GroupEntity, Long> { @Query(nativeQuery = true, value = "SELECT g.name as group_name, COUNT(u.id) as count " + "FROM groups g LEFT JOIN users u ON g.id = u.group_id " + "GROUP BY g.name", resultSetMapping = "GroupVOMapping") List<GroupVO> findGroupStatistics(); } ``` 4. **调用查询方法** Service层直接使用映射后的DTO结果: ```java List<GroupVO> results = groupRepository.findGroupStatistics(); ``` ### 二、关键配置解析 | 配置项 | 作用说明 | |----------------------|--------------------------------------------------------------------------| | $name$ | 映射规则标识符,需与@Query的resultSetMapping对应 | | $targetClass$ | 指定目标DTO类(需全路径类名或已导入类) | | $columns$ | 字段映射配置,name对应SQL查询结果列别名,type定义数据类型 | | 全参构造方法 | DTO必须包含与columns顺序严格匹配的构造函数[^5] | ### 三、注意事项 1. **字段别名一致性** SQL查询中的列别名必须与$@ColumnResult$的name属性完全匹配,否则会抛出$ColumnNotFoundException$ 2. **类型安全** 推荐显式声明$type$参数以避免类型转换错误,特别是处理数字类型时 3. **映射位置限制** $@SqlResultSetMapping$必须定义在JPA实体类上,这是规范要求的强制约束[^1] 4. **替代方案对比** 相较于投影接口或JdbcTemplate方式,该方法更适用于复杂结果集的映射场景[^4][^5] $$性能优化建议:当处理大数据集时,可配合分页查询Pageable使用$$
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

凌康ACG

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值