首先說明使用的環境是:java和Sqlserver。
最近公司需要進行大數據量的導入操作。原來使用的是Apache POI,雖然可以實現功能,但是因為邏輯處理中需要進行許多校驗,處理速度太慢,使用多線程之后也不盡如人意。在網上搜索之后,找到了OPENROWSET和OPENDATASOURCE,發現使用OPENROWSET,可以非常快速的把Excel導入到數據庫中。之后的各種校驗,我可以通過編寫sql來實現。最終結果是6w條數據可以在10秒內完成。當然數據量增加之后,完成時間並不會明顯增加。這需要編寫的sql比較高效,是另一方面的問題了。
首先可能需要下載一個小的程序AccessDatabaseEngine_X64.exe。
之后需要開啟配置
啟用:
exec sp_configure 'show advanced options',1 reconfigure exec sp_configure 'Ad Hoc Distributed Queries',1 reconfigure
關閉:
exec sp_configure 'Ad Hoc Distributed Queries',0 reconfigure exec sp_configure 'show advanced options',0 reconfigure
之后可以通過OPENROWSET來查詢Excel文件的內容。當然也可以改為SELECT INTO存到數據庫中。
SELECT * FROM OPENROWSET('Microsoft.ACE.OLEDB.12.0',
'Excel 12.0;HDR=YES;Database=E:\DataBack\Copy of SD Expired Contracts.xlsx', ['Copy of SD Expired Contracts$'])
這樣使用的前提是你知道需要導入的Sheet的名稱。而如果不知道的話就需要使用下面的方法,查詢出所有的Sheet名稱,再由用戶選擇導入哪一個。
EXEC sp_addlinkedserver 'ExcelSource', '', 'Microsoft.ACE.OLEDB.12.0', 'E:\DataBack\Copy of SD Expired Contracts.xlsx', NULL, 'Excel 8.0' EXEC sp_addlinkedsrvlogin 'ExcelSource', 'false' GO EXECUTE SP_TABLES_EX 'ExcelSource'
下面是我的Java代碼:
這個方法用戶獲得Sheet Name List。
public static List<String> getSheetNameList(String filePath,BaseDao baseDao){
String excelSource = "ExcelSource_"+StringUtils.getUUIDString();
String addSourceSql = "{CALL SP_ADDLINKEDSERVER(?,'','Microsoft.ACE.OLEDB.12.0',?,NULL,'Excel 8.0')}";
SQLQuery query = baseDao.getSQLQuery(addSourceSql);
query.setParameter(0, excelSource);
query.setParameter(1, filePath);
query.executeUpdate();
String loginSourceSqql = "{CALL SP_ADDLINKEDSRVLOGIN(?,'false')}";
query = baseDao.getSQLQuery(loginSourceSqql);
query.setParameter(0, excelSource);
query.executeUpdate();
String sheetNameSql = "{CALL SP_TABLES_EX(?)}";
query = baseDao.getSQLQuery(sheetNameSql);
query.setParameter(0, excelSource);
query.setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP);
List<Map<String, Object>> list = query.list();
List<String> sheetList = new ArrayList<String>();
for(int i=0;i<list.size();i++){
String sheetName = (String) list.get(i).get("TABLE_NAME");
if(sheetName.endsWith("_xlnm#_FilterDatabase")){
}else{
sheetList.add(sheetName);
}
}
return sheetList;
}
這個方法用戶創建一個臨時表,存儲Excel文件內容。臨時表的字段名是根據Excel表頭來創建的。
public static String uploadAndCreateTable(String filePath,String sheetName,BaseDao baseDao) {
String importTableName = "tbl_zz_"+StringUtils.getUUIDString();
String uploadFileSql = "SELECT IDENTITY(int, 1, 1) as %s,t.*,CAST(NEWID() AS VARCHAR(36)) AS %s into %s FROM OPENROWSET('Microsoft.ACE.OLEDB.12.0','Excel 12.0;HDR=YES;Database=%s', [%s]) as t";
uploadFileSql = String.format(uploadFileSql,ImportVisitorUtil.importIndex,ImportVisitorUtil.importUUID,importTableName,filePath,sheetName);
SQLQuery query = baseDao.getSQLQuery(uploadFileSql);
query.executeUpdate();
replaceSpecialCharacter(baseDao, importTableName);
changeColumnCollation(baseDao, importTableName);
return importTableName;
}
代碼編寫過程中發現一個問題,如果Excel表頭中含有":",在編寫sql過程中會跟:name這種占位符沖突,我冒號替換成了空格。
private static void replaceSpecialCharacter(BaseDao baseDao,String tableName){
String cha = ":";
String cha_ = "%:%";
String sql = "SELECT COLUMN_NAME columnName FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME like ? ";
SQLQuery query = baseDao.getSQLQuery(sql);
query.setParameter(0, tableName);
query.setParameter(1, cha_);
query.setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP);
List<Map<String,Object>> list = query.list();
for(Map<String,Object> map : list){
String columnName = (String) map.get("columnName");
String newColumnName = columnName.replace(cha, " ");
String tableColumnName = String.format("%s.[%s]", tableName, columnName);
String changeSql = "{CALL SP_RENAME(?,?,'column')}";
query = baseDao.getSQLQuery(changeSql);
query.setParameter(0, tableColumnName);
query.setParameter(1, newColumnName);
query.executeUpdate();
}
}
另一個問題是,本地數據庫安裝的時候使用的排序規則與服務器不一致,導致編寫的sql運行時出現錯誤,提前修改排序規則。
private static void changeColumnCollation(BaseDao baseDao,String tableName){
String defaultCollation = "SQL_Latin1_General_CP1_CI_AS";
String dataType = "nvarchar";
String sql = "SELECT COLUMN_NAME AS columnName,CHARACTER_MAXIMUM_LENGTH AS length FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND COLLATION_NAME <> ? AND DATA_TYPE = ? ";
SQLQuery query = baseDao.getSQLQuery(sql);
query.setParameter(0, tableName);
query.setParameter(1, defaultCollation);
query.setParameter(2, dataType);
query.setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP);
List<Map<String,Object>> list = query.list();
for(Map<String,Object> map : list){
String columnName = (String) map.get("columnName");
Integer length = (Integer) map.get("length");
String changeSql = String.format("ALTER TABLE [%s] ALTER COLUMN [%s] %s(%d) COLLATE %s",tableName,columnName,dataType,length,defaultCollation);
query = baseDao.getSQLQuery(changeSql);
query.executeUpdate();
}
}
可以使用下面sql查詢出表中的所有列。
SELECT * FROM INFORMATION_SCHEMA.COLUMNS
前台可以讓用戶現在Excel中每一列對應的真實表的列。對應關系組織好之后,根據需要給臨時表添加字段、修改數據等等,最后使用INSERT SELECT插入數據或修改數據。
編寫sql的時候可能因為列名的不規范,導致sql語法錯誤。這時候需要在列名或表名前后添加中括號“[]”。Jaya使用String.format();比較方便。
