关键字: JPA复杂查询,JPA返回自定义实体,JPA返回自定义DTO,JPA联表查询,JPA原生SQL查询,JPA踩坑
注意看错误日志ConverterNotFoundException: No converter found capable of converting from type
既然是converter没有找到那就注入对应的converter就完事儿了,
查出的原始类型为AbstractJpaQueryT u p l e C o n v e r t e r TupleConverterTupleConverterTupleBackedMap,是一个Map,也就是Map转实体呗,对应的实体注入一个Map转dto的converter就行了
继续看错误日志, at org.springframework.data.repository.query.ResultProcessor.processResult(ResultProcessor.java:156) ~[spring-data-commons-2.2.5.RELEASE.jar:2.2.5.RELEASE]
在这个ResultProcessor有所发现,用到的ConversionService是通过DefaultConversionService.getSharedInstance()获取的,如下图:
那么把我们需要注入的converter注入到DefaultConversionService.getSharedInstance()应该就能实现自动转换为自定义Dto了
实现方案:
1.添加自定义注解@JpaDto
JpaDto.java
package com.lmt.zeus.jpa.annotation;
/**
* @description 自定义注解表示,加在类上表示是一个JpaDto类
* @author bazhandao
* @date 2020/3/26 16:39
* @since JDK1.8
*/
import org.springframework.stereotype.Component;
import java.lang.annotation.*;
@Documented
@Component
@Target(value = {ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface JpaDto {}
2.添加ZeusJpaConfiguration类实现注入加有@JpaDto注解的类对应的converter功能
ZeusJpaConfiguration.java
package com.lmt.zeus.jpa.config;
import com.lmt.zeus.jpa.annotation.JpaDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.FatalBeanException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.convert.support.GenericConversionService;
import javax.annotation.PostConstruct;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Map;
/**
* @description
*
* @author bazhandao
* @date 2020/3/26 17:51
* @since JDK1.8
*/
@Slf4j
@Configuration
public class ZeusJpaConfiguration {
@Autowired
private ApplicationContext applicationContext;
/**
* 初始化注入@JpaDto对应的Converter
*/
@PostConstruct
public void init() {
Map<String, Object> map = applicationContext.getBeansWithAnnotation(JpaDto.class);
for (Object o : map.values()) {
Class c = o.getClass();
log.info("Jpa添加Converter,class={}", c.getName());
GenericConversionService genericConversionService = ((GenericConversionService) DefaultConversionService.getSharedInstance());
genericConversionService.addConverter(Map.class, c, m -> {
try {
Object obj = c.newInstance();
// 这里可以扩展,注入的converter,实现sql查寻出的结果为数据库中带下划线的字段,通过程序转为驼峰命名再设置到实体中
// 也可以做类型转换判断,这里未做类型判断,直接copy到dto中,类型不匹配的时候可能会出错
return copyMapToObj(m, obj);
} catch (Exception e) {
throw new FatalBeanException("Jpa结果转换出错,class=" + c.getName(), e);
}
});
}
}
/**
* 将map中的值copy到bean中对应的字段上
* @author bazhandao
* @date 2020-03-26
* @param map
* @param target
* @return
*/
private Object copyMapToObj(Map<String, Object> map, Object target) {
if(map == null || target == null || map.isEmpty()){
return target;
}
Class<?> actualEditable = target.getClass();
PropertyDescriptor[] targetPds = BeanUtils.getPropertyDescriptors(actualEditable);
for (PropertyDescriptor targetPd : targetPds) {
if(targetPd.getWriteMethod() == null) {
continue;
}
try {
String key = targetPd.getName();
Object value = map.get(key);
if (value == null) {
continue;
}
Method writeMethod = targetPd.getWriteMethod();
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}
writeMethod.invoke(target, value);
} catch (Exception ex) {
throw new FatalBeanException("Could not copy properties from source to target", ex);
}
}
return target;
}
}
3.实体类
package com.lmt.stock.data;
import com.lmt.zeus.jpa.annotation.JpaDto;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
/**
* @description 小试牛刀
*
* @author bazhandao
* @date 2020/3/26 13:53
* @since JDK1.8
*/
@JpaDto // 注意实体类要加上@JpaDto注解,将该类注入到容器
@Getter
@Setter
public class XxxDto {
BigDecimal profitPercent;
String accountCode;
}
4.接口实现
import com.lmt.stock.data.XxxDto;
import com.lmt.stock.data.entity.StockHisHq;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
/**
* @author bazhandao
* @date 2020/3/23 11:08
* @since JDK1.8
*/
public interface StockHisHqRepository extends JpaRepository<StockHisHq, Long> {
// 这里可以用HQL查询,也可以用原生SQL查询
// 查寻出的字段命名要规范
// @Query(value = "select a.account_code as accountCode, b.profit_percent as profitPercent from trade_account a, trade_order b where a.account_code = b.account_code and a.account_code = ?1", nativeQuery = true)
@Query(value = "select a.accountCode as accountCode, b.profitPercent as profitPercent from TradeAccount a, TradeOrder b where a.accountCode = b.accountCode and a.accountCode = ?1")
List<XxxDto> findXxxDtoByAccountCode(String accountCode);
}
ps:该方案可以使用原生SQL也可以使用HQL,只要查寻出的结果字段名跟实体中的字段名保持一致即可,还可以对注入的converter做扩展,支持更多功能
这里使用了自定义注解将dto注入到容器,通过Spring Boot的Configuration功能实现注入该dto对应的converter功能
也可以不用自定义注解,通过自定义类扫描器或者通过配置加载到dto对应的class注入对应的converter,总之只要开拓思维玩法是多样的