JPA Hibernate直接执行sql 转换成dto AliasToEntityMapResultTransformer

前言

公司中有一个老的项目使用了JPA+Hibernate框架,在接触了这个项目之后我的感受就是异常难用(o(╥﹏╥)o),相对于目前主流使用的Mybatis框架,在使用这种框架的项目中查询数据库有以下几种方式:

  1. 使用JpaRepository映射实体类和表。
@Repository
@Transactional(rollbackFor = Exception.class)
public interface OrderRepository extends JpaRepository<Order, Long> {

}

  • 使用这个Repository会自动映射出一些方法find方法,虽然可以不用手动写方法,但是相较于Mybatis-plus可以自由构建QueryWarp,被Mybatis-plus完全秒杀。
  1. 当遇到查询返回的结果不是对应一个表实体,这时候就麻烦了,因为DTO是无法映射JpaRepository,此时想要使用JpaRepository只能新建一个视图来映射DTO,后期数据库里可能会产生大量视图。(强烈不推荐
  2. 不使用JpaRepository,通过EntityManager执行sql语句,并手动映射类字段。

这种方式用的比较多,当查询比较复杂的情况时,例如连表查询,此时注入EntityManager,通过EntityManager执行sql,返回的是List<Object[]>类型,手动转成dto

public class TestEntityManager {
	
	@Autowired
    private EntityManager entityManager;

    public Sku findSkuBySkuCode(String skuCode) {
    	Query query = entityManager.createNativeQuery("select sku_id,name from m_sku where sku_code = '" + skuCode + "'");
    	List<Object[]> resultList = query.getResultList();
    	return resultList.stream().map(item->{
            Sku sku  =new Sku();
            sku.setSkuId(Long.valueOf(item[0].toString()));
            sku.setName(item[2].toString());
            return sku;
        });
    }

这种方法简直是原始人,需要处理Object数组,容易数组下标越界不说,还有大量类型转换的代码,一不小心就空指针。(强烈不推荐

在介绍了以上的缺点后,下面我再介绍解决办法,可以不建Repository;可以不手动做类型转换,能够像Mybatis一样,通过as取别名的方式根据字段名直接映射到类字段上。

通过AliasToEntityMapResultTransformer实现自动映射

我们同样通过EntityManager执行sql,但是返回参数并不使用List<Object[]>去接,我们可以通过指定NativeQuery的ResultTransformer为AliasToEntityMapResultTransformer

AliasToEntityMapResultTransformer

该映射策略支持查询结果返回List<Map<String, Object>>结构,Map中的key是字段别名,value是查询的结果。

/**
 *ResultTransformer实现为每个“行”构建一个映射,由每个别名值组成,其中别名是映射键。
 * 由于该变压器是无状态的,因此所有实例都将被视为平等。因此,出于优化目的,我们将其限制为单个单例    *instance 
 *  * @author Gavin King
 * @author Steve Ebersole
 */
public class AliasToEntityMapResultTransformer extends AliasedTupleSubsetResultTransformer {

	public static final AliasToEntityMapResultTransformer INSTANCE = new AliasToEntityMapResultTransformer();

	/**
	 * Disallow instantiation of AliasToEntityMapResultTransformer.
	 */
	private AliasToEntityMapResultTransformer() {
	}

	@Override
	public Object transformTuple(Object[] tuple, String[] aliases) {
		Map result = new HashMap(tuple.length);
		for ( int i=0; i<tuple.length; i++ ) {
			String alias = aliases[i];
			if ( alias!=null ) {
				result.put( alias, tuple[i] );
			}
		}
		return result;
	}

	@Override
	public boolean isTransformedValueATupleElement(String[] aliases, int tupleLength) {
		return false;
	}

	/**
	 * Serialization hook for ensuring singleton uniqueing.
	 *
	 * @return The singleton instance : {@link #INSTANCE}
	 */
	private Object readResolve() {
		return INSTANCE;
	}
}

代码实现

基于以上思想,对查询方法进行了封装,提供

  • listForDto(String sql, Class<T> clazz)
  • listForDto(String sql, Class<T> clazz, Consumer<NativeQuery<?>> queryConsumer)

    与上面的区别是可以对query对象进行中间操作,例如
    query -> query.setParameter(“storeSet”, storeSet)
    添加查询参数

  • listForEntity(String sql, Class clazz)
  • getOneForEntity(String sql, Class clazz)
/**
 * 通用Dao
 *
 * @author wangmeng
 * @since 2024/2/24
 */
@Repository
@AllArgsConstructor
public class CommonDao {

    private final EntityManager entityManager;


    // 单列结果返回类型,特殊处理
    private final static List<Class<?>> SINGLE_CLOUMN_LIST = Arrays.asList(String.class, Integer.class, BigDecimal.class, Long.class);

    // 对每种单列返回值的处理策略
    private final static Map<Class<?>, Function<String, ?>> converters = new HashMap<>();

    static {
        converters.put(String.class, String::valueOf);
        converters.put(Integer.class, Integer::valueOf);
        converters.put(BigDecimal.class, BigDecimal::new);
        converters.put(Long.class, Long::valueOf);
    }


    /**
     * 查询结果转换成clazz
     * @param sql sql
     * @param clazz 映射实体类
     * @return 查询结果
     * @param <T>
     */
    public <T> List<T> listForDto(String sql, Class<T> clazz) {
        return listForDto(sql, clazz, null);
    }

    /**
     * 查询结果转换成clazz
     * @param sql sql
     * @param clazz 映射实体类
     * @param queryConsumer 对query中间处理,例如query -> query.setParameter("storeSet", storeSet)
     * @return 查询结果
     * @param <T>
     */
    public <T> List<T> listForDto(String sql, Class<T> clazz, Consumer<NativeQuery<?>> queryConsumer) {
        NativeQuery nativeQuery = (NativeQuery) entityManager.createNativeQuery(sql);
        if (queryConsumer != null) {
            queryConsumer.accept(nativeQuery);
        }
        nativeQuery.setResultTransformer(AliasToEntityMapResultTransformer.INSTANCE);
        List<Map<String, Object>> list = nativeQuery.getResultList();
        if (CollectionUtils.isEmpty(list)) {
            return new ArrayList<>();
        }
        // 获取类中所有字段名
        Map<String, String> filedNameMap = Arrays.stream(ReflectUtil.getFields(clazz)).collect(Collectors.toMap(field -> field.getName().toUpperCase(), Field::getName));
        // 查询结果返回的别名都是大写,需要将key值替换成类字段名
        List<Map<String, Object>> transferList = new ArrayList<>();
        for (Map<String, Object> obj : list) {
            Map<String, Object> map = new HashMap<>();
            for (String key : obj.keySet()) {
                if (filedNameMap.containsKey(key)) {
                    map.put(filedNameMap.get(key), obj.get(key));
                }
            }
            transferList.add(map);
        }
        // 从map转换成结果集
        return BeanUtil.copyToList(transferList, clazz);
    }


    /**
     * 当clazz是表映射的类时,可以使用该方法
     * @param sql sql
     * @param clazz 实体类 
     * @return
     * @param <R>
     */
    public <R> List<R> listForEntity(String sql, Class<R> clazz) {
        Session session = entityManager.unwrap(Session.class);
        NativeQuery sqlQuery = session.createSQLQuery(sql).addEntity(clazz);
        List<R> list = sqlQuery.list();
        if (CollectionUtils.isEmpty(list)) {
            return new ArrayList<>();
        }
        return list;
    }


    /**
     * 查询单个结果
     * @param sql 
     * @param clazz
     * @return
     * @param <R>
     */
    public <R> R getOneForEntity(String sql, Class<R> clazz) {
        // 单列返回特殊处理
        if (SINGLE_CLOUMN_LIST.contains(clazz)) {
            NativeQuery nativeQuery = (NativeQuery) entityManager.createNativeQuery(sql);
            Object singleResult = nativeQuery.getSingleResult();
            if (singleResult == null) {
                return null;
            }
            return (R) converters.get(clazz).apply(singleResult.toString());
        }
        List<R> list = listForEntity(sql, clazz);
        if (CollectionUtils.isEmpty(list)) {
            return null;
        }
        return list.get(0);
    }

    @Modifying
    @Transactional
    public int update(String sql, Consumer<NativeQuery<?>> queryConsumer) {
        NativeQuery nativeQuery = (NativeQuery) entityManager.createNativeQuery(sql);
        if (queryConsumer != null) {
            queryConsumer.accept(nativeQuery);
        }
        return nativeQuery.executeUpdate();
    }

}

核心处理

核心处理是使用AliasToEntityMapResultTransformer获取到List<Map<>>结构后进行以下步骤

  1. Oracle查询中返回的列名都是大写,因此需要将列名与类字段对应上,需要先将类字段也转成大写
  2. 将Map<String,Object> 对象转换成类对象,这里巧妙借用了hutool工具包的BeanUtil,通过cn.hutool.core.bean.BeanUtil#copyToList(java.util.Collection<?>, java.lang.Class<T>)方法能够直接做这种转换,这个步骤省下了很多自己做类型转换的功夫!
  • 38
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值