根據資料庫欄位動態生成excel模版下載,上傳模版獲取資料存入資料庫(poi+反射)
環境:mysql5.7.28+java8+Spring boot 2.2.4 +mybatis-plus3.10
動態:根據需求,使用者可以選擇對應的欄位生成excle模版 下載
poi+反射:poi是excel的第三方jar,反射的作用是給表實體物件屬性賦值,方便入庫操作。
現在很多的應用都有批量匯入的功能,批量匯入用的最多的也是excel。我們實際的專案中也用了很多這方面的功能,所以博主系統的CV了一下這方面的程式碼,下面分步驟進行該功能的實現。此方法的優點:不限於模版欄位的排列順序,避免過多的重複set程式碼。動態的生成模版資訊。
注意:資料庫實體類屬性變數名,要嚴格按照駝峰的模式命名,方便資料的讀取,反射的賦值。
如果資料量過大,建議採用多執行緒的方式匯入資料,資料的分割根據實際情況,本文采用的是單執行緒方式執行。
1:依賴的jar包
<dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>3.17</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>3.17</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.4</version> </dependency>
2:生成對應資料庫的模版
生成模版的前提是查詢資料庫有哪些欄位,有了欄位的資訊,就可以根據java se中的流知識,生成對應的模版檔案,下面的這個是查詢表結構的所有資訊。因為是要生成欄位對應的模版,所以我們把sql修改一下即可
select * from information_schema.COLUMNS where table_name = '表名'
查詢欄位的sql如下:
select COLUMN_NAME from information_schema.COLUMNS where table_name = '表名'
同樣的這個查詢也可以用mybatis框架進行對映,返回的是List<String> 型別,對應的mapper層次如下。
List<String> queryColumn();
到這步,我們的欄位資訊就有了,也就是excel的表頭資訊有了,下面就是根據表頭資訊生成對應的模版了。
生成程式碼如下圖:需要說明的是傳入的引數:資料庫的欄位資訊,模版的名稱(可任意取),生成模版的路徑所在地
public static boolean createModel(List<String> list,String modelName,String modelPath) {
boolean newFile = false;
//建立excel工作簿
HSSFWorkbook workbook = new HSSFWorkbook();
//建立工作表sheet
HSSFSheet sheet = workbook.createSheet();
//建立第一行
HSSFRow row = sheet.createRow(0);
HSSFCell cell;
//設定樣式
CellStyle style = workbook.createCellStyle();
style.setFillForegroundColor(IndexedColors.AQUA.getIndex());
//插入第一行資料的表頭
for (int i = 0; i < list.size(); i++) {
cell = row.createCell(i);
cell.setCellStyle(style);
cell.setCellValue(list.get(i));
}
//建立excel檔案
File file = new File(modelPath + File.separator + modelName);
try {
//刪除該資料夾下原來的模版檔案
deleteDir(new File(modelPath + File.separator));
//判斷對應的資料夾是否有,無則新建
File myPath = new File(modelPath);
if (!myPath.exists()) {
myPath.mkdir();
}
//建立新的模版檔案
newFile = file.createNewFile();
//將excel寫入
FileOutputStream stream = FileUtils.openOutputStream(file);
workbook.write(stream);
stream.close();
} catch (IOException e) {
e.printStackTrace();
}
return newFile;
}
下面的是刪除原來模版檔案的工具方法
private static boolean deleteDir(File dir) {
if (dir.isDirectory()) {
String[] children = dir.list();
if (children != null)
//遞迴刪除目錄中的子目錄下
for (String child : children) {
boolean success = deleteDir(new File(dir,child));
if (!success) {
return false;
}
}
}
// 目錄此時為空,可以刪除
return dir.delete();
}
檔案已經生成了,剩下的就是下載檔案。這裡需要說明一下,如何動態的生成模版
動態的生成模版就是動態的獲取資料庫的欄位,只需根據使用者選取的資料,在sql的查詢,或者程式碼中做修改。如下的not in就是排除這些不需要的欄位,當然你也可以選擇其他方式進行過濾,程式中過濾是最好的選擇。
<select id="queryColumn" resultType="string">
select COLUMN_NAME from information_schema.COLUMNS where table_name = 'cpa_account_list'
and column_name not in ('account_type_id','account_status','create_time','id','out_time','use_time','update_time')
</select>
生成模版後,通過瀏覽器訪問即可下載。下載的程式碼如下:傳入檔案的生成路徑,檔案的名字,下載生成新的檔名(可任意)
public static ResponseEntity<InputStreamResource> download(String filePath,String fileName,String newName) {
String path;
ResponseEntity<InputStreamResource> response = null;
try {
path = filePath + separator + fileName;
log.info("下載的路徑為-->[{}]",path);
File file = new File(path);
InputStream inputStream = new FileInputStream(file);
HttpHeaders headers = new HttpHeaders();
headers.add("Cache-Control","no-cache,no-store,must-revalidate");
headers.add("Content-Disposition","attachment; filename="
+ new String(newName.getBytes(StandardCharsets.UTF_8)) + ".xlsx");
headers.add("Pragma","no-cache");
headers.add("Expires","0");
response = ResponseEntity.ok().headers(headers)
.contentType(MediaType.parseMediaType("application/octet-stream"))
.body(new InputStreamResource(inputStream));
} catch (FileNotFoundException e1) {
log.error("找不到指定的檔案",e1);
}
return response;
}
最後就是在web層次呼叫上述方法,即可完成下載,博主的controlle程式碼如下,僅供參考
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.ResponseEntity;
@GetMapping(value = "/downloadModel",produces = "application/json;charset=UTF-8")
@ApiOperation(value = "賬號資訊的模板下載",produces = "application/json;charset=UTF-8")
public Object downloadAccountModel() {
//檔名
String modelFileName = "accountList.xlsx";
//下載展示的檔名
ResponseEntity<InputStreamResource> response = null;
try {
List<String> columns = cpaAccountListService.queryColumn();
// 傳人資料庫的字端,建立資料的模版
boolean model = CpaDownloadFileUtil.createModel(columns,modelFileName,modelPath);
if (model) response = CpaDownloadFileUtil.download(modelPath,"AccountListModel");
} catch (Exception e) {
e.printStackTrace();
log.error("下載模板失敗");
}
return response;
}
採用的是swagger測試下載的結果如下
3: 將模版的資料匯入到資料庫中
批量匯入模版中的資料,關鍵點就是如何將資料準確的讀取,生成java物件,放入集合中。其次是利用mybatis-plus的批量匯入資料即可。
下面為讀取excel的方法,只讀取sheet0的資料。讀取的資料為一行行陣列。將陣列放入到集合中返回。具體的解釋,程式碼註釋都有。
需要注意處理單元格資料為空的方法。
/**
* 解析excel
* auth psy
* @param inp excel InputStream.
* @return 對應資料列表
*/
public static List<List<Object>> readExcel(InputStream inp) {
Workbook wb = null;
try {
wb = WorkbookFactory.create(inp);
// 獲取地0個sheet的資料
Sheet sheet = wb.getSheetAt(0);
List<List<Object>> excels = new ArrayList<>();
// 遍歷每一行資料
int cellsNumber = 0;
for (int i = 0; i <= sheet.getLastRowNum(); i++) {
if (i == 0) {
// 獲取每一行總共的列數
cellsNumber = sheet.getRow(i).getPhysicalNumberOfCells();
}
List<Object> excelRows = new ArrayList<>();
// 遍歷每一行中的每一列中的
for (int j = 0; j < cellsNumber; j++) {
// i和j組成二維座標可以定位到對應到單元格內
Cell cell = sheet.getRow(i).getCell(j);
if (i >= 1) {
// 如果單元格到內容為空就設定為"null"代表的是無資料
if (cell == null) {
excelRows.add("null");
} else {
// 不是空值的單元格資料
excelRows.add(getValue(cell));
}
} else {
// 該資料為表格的表頭資訊,單獨儲存與集合的首位
excelRows.add(getValue(cell));
}
}
excels.add(excelRows);
}
return excels;
} catch (Exception e) {
log.error("匯入excel錯誤 : " + e.getMessage());
return null;
} finally {
try {
if (wb != null) {
wb.close();
}
if (inp != null) {
inp.close();
}
} catch (Exception e) {
log.error("匯入excel關流錯誤 : " + e.getMessage());
}
}
}
由於poi的版本不同,獲取excel資料的格式方法也不同,本文所使用的工具方法為下,傳入的是資料的單元格的物件。
public static String getValue(Cell cell) {
String birthdayVal = null;
switch (cell.getCellTypeEnum()) {
case STRING:
birthdayVal = cell.getRichStringCellValue().getString();
break;
case NUMERIC:
if ("General".equals(cell.getCellStyle().getDataFormatString())) {
// 此處為double型別的,轉成對應的String型別資料
birthdayVal = Integer.toString(new Double(cell.getNumericCellValue()).intValue());
} else if ("m/d/yy".equals(cell.getCellStyle().getDataFormatString())) {
birthdayVal = DateToStr(cell.getDateCellValue());
} else {
birthdayVal = DateToStr(HSSFDateUtil.getJavaDate(cell.getNumericCellValue()));
}
break;
}
return birthdayVal;
}
public static String DateToStr(Date date) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return format.format(date);
}
模版資料中,第一行資料,ip_address為空 ,我讀取的時候,設定為“null”,如下結果圖顯示,同時也發現,我讀取的資料是一個數組(list)這個時候資料的讀取已經完成了。
以上都是excel資料的讀取,這一步之後,我們如何將讀取的資料,根據表頭的資訊賦值到對應的資料庫中呢?這就是關鍵的地方,也是模版的存在的原因。
首先明確兩點內容:1:模版的表頭資訊,就是資料庫的欄位
2:資料庫的欄位與實體屬性的對應是駝峰命名的方式(user_id--->userId)。
知道以上兩點,問題就變成了如何將資料庫的值,賦值到實體類的屬性。思路:首先將資料庫欄位(表頭)轉成實體的屬性變數名,然後通過每一行獲取的資料,利用反射的原理,通過類屬性名,將對應的單元格資訊賦值到物件的屬性中。最後儲存到集合中,如此迴圈,便可以將excel對應的表資料,逐行賦值到每一個物件中了。最後就是批量入庫操作。
下面是程式碼的實現:
web層面
@PostMapping(value = "/addDataByModel",produces = "application/json;charset=UTF-8")
@ApiOperation(value = "通過模版匯入對應的資料資料",produces = "application/json;charset=UTF-8")
public Object addDataByModel(MultipartFile file) {
//檔名
try {
List<CpaDataList> cpaDataLists = null;
InputStream inputStream = file.getInputStream();
List<List<Object>> lists = CpaExcelUtil.readExcel(inputStream);
if (lists != null) {
cpaDataLists = CpaImportDbUtil.getCpaDataList(lists);
}
if (null != cpaDataLists) {
boolean b = cpaDataListService.saveBatch(cpaDataLists,cpaDataLists.size());
if (b) {
log.info("匯入資料的的個數為--->[{}]",cpaDataLists.size());
return ReturnResult.success(ReturnMsg.SUCCESS.getCode(),ReturnMsg.SUCCESS.getMsg(),cpaDataLists.size());
}
}
} catch (Exception e) {
e.printStackTrace();
log.error("通過模版匯入資料資料出現異常!");
}
return ReturnResult.error(ReturnMsg.ERROR.getCode(),ReturnMsg.ERROR.getMsg());
}
將讀取的excel資料變成對應的集合,集合中是實體物件與資料庫欄位的對應
public static List<CpaAccountList> getCpaAccountList(List<List<Object>> excels) throws Exception {
List<CpaAccountList> cpaAccountLists = new ArrayList<>();
CpaAccountList cpaAccount;
// 第一行代表的是該表格的資料庫欄位,需要單獨拿出來進行處理
List<Object> cellList = excels.get(0);
// 將首位資料移除
excels.remove(0);
String filedName;
String value;
// 遍歷每一行的資料
for (List<Object> excel : excels) {
// 遍歷每一行的中的每一列資料
cpaAccount = new CpaAccountList();
for (int i = 0; i < cellList.size(); i++) {
filedName = cellList.get(i).toString();
value = excel.get(i).toString();
if ("null".equals(value)) {
continue;
}
// 通過反射的方式,給屬性值set value
setValue(cpaAccount,cpaAccount.getClass(),filedName,CpaAccountList.class.getDeclaredField(fieldToProperty(filedName)).getType(),value);
}
cpaAccount.setCreateTime(LocalDateTime.now());
cpaAccount.setAccountStatus(1);
cpaAccountLists.add(cpaAccount);
}
return cpaAccountLists;
}
/**
* @return
* @author PSY
* @date 2020/2/25 15:21
* @介面描述: 將資料庫欄位轉換成類的屬性
* @parmes
*/
public static String fieldToProperty(String field) {
if (null == field) {
return "";
}
char[] chars = field.toCharArray();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < chars.length; i++) {
char c = chars[i];
if (c == '_') {
int j = i + 1;
if (j < chars.length) {
sb.append(StringUtils.upperCase(CharUtils.toString(chars[j])));
i++;
}
} else {
sb.append(c);
}
}
return sb.toString();
}
/**
* @return
* @author PSY
* @date 2020/2/25 15:21
* @介面描述: 通過屬性,獲取對應的set方法,並且設定值
* @parmes
*/
public static void setValue(Object obj,Class<?> clazz,String filedName,Class<?> typeClass,Object value) {
filedName = fieldToProperty(filedName);
String methodName = "set" + filedName.substring(0,1).toUpperCase() + filedName.substring(1);
try {
Method method = clazz.getDeclaredMethod(methodName,typeClass);
method.invoke(obj,getClassTypeValue(typeClass,value));
} catch (Exception ex) {
ex.printStackTrace();
}
}
private static Object getClassTypeValue(Class<?> typeClass,Object value) {
// 對於String型別的有個強行轉換成int型別的操作。
if (typeClass == LocalDateTime.class && null != value) {
DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
return LocalDateTime.parse(value.toString(),df);
}
if (typeClass == LocalDateTime.class) {
return null;
}
if (typeClass == Integer.class) {
value = Integer.valueOf(value.toString());
return value;
} else if (typeClass == short.class) {
if (null == value) {
return 0;
}
return value;
} else if (typeClass == byte.class) {
if (null == value) {
return 0;
}
return value;
} else if (typeClass == double.class) {
if (null == value) {
return 0;
}
return value;
} else if (typeClass == long.class) {
if (null == value) {
return 0;
}
return value;
} else if (typeClass == String.class) {
if (null == value) {
return "";
}
return value;
} else if (typeClass == boolean.class) {
if (null == value) {
return true;
}
return value;
} else if (typeClass == BigDecimal.class) {
if (null == value) {
return new BigDecimal(0);
}
return new BigDecimal(value + "");
} else {
return typeClass.cast(value);
}
}
以上程式碼關鍵的就是反射的應用,一一對應實體屬性。
資料庫中含有500條資料,匯入成功。