1. 程式人生 > 資料庫 >根據資料庫欄位動態生成excel模版下載,上傳模版獲取資料存入資料庫(poi+反射)

根據資料庫欄位動態生成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條資料,匯入成功。