Excel (SpringBoot + JPA)
SpringBoot+Jpa實現Excel的導入導出(動態Sql、分頁查詢、聯表join)
最近做了一個項目甲方需求中要求數據的導入導出到Excel文件,Excel的複雜表頭和數據格式一直是個頭疼的問題,使用poi或者jexcelapi的話就需要花費大量時間處理表頭以及數據格式問題,但是整個項目的開發時間只有一週,對接和聯調一週,那就只能去找快速的“黑科技”了–EasyExcel,雖然EasyExcel只是對poi的再封裝,但是EasyExcel中有一個模板寫入的功能,是可以很好的解決複雜表頭和一些基本的數據格式問題的。抽了點時間整理了一個demo,也包含一些對Jpa的進階用法(動態Sql分頁查詢、聯表join等)。
開發環境
環境: windows 10
編譯器: IDEA 2018
數據庫: MySQL 5.6
JDK: jdk1.8.0_92
Maven: 3.6
項目結構樹
│ .gitignore
│ LICENSE
│ pom.xml --項目pom文件
│ README.md --readme.md文件
├─github
│ └─image --用於存放readme.md中鏈接的圖片
├─other
│ Excel.sql --數據庫建表Sql語句
│ Excel數據導入測試[PostMan].postman_collection.json --信息導入接口PostMan導出的json文件
│ 個人信息導入.xlsx --個人信息導入測試數據
│ 畢業信息導入.xlsx --畢業信息導入測試數據
│ 目錄結構樹.txt
├─src
│ ├─main
│ │ ├─java
│ │ │ └─com
│ │ │ └─cyan
│ │ │ └─excel
│ │ │ │ ExcelApplication.java --SpringBoot啓動類
│ │ │ │
│ │ │ ├─controller
│ │ │ │ ExcelController.java --Controller層
│ │ │ │
│ │ │ ├─entity
│ │ │ │ │ GraduateInfo.java --畢業信息實體
│ │ │ │ │ UserInfo.java --用戶信息實體
│ │ │ │ │
│ │ │ │ ├─joint
│ │ │ │ │ GraduateUserJoint.java --畢業和用戶聯表Join實體
│ │ │ │ │
│ │ │ │ └─model
│ │ │ │ DetailModel.java --詳細信息Excel文件對應模型
│ │ │ │ GraduateModel.java --畢業信息Excel文件對應模型
│ │ │ │ UserModel.java --用戶信息Excel文件對應模型
│ │ │ │
│ │ │ ├─enums
│ │ │ │ ExcelFileEnum.java --Excel文件類型枚舉
│ │ │ │ SexTypeEnum.java --性別類型枚舉
│ │ │ │
│ │ │ ├─exception
│ │ │ │ │ ExcelException.java --全局異常
│ │ │ │ │
│ │ │ │ └─handler
│ │ │ │ ExcelExceptionHandler.java --全局異常捕獲處理
│ │ │ │
│ │ │ ├─listener
│ │ │ │ ExcelListener.java --excel數據處理
│ │ │ │
│ │ │ ├─repository
│ │ │ │ │ GraduateInfoRepository.java --畢業信息DAO層(jpa)
│ │ │ │ │ GraduateUserJointRepository.java --用戶和畢業聯表joinDAO層(jpa)
│ │ │ │ │ UserInfoRepository.java --用戶信息DAO層(jpa)
│ │ │ │ │
│ │ │ │ └─Impl
│ │ │ │ GraduateUserJointRepositoryImpl.java--用戶和畢業聯表joinDAO層(代碼實現分頁/動態Sql)
│ │ │ │
│ │ │ ├─result
│ │ │ │ ResponseResult.java --返回結果體
│ │ │ │ ResponseResultEnum.java --返回結果異常枚舉
│ │ │ │ ResponseResultUtils.java --返回結果工具類
│ │ │ │
│ │ │ ├─service
│ │ │ │ │ DetailService.java --詳細信息的Service層
│ │ │ │ │ GraduateService.java --畢業信息的Service層
│ │ │ │ │ UserService.java --用戶信息的Service層
│ │ │ │ │
│ │ │ │ ├─handler
│ │ │ │ │ ImportHandler.java --Excel數據導入處理
│ │ │ │ │
│ │ │ │ └─Impl
│ │ │ │ DetailServiceImpl.java --詳細信息的Service實現
│ │ │ │ GraduateServiceImpl.java --畢業信息的Service實現
│ │ │ │ UserServiceImpl.java --用戶信息的Service實現
│ │ │ │
│ │ │ ├─utils
│ │ │ │ ConvertUtils.java --數據轉換工具類
│ │ │ │ CopyUtils.java --Bean拷貝工具類(不拷貝null值)
│ │ │ │ ExcelUtils.java --Excel相關工具類
│ │ │ │ FileUtils.java --文件相關工具類
│ │ │ │ PathUtils.java --路徑相關工具類
│ │ │ │ StreamUtils.java --流處理工具類
│ │ │ │
│ │ │ └─vo
│ │ │ ImportResultVO.java --Excel導入結果VO層
│ │ │
│ │ └─resources
│ │ │ application.yml --項目配置文件
│ │ │
│ │ ├─static
│ │ │ └─model --Excel導入導出模板
│ │ │ 個人信息模板.xlsx
│ │ │ 分類信息模板.xlsx
│ │ │ 畢業信息模板.xlsx
│ │ │ 詳細信息模板.xlsx
Excel模板
1、個人信息模板
2、分類信息模板
3、畢業信息模板
4、詳細信息模板
項目測試
1、導入數據測試
數據導入時,接收一個formData格式的excel文件對象,參數爲excel,後臺接收MultipartFile類型,暫時只做了單文件導出,有多文件導入需求的可以去度娘查詢。
Excel的序號一列是不會導入數據庫的,UserInfo使用UserId身份證作爲主鍵, GraduateInfo使用自增主鍵,數據庫裏兩表不設外鍵關係,只在業務層控制外鍵關係。設計數據庫時只是爲了做兩表join的功能,把一對一的數據拆分到倆個表裏了,導致GraduateInfo不控制的話數據會出現大量重複,因此在Service>handler下做了邏輯上的控制,因此GraduateInfo表參考意義不大。
/** 數據導入信息 返回結果體data */
public class ImportResultVO<T> {
/** 成功數量 */
private Integer success = 0;
/** 失敗數量 */
private Integer failure = 0;
/** 重複數量 */
private Integer repeat = 0;
/** 失敗數據 */
private List<T> failureList;
/** 重複數據 */
private List<T> repeatList;
//...
}
1、個人信息導入測試
模擬數據:
PostMan測試:
數據庫驗證:
2、畢業信息導入測試
模擬數據:
PostMan測試:
數據庫驗證:
注:數據庫之前沒做覆蓋,就存在很多重複數據,手動刪除了一些,又重新導入,所以id不連續
2、導出測試
數據導出測試沒有返回結果體,直接返回Response,會將生成好的excel文件直接以字節流的形式寫入到Response,通過設置Response的Header屬性讓瀏覽器自行解析文件。
關於請求頭相關知識請自行度娘
{
"Content-Disposition":"attachment;filename=文件名.xlsx",
"Content-Type": "application/octet-stream"
}
1、個人信息導出測試
2、畢業信息導出測試
3、分類信息導出測試
按性別導出主要是爲了滿足,不同的數據分別寫入到同一個excel的不同sheet中的這個需求。
按性別分類導出(男):
按性別分類導出(女):
4、詳細信息導出測試
詳細信息導出用到了聯表Join,用原生SQL查詢的話這其實不算個問題,我這裏最想展示的是當用Jpa時,分頁+動態sql+聯表Join的較爲複雜的實現方式。
public class GraduateUserJointRepositoryImpl {
@PersistenceContext
private EntityManager entityManager;
/**
* 動態SQL查詢數據分頁
* @param whereSql
* @param pageable
* @return
*/
@SuppressWarnings("unchecked")
public Page<GraduateUserJoint> findAllByGraduateUserJointWithWhereSql(String whereSql, Pageable pageable) {
/** 原生SQL語句結合外部動態構建的whereSql實現動態SQL拼接(在這裏拼也不是不可以) */
/** 聯表Join時根據需求確定 left/right/inner */
/** whereSql 中最少要包含 1=1 類似的邏輯表達式 防止SQL報錯 */
String dataSql = "select * from graduate_info g inner join user_info u on u.user_id = g.user_id where " + whereSql;
String countSql = "select count(1) from graduate_info g inner join user_info u on u.user_id = g.user_id where " + whereSql;
Query dataQuery = entityManager.createNativeQuery(dataSql, GraduateUserJoint.class);
Query countQuery = entityManager.createNativeQuery(countSql);
/** 分頁供能實現 */
dataQuery.setFirstResult((int) pageable.getOffset());
dataQuery.setMaxResults(pageable.getPageSize());
/** 分頁總數統計 */
BigInteger count = (BigInteger) countQuery.getSingleResult();
long total = count.longValue();
/** 分頁數據 */
List<GraduateUserJoint> graduateUserJointList = total > pageable.getOffset() ? dataQuery.getResultList() : Collections.<GraduateUserJoint> emptyList();
return new PageImpl<>(graduateUserJointList, pageable, total);
}
//...
}