清单 3. 获取单元格的值
private static Object getCellValue(Cell cell) { Object obj = null; if(cell == null) return null; switch (cell.getCellType()) { case Cell.CELL_TYPE_STRING: obj = cell.getRichStringCellValue().getString().trim(); break; case Cell.CELL_TYPE_NUMERIC: if (DateUtil.isCellDateFormatted(cell)) { obj = cell.getDateCellValue(); } else { obj = cell.getNumericCellValue(); } break; case Cell.CELL_TYPE_BOOLEAN: obj = cell.getBooleanCellValue(); break; case Cell.CELL_TYPE_FORMULA: obj = cell.getCellFormula(); break; default: } return obj; } |
对于合并的单元格,应用上面的 API,除了左上角第一个单元格有值外,其它已经合并起来的单元格返回值均是 null 。 HSSF 的解析 API 中对此有一个类叫 CellRangeAddress,该类用以记录 Excel 表中被合并(Merged)的一个区域。在程序中,我们只关心给定区域内的合并单元块,这样可以极大的提高性能,如清单 4 所示。
在清单 5 中,我们给出了如何获取某一行内指定列的组合值。
清单 4. 获取指定合并区域
public static List<CellRangeAddress> getCellRawRangeAddresses(HSSFSheet sheet1, int startIndex, int endIndex, short[] indexes){ //indexes, excel column indexes List<CellRangeAddress> resultList = new ArrayList<CellRangeAddress>(); for (int i = 0; i < sheet1.getNumMergedRegions(); i++) { CellRangeAddress cellRA = sheet1.getMergedRegion(i); int firstRow = cellRA.getFirstRow(); int lastRow =cellRA.getLastRow(); int firstCol = cellRA.getFirstColumn(); int lastCol = cellRA.getLastColumn(); if(lastRow >= startIndex && firstRow <= endIndex){ if(lastCol>= getMin(indexes) && firstCol <= getMax(indexes)) resultList.add(cellRA); } } return resultList; } |
清单 5. 获取给定行的组合值
public static String getCominbedValue(HSSFSheet sheet1, Row row, short[] indexes , List<CellRangeAddress> limitedCellRAs){ StringBuffer resultBf = new StringBuffer(); // 用以存储最终结果 Map<CellPosition, CellRangeAddress> theMap = new LinkedHashMap<CellPosition, CellRangeAddress>(); // 记录被合并的单元格所在的合并区域 int rowIndex = row.getRowNum(); Iterator<CellRangeAddress> itcra = limitedCellRAs.iterator(); while(itcra.hasNext()){ CellRangeAddress cra = itcra.next(); int firstRow = cra.getFirstRow(); int lastRow =cra.getLastRow(); if(rowIndex>= firstRow && rowIndex<= lastRow){ int firstCol = cra.getFirstColumn(); int lastCol = cra.getLastColumn(); for(int j=0; j<indexes.length;j++){ short index = indexes[j]; if(index >= firstCol && index <= lastCol){ theMap.put(new CellPosition(rowIndex,index),cra); } } } } // 记录合并单元格所对应的列号 Iterator<CellPosition> itpos = theMap.keySet().iterator(); short[] compIndexes = new short[theMap.size()]; int count =0; while(itpos.hasNext()){ CellPosition cellPos = itpos.next(); int colIndex = cellPos.getCol(); compIndexes[count]=(short)colIndex; count++; } List<CellPosition> cachedList =new ArrayList<CellPosition>(); for(int i =0;i< indexes.length;i++){ if(!constains(compIndexes, indexes[i])){ // 如果不包含此列号,则对应的 cell 不是一个合并单元格,调用清单 2 中的方法获取其值 Cell cell = row.getCell(indexes[i]); Object result= getCellValue(cell); if(!StringUtility.checkNull(result)) resultBf.append( result+","); //$NON-NLS-1$ }else{ CellRangeAddress cellRA = theMap.get(new CellPosition (rowIndex, indexes[i])); if(cellRA != null){ int cellrow = cellRA.getFirstRow(); int cellcol = cellRA.getFirstColumn(); CellPosition cPos = new CellPosition(cellrow,cellcol); //If it is Not the cached CellRangeAddress object if(!cachedList.contains(cPos)){ cachedList.add(cPos); // 放到缓存中,如果是同一个合并单元格,我们取其值一次 Object value = getMergedRegionValue(sheet1,cellRA); // 获得合并区域的值 resultBf.append(value +","); //$NON-NLS-1$ } } } } //resultBf 可能不包含任何数据 if(resultBf.length()>0 && resultBf.charAt(resultBf.length()-1)==','){ resultBf.deleteCharAt(resultBf.lastIndexOf(",")); //$NON-NLS-1$ } return resultBf.toString(); } |
首先我们创建了一个 LinkedHashMap,用以保存那些和当前行的单元格相关的合并区域(该合并区域局限于清单 4 中所生成的合并区域),这里用 LinkedHashMap 既是保证 Map 里合并区域的顺序性。 CellPosition 实例记录了单元格的位置信息。接下来用 short 数组记录哪些列对应的单元格是合并单元格。进而程序遍历单元格,如果单元格所在的列不在构建的 short 数组中,则直接调用清单 3 的方法获取其值。如果单元格是合并单元格,如果在缓存中还没找到该合并区域,则获取该合并区域的值。也就是说,我们对这个合并区域的值只会读取一次。这样才能保证组合后的结果是正确性的。
应用 JDBC/Hibernate 插入数据
向数据库中插入数据的技术话题,大多读者都非常熟悉了。在本文中,程序同样是从映射规则的元数据中读到有关数据库的表信息和配置信息的,也就是说,应用 hibernate 等技术构建持久化层在本例中显得不太合适,因为我们不关心数据库的变动情况。读者在自己的项目中,可以根据实际情况决定是否采有 Hibernate 等持久化存储策略。本文主要采用 IBM DB2 数据库作为数据存储,通过解读 properties 文件获取数据库文件配置信息,调用 DAO 的 write 方法,从而生成数据库中的数据记录信息的,如清单 6 所示。
清单 6. 完成 SQL 语句的插入操作
public void write(List<String> sqlList){ if(sqlList == null ||sqlList.size() ==0){ throw new IllegalArgumentException("SQL List can't be null or empty"); //$NON-NLS-1$ } try { conn = DriverManager.getConnection(uri, user, password); conn.setAutoCommit(false); stmt = conn.createStatement(); Iterator<String> it = sqlList.iterator(); while(it.hasNext()){ String sql = it.next(); stmt.executeUpdate(sql); } conn.commit(); stmt.close(); conn.close(); } catch (SQLException sqlE) { sqlE.printStackTrace(); try { if(conn!= null){ _logger.log(Level.FINE, "Roll back"); //$NON-NLS-1$ conn.rollback(); } } catch (Exception eE) { _logger.log(Level.FINE, "Rollback failed"); //$NON-NLS-1$ } } } |
本文的解决方案不局限于 IBM DB2 数据库,同时支持 MySQL 等若干数据库。针对不同数据库的数据类型,解析框架会动态的加载相应的类型配置文件,并对解析到的 Excel 信息作相应的修整(比如:DB2 数据库某一字段的数据类型要求是 decimal 的 , 如果解析器取到的 excel 相应的内容是字符串格式的话,那么需要进行转换,并保证一定的容错性),从而保证生成正确的 SQL 语句信息。
小结
本文提供了一种解决 Excel 数据转存数据库的通用解决方案。重点介绍了几种映射规则,这些规则都是最基本的,也是最常用的,读者可以根据需要,基于此而设计更复杂的映射规则。同时本文也着重介绍了,映射规则四的一些实现算法,希望能够为被这类问题所困扰的同志们提供一些有价值的参考。(责任编辑:A6)