填坑:讓JPA使用更順手

更多最新文章歡迎大家訪問我的個人博客😄:豆腐別館

用習慣了mybatis及mybatisplus,剛開始拿起spring-data-jpa,其無法局部更新、原生sql查詢返回非JavaBean的List<Map<String, Object>>、分頁無法返回自定義對象等等的問題着實差點沒讓我拔出我的30米大砍刀。不過咱吐槽歸吐槽,遇到問題,想辦法一一去解決問題即可。

一、讓JPA支持局部更新

jpa中的新增與更新都是調用的save()方法,以是否攜帶id做區分,無奈的是它對局部變量更新並未做何處理,依舊需要我們自己動手。

  • 解決方法:
    BeanUtils複製對象,BeanUtils中的構造方法屬性中可以通過傳入更新時忽略的屬性值來實現選擇性複製原對象的字段。更新部分字段時,我們僅需要傳入複製後的字段即可。

  • 解析和實現:
    (1)查詢出待更新對象的原有信息
    (2)通過傳入的更新的象去複製產生一個新對象,其中新對象中爲null的字段不需要更新。
    (3)執行更新操作,操作對象時步驟2得出的複製對象。

  • 代碼如下:

@Override
@Transactional
public Long update(TestCommand command) {
    Optional<TestEntity> optional = testRepository.findById(command.getId());
    if (!optional.isPresent()) {
        return 0L;
    }
    Appointment target = optional.get();
    // 複製對象
    BeanUtils.copyProperties(command, target, BeanUtil.getNullProperty(command));
    
    target.setOther(command.getOther());
    TestEntity result = testRepository.save(target);
    if (result == null) {
        return 0L;
    }
    return result.getId();
}

代碼中的getNullProperty作爲通用工具類,代碼如下:

/**
 * 獲取空屬性
 */
public static String[] getNullProperty(Object source) {
    final BeanWrapper src = new BeanWrapperImpl(source);
    PropertyDescriptor[] pds = src.getPropertyDescriptors();

    Set<String> emptyNames = new HashSet<String>();
    for (PropertyDescriptor pd : pds) {
        Object srcValue = src.getPropertyValue(pd.getName());
        if (srcValue == null) {
            emptyNames.add(pd.getName());
        }
    }
    String[] result = new String[emptyNames.size()];
    return emptyNames.toArray(result);
}

二、讓JPA原生SQL查詢返回ListBean

jpa使用@Query註解來做原生sql查詢,一般是這樣的畫風:

@Query(value = "SELECT id, name FROM xxx WHERE type = :type", nativeQuery = true)
List<Map<String, Object>> findTest(@Param("type") String type);

其返回值List<Map<String, Object>>怎麼看怎麼不舒服,那麼如何將之轉換爲我們想要的List<JavaBean>呢?先直接看看經封裝轉換後的返回效果:

// 正常原生SQL查詢
List<Map<String, Object>> mapList = testRepository.findTest("testType");
// 工具類轉換
List<TestResult> beanList = BeanUtil.mapToBean(mapList, TestResult.class);

工具類代碼:

/**
  * 根據List<Map<String, Object>>數據轉換爲List<JavaBean>數據
  */
 public static <T> List<T> mapToBean(List<Map<String, Object>> datas, Class<T> beanClass) {
     // 返回數據集合
     List<T> list = null;
     // 對象字段名稱
     String fieldName = "";
     // 對象方法名稱
     String methodname = "";
     // 對象方法需要賦的值
     Object methodsetvalue = "";
     try {
         list = new ArrayList<T>();
         // 得到對象所有字段
         Field fields[] = beanClass.getDeclaredFields();
         // 遍歷數據
         for (Map<String, Object> mapdata : datas) {
             // 創建一個泛型類型實例
             T t = beanClass.newInstance();
             // 遍歷所有字段,對應配置好的字段並賦值
             for (Field field : fields) {
                 // 獲取字段名稱
                 fieldName = field.getName();
                 if ("serialVersionUID".equals(fieldName)) {
                     continue;
                 }
                 // 拼接set方法
                 methodname = "set" + StringUtils.capitalize(fieldName);
                 // 獲取data裏的對應值
                 methodsetvalue = mapdata.get(fieldName);
                 // 賦值給字段
                 Method m = beanClass.getDeclaredMethod(methodname, field.getType());
                 // Integer
                 if (field.getType() == Integer.class) {
                     if (methodsetvalue == null) {
                     } else {
                         m.invoke(t, Integer.parseInt(methodsetvalue.toString()));
                     }
                 }
                 // Long
                 else if (field.getType() == Long.class) {
                     if (methodsetvalue == null) {
                     } else {
                         m.invoke(t, Long.parseLong(methodsetvalue.toString()));
                     }
                 }
                 // Boolean
                 else if (field.getType() == Boolean.class) {
                     if (methodsetvalue == null) {
                         m.invoke(t, Boolean.FALSE);
                     } else {
                         m.invoke(t, Boolean.parseBoolean(methodsetvalue.toString()));
                     }
                 }
                 // Byte
                 else if (field.getType() == Byte.class) {
                     if (methodsetvalue == null) {
                         m.invoke(t, null);
                     } else {
                         m.invoke(t, Byte.parseByte(methodsetvalue.toString()));
                     }
                 }
                 // Short
                 else if (field.getType() == Short.class) {
                     if (methodsetvalue == null) {
                         m.invoke(t, null);
                     } else {
                         m.invoke(t, Short.parseShort(methodsetvalue.toString()));
                     }
                 }
                 // Float
                 else if (field.getType() == Float.class) {
                     if (methodsetvalue == null) {
                         m.invoke(t, null);
                     } else {
                         m.invoke(t, Float.parseFloat(methodsetvalue.toString()));
                     }
                 }
                 // Double
                 else if (field.getType() == Double.class) {
                     if (methodsetvalue == null) {
                         m.invoke(t, null);
                     } else {
                         m.invoke(t, Double.parseDouble(methodsetvalue.toString()));
                     }
                 } else {
                     m.invoke(t, methodsetvalue);
                 }

             }
             // 存入返回列表
             list.add(t);
         }
     } catch (InstantiationException e) {
         try {
             throw new Exception("創建beanClass實例異常", e);
         } catch (Exception ex) {
             ex.printStackTrace();
         }
     } catch (IllegalAccessException e) {
         try {
             throw new Exception("創建beanClass實例異常", e);
         } catch (Exception ex) {
             ex.printStackTrace();
         }
     } catch (SecurityException e) {
         try {
             throw new Exception("獲取[" + fieldName + "] getter setter 方法異常", e);
         } catch (Exception ex) {
             ex.printStackTrace();
         }
     } catch (NoSuchMethodException e) {
         try {
             throw new Exception("獲取[" + fieldName + "] getter setter 方法異常", e);
         } catch (Exception ex) {
             ex.printStackTrace();
         }
     } catch (IllegalArgumentException e) {
         try {
             throw new Exception("[" + methodname + "] 方法賦值異常", e);
         } catch (Exception ex) {
             ex.printStackTrace();
         }
     } catch (InvocationTargetException e) {
         try {
             throw new Exception("[" + methodname + "] 方法賦值異常", e);
         } catch (Exception ex) {
             ex.printStackTrace();
         }
     }
     // 返回
     return list;
 }

