前言
公司中有一个老的项目使用了JPA+Hibernate框架,在接触了这个项目之后我的感受就是异常难用(o(╥﹏╥)o),相对于目前主流使用的Mybatis框架,在使用这种框架的项目中查询数据库有以下几种方式:
- 使用
JpaRepository
映射实体类和表。
@Repository
@Transactional(rollbackFor = Exception.class)
public interface OrderRepository extends JpaRepository<Order, Long> {
}
- 使用这个Repository会自动映射出一些方法
find方法
,虽然可以不用手动写方法,但是相较于Mybatis-plus可以自由构建QueryWarp,被Mybatis-plus完全秒杀。
- 当遇到查询返回的结果不是对应一个表实体,这时候就麻烦了,因为DTO是无法映射
JpaRepository
,此时想要使用JpaRepository只能新建一个视图
来映射DTO,后期数据库里可能会产生大量视图。(强烈不推荐
) - 不使用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<>>结构后进行以下步骤
- Oracle查询中返回的列名都是大写,因此需要将列名与类字段对应上,
需要先将类字段也转成大写
- 将Map<String,Object> 对象转换成类对象,这里巧妙借用了
hutool工具包的BeanUtil
,通过cn.hutool.core.bean.BeanUtil#copyToList(java.util.Collection<?>, java.lang.Class<T>)
方法能够直接做这种转换,这个步骤省下了很多自己做类型转换的功夫!