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的一些理解。花了好几个小时,帮我点点赞吧!
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
添加上我们需要的类转换:FolderEntity
→FolderVo
,而且它有对应的添加方法:
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;
}
注释中说明了这是一个公共共享的转换服务,我们可以直接拿到操作它,往里面添加我们的转换器:
FolderEntity
→FolderVo
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());
}
}
再执行一次查询,能根据解析返回结果
断点也能找到我定义的处理类: 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();
}
返回结果:
可以看到,e.createTime
未曾 as createTime
导致无法映射到返回的实体类
5、实体类展示
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;
import top.lingkang.lalanote.config.PrimaryGenerator;
/**
* @author lingkang
* created by 2023/7/27
*/
@Data
@Entity
@Table(name = "n_folder")
public class FolderEntity extends BaseEntity {
@Override
public void createEntity() {
super.createEntity();
if (id == null)
id = PrimaryGenerator.getId();
}
@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;
}