三、讓JPA分頁查詢返回自定義對象

JPA分頁查詢的方式有多種,以最常見的Page<TestEntity> page = testRepository.findAll(specification, pageable);爲例,其返回值的泛型即爲實體。如果想返回自定對象怎麼辦?不好意思,在不寫SQL的情況下JPA目前並沒有相應的支持,只能自己去動手豐衣足食。以Jave8的語法爲例,我們可以這樣處理:

  1. 首先我們需要自己再封裝一個Page對象,用來取代原生返回的Page對象
package com.xfktech.nurse.appointment.nursing.common.po;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;

import java.util.List;

@Getter
@Setter
@NoArgsConstructor
public class PageResponse<T> {

    /**
     * 頁碼
     */
    private int pageNum;

    /**
     * 頁數
     */
    private int pageSize;

    /**
     * 總頁數
     */
    private int totalPage;

    /**
     * 總條數
     */
    private long totalSize;

    /**
     * 是否最後一頁
     */
    private boolean last;

    /**
     * 數據列表
     */
    private List<T> content;

    public static <T> PageResponse<T> of(Page<T> page) {
        return new PageResponse<>((PageImpl<T>) page);
    }

    private PageResponse(PageImpl<T> page) {
        this.setPageNum(page.getPageable().getPageNumber() + 1);
        this.setPageSize(page.getPageable().getPageSize());
        this.setTotalPage(page.getTotalPages());
        this.setTotalSize(page.getTotalElements());
        this.setLast(page.isLast());
        this.setContent(page.getContent());
    }
}

  1. 爲接口返回值新建vo轉換類,如:
public class TestGenerator {
	// TestResult即我們需返回的自定義對象
    public static TestResult test(TestEntity entity) {
    	/**
    	 * 此處與以下代碼等同:
    	 * TestResult result = new TestResult();
    	 * result.setId(entity.getId());
    	 * ...
    	 * return result;
    	 */
        return TestResult.builder()
                .id(entity.getId())
                .name(entity.getCreateTime())
                .build();
    }
}
  1. 最後,僅需要在如常調用原生分頁Page<TestEntity> page = testRepository.findAll(specification, pageable);之後,將page做以下轉換即可:
PageResponse<TestResult> result = PageResponse.of(page.map(TestGenerator::test));

如需往test()方法額外傳參,那麼也可以選擇這樣寫:

PageResponse<TestResult> result = PageResponse.of(page.map(obj -> AlarmGenerator.common(test, "xxx")));

完整分頁示例代碼如下:

@Override
@Transactional(readOnly = true)
public PageResponse<TestResult> list(TestQuery query) {
    Pageable pageable = PageRequest.of(query.getPageNum(), query.getPageSize());
    Specification<TestEntity> specification = (Specification<TestEntity>) (root, criteriaQuery, criteriaBuilder) -> {
        List<Predicate> list = new ArrayList<>();
        // 未刪除
        list.add(criteriaBuilder.equal(root.get("isDelete"), false));
        // 排序
        criteriaQuery.orderBy(criteriaBuilder.desc(root.get("createTime")));
        return criteriaBuilder.and(list.toArray(new Predicate[list.size()]));
    };
    Page<TestEntity> page = testRepository.findAll(specification, pageable);
    // 轉vo
    PageResponse<TestResult> result = PageResponse.of(page.map(TestGenerator :: test));
    
    return result;
}

The end.

參考資料:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章