JAVA
[JAVA] POI - SAX 엑셀 대용량 업로드
unboxing
2024. 2. 13. 17:58
서비스 단
대용량 엑셀파일을 읽어드리고 insert 하는 서비스
public int xlsxUploadSaxBatch(File file, final MultipartHttpServletRequest request, HttpServletResponse response) throws Exception {
OPCPackage opcPackage = OPCPackage.open(file); // file
// OPCPackage opcPackage = OPCPackage.open(file.getInputStream()); // MultipartFile
XSSFReader xssfReader = new XSSFReader(opcPackage);
StylesTable styles = xssfReader.getStylesTable(); //excel 스타일
SharedStringsTable strings = xssfReader.getSharedStringsTable(); //excel 중복된 문자열 공유
XMLReader parser = XMLReaderFactory.createXMLReader(); //SAX 파서생성
ExcelSheetHandler handler = new ExcelSheetHandler(styles, strings);
parser.setContentHandler(handler);
XSSFReader.SheetIterator sheets = (XSSFReader.SheetIterator) xssfReader.getSheetsData();
while (sheets.hasNext()) {
InputStream sheetInputStream = sheets.next();
InputSource sheetSource = new InputSource(sheetInputStream);
parser.parse(sheetSource);
sheetInputStream.close();
}
opcPackage.close();
List<List<String>> excelData = handler.getExcelData(); // 핸들러 getData 메서드호출
List<Map<String, String>> paramList = new ArrayList<>();
int batchSize = 2100; // 한 번에 처리할 데이터의 개수
for (int i = 0; i < excelData.size(); i++) {
List<String> row = excelData.get(i);
Map<String, String> paramMap = new HashMap<>();
paramMap.put("row1",row.get(0));
paramMap.put("row2",row.get(1));
paramMap.put("row3",row.get(2));
paramMap.put("row4",row.get(3));
paramList.add(paramMap); // paramMap을 복제하여 추가
if ((i+1) % batchSize == 0 || (i+1) == excelData.size()) {
result = query.insert("unboxing_query.insertExcelSax", paramList);
paramList.clear(); // paramList를 초기화
}
}
return result;
}
핸들러
DefaultHandler 클래스 상속 받아서 구현
DefaultHandler 클래스를 상속 받은 SheetContentsHandler를 사용해도 된다.
package com.unboxing.it.core.common.util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.poi.xssf.model.SharedStringsTable;
import org.apache.poi.xssf.model.StylesTable;
import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTRst;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;
public class ExcelSheetHandler extends DefaultHandler{
private SharedStringsTable strings;
private boolean isCell;
private boolean sstCell; //SharedStringsTable 사용여부 체크
private StringBuilder cellValue;
private String cellRef;
private List<List<String>> excelData;
private List<String> cellRow;
private int headerCnt;
private int cellIndex;
private int currentRowNum;
private int cellHeadIndex = 1; //엑셀 헤더행
private int cellStartIndex = 2; // 엑셀 업로드 행
public ExcelSheetHandler(StylesTable styles, SharedStringsTable strings) {
this.strings = strings;
this.isCell = false;
this.sstCell = false;
this.cellValue = new StringBuilder();
this.cellRef = "";
this.excelData = new ArrayList<>();
this.currentRowNum = 0;
}
@Override
public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXParseException {
if (name.equals("row")) {
// "row" 엘리먼트가 시작되면 현재 행 번호를 가져옴
String rowNumStr = attributes.getValue("r");
if (rowNumStr != null) {
currentRowNum = Integer.parseInt(rowNumStr);
}
}else if (currentRowNum == cellHeadIndex && name.equals("c")){
headerCnt++; // Excel의 헤더 개수
}else if (currentRowNum >= cellStartIndex && name.equals("c")) {
// 2번째 행 이후의 "c" 엘리먼트가 시작되면 셀로 처리
isCell = true;
cellValue.setLength(0);
// 셀의 내용을 확인
String cellType = attributes.getValue("t"); // 셀의 데이터 타입
cellRef = attributes.getValue("r").replaceAll("\\d",""); //header 열
if (cellType != null && cellType.equals("s")) {
// "s" 타입은 SharedStringsTable
sstCell = true;
} else {
sstCell = false; //일반형식
}
}
}
@Override
public void endElement(String uri, String localName, String name) throws SAXException {
if (name.equals("c")) {
if (sstCell) {
// SharedStringsTable 타입인 경우
if(!cellValue.toString().equals("")) {
int sharedStringIndex = Integer.parseInt(cellValue.toString());
CTRst xmlData = strings.getEntryAt(sharedStringIndex);
cellRow.set(cellIndex,xmlData.getT());
}
}else if(!sstCell) {
if(!cellValue.toString().equals("")) {
Double numericValue = Double.parseDouble(cellValue.toString());
if( numericValue < 0) {
cellRow.set(cellIndex,String.valueOf(numericValue));
}else {
cellRow.set(cellIndex,cellValue.toString());
}
}
}
isCell = false;
sstCell = false;
}else if (name.equals("row")) {
if(currentRowNum == cellHeadIndex){
cellRow = new ArrayList<>(Collections.nCopies(headerCnt, "")); // 헤더행일 경우 List size 설정
}else if (currentRowNum >= cellStartIndex) {
excelData.add(new ArrayList<>(cellRow));
}
Collections.fill(cellRow, ""); //행 추가 이후 cellRow 다시 초기화
}
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
if (isCell) {
//셀 열위치 ( List의 위치 )
cellIndex = getIndexFromLetter(cellRef);
//셀내용
String cellContent = new String(ch, start, length);
cellValue.append(cellContent);
}
}
// 문자열을 받아서 A부터 시작하는 인덱스로 변환하는 메서드
private static int getIndexFromLetter(String letter) {
char c = letter.charAt(0);
return c - 'A'; // 'A'를 0으로 매핑
}
public List<List<String>> getExcelData() {
return excelData;
}
}
xml ( postgres )
> bulk insert
<insert id="insertExcelSax">
/* unboxing_query.insertExcelSax */
INSERT INTO unboxing_table
( column1 , column2 , column3 , column4)
VALUES
<foreach collection="list" item="item" open="(" close=")" index="index">
<if test="index % 300 == 0 and index != 0">
<!-- 매 300번째 반복마다 commit 수행 -->
)
<![CDATA[; COMMIT;]]>
<!-- 새로운 INSERT 구문 시작 -->
<![CDATA[
INSERT INTO unboxing_table
(column1 , column2 , column3 , column4)
VALUES(
]]>
</if>
<if test="index % 300 != 0">
<![CDATA[), (]]>
</if>
#{item.row1}
,#{item.row2}
,#{item.row3}
,#{item.row4}
</foreach>
<if test="list.size() % 300 != 0">
<![CDATA[; COMMIT;]]>
</if>
<!-- 남은 데이터에 대한 commit 수행 -->
</insert>
엑셀 데이터 100만건 기준 3분 소요