วันเสาร์ที่ 18 กรกฎาคม พ.ศ. 2558

Read ExcelFile to Entity By @Annotation

 เรามาเขียน @Annotation กันเถอะ

 วันนี้ผมมีงานที่ต้องนำข้อมูล Excel file ลง Database 
 ลักษณะงานแบบนี้หลายคนคงเจอกันบ่อยมากมาย และหลายคนก็คงเขียนกันแบบถึก ๆ 

 ... เสียใจด้วย ที่ java code ที่ท่านเห็นต่อไปนี้ จะเป็น version 7 
 ... #ร้องให้หนักมาก 
 
 วันนี้เรามาทำให้มันยากกว่าเดิมกันดีกว่า ... จะทำไปทำไม 

 เปลี่ยนเป็นวันนี้เรามาทำอะไรกันเล่น ๆ  สนุก ๆ กันดีกว่า 
 
 ลักษณะ การทำงาน ก็คงคล้าย ๆ กับ การทำ Mapping ... ใครที่เคยเขียน 
 Hibernate Annotation Mapping คงคุ้นเคยกันดีว่า เราสามารถใช้ความ
 สามารถของเจ้า @Annotation นั้น ทำการ Map Field ใน Entity Class
 ให้ตรงกับ Field ใน Database ... แต่อันนั้นมันฉลาดกว่าที่ผมทำอะ -_-"

 เริ่มเลยดีกว่า เรามาทำหนด Class ที่เป็น Entity ที่ต้องการนำข้อมูลจากเจ้า
 Excel File มายัดลงกันเลย 
package x.y.z.util.excelupload;

import x.y.z.util.excelupload.annotation.ExcelField;
import x.t.z.util.excelupload.type.ExcelFieldType;

public class TestBean {
    
    @ExcelField(type = ExcelFieldType.STRING, position = 0) 
    private String pid;
    
    @ExcelField(type = ExcelFieldType.STRING, position = 1) 
    private String name;

    @ExcelField(type = ExcelFieldType.STRING, position = 2) 
    private String surname;
    
    public TestBean(){
        
    }

// get - set ...
    public String getPid() {
        return pid;
    }
//...
}
 เราทำการกำหนด class มาตัวนึง ใช้เก็บข้อมูล pid, name, surname
 ก็ประมาณว่า เลขที่บัตรประจำตัวประชาชน, ชื่อ, นามสกุล อะไรประมาณนี้

 อย่าลืมเขียน get - set ให้ครบนะครับ แต่หลัก ๆ เราจะใช้ set เพื่อจะ
 ใช้ในการบันทึกข้อมูล 

 จากด้นบน เราจะเห็นว่ามี @ExcelField ดูหน้าตาแปลก ๆ ไปนิดถ้าชอบเดี๋ยวก็ชิน
 เรามาดูข้างใน @Annotation ตัวนี้กันเลยว่ามันมีหน้าตาเป็นแบบใหน
package x.y.z.util.excelupload.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import x.y.z.util.excelupload.type.ExcelFieldType;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelField {

    public int position() default 0;
    
    public ExcelFieldType type() default ExcelFieldType.STRING;

    public String format() default "";

}
 จาก code หลัก ๆ ผมก็จำลองกำหนดแค่ 
 - position คือตำแหน่ง cell ใน excelfile
 - type คือรูปแบบข้อมูล เป็นตัวอักษร, ตัวเลข, วัน(แต่ตรงนี้เราคงไม่ได้กำหนดหมด)
 - format อันนี้ผมทำเผื่อไว้ กรณีต้องกำหนดข้อมูลวันที่เข้ามามีรูปแบบ วันที่จาก excel 
 file หรือจะเป็นรูปแบบตัวเลข 

 มีการประกาศ enum ไว้ในส่วน type เพื่อกันการเลือกประเภทข้อมูลผิด 
package x.y.z.util.excelupload.type;

public enum ExcelFieldType {

    STRING, DATE, INTEGER, BIGDECIMAL
}
ก็มีแค่นี้แหละ enum จัดไปซะตั้ง 1 file เปลืองจริง เอาหละ แล้วเรามาดูในส่วน util ที่เราสร้างมาเอาฮา เอ้ย มาทำงานกัน
package x.y.z.util.excelupload;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import x.y.z.util.excelupload.annotation.ExcelField;
import x.y.z.util.excelupload.type.ExcelFieldType;

public class ExcelToBeanUtil {

    private Class clazz;
    private List dataList;
    private int rowAt = 0;

    public ExcelToBeanUtil(Class clazz) {
        this.clazz = clazz;
        dataList = new ArrayList<>();
    }

    private void addDataTolist(T data) {
        if (null == dataList) {
            dataList = new ArrayList<>();
        }
        dataList.add(data);
    }

    public ExcelToBeanUtil startRowAt(int rowAt) {
        this.rowAt = rowAt;
        return this;
    }

    public List execute(String pathFile) throws
            InvalidFormatException, IOException,
            InstantiationException, IllegalAccessException {
        
        FileInputStream file = new FileInputStream(
                new File(pathFile));

        XSSFWorkbook workbook = new XSSFWorkbook(file);
        XSSFSheet sheet = workbook.getSheetAt(0);

        Iterator rowIterator = sheet.iterator();
        for (int i = 0; rowIterator.hasNext() && i < rowAt; i++) {
            rowIterator.next();
        }

        while (rowIterator.hasNext()) {
            Row row = rowIterator.next();
            if (null == row.getCell(0).toString()
                    || row.getCell(0).getStringCellValue().equalsIgnoreCase("")) {
                break;
            }

            T data = clazz.newInstance();
            Field[] fields = data.getClass().getDeclaredFields();
            for (Field f : fields) {
                if (null == f.getAnnotations() || 0 == f.getAnnotations().length) {
                    continue;
                }
                ExcelField exField = f.getAnnotation(ExcelField.class);
                String fName = f.getName();
                fName = "set"
                        + fName.substring(0, 1).toUpperCase()
                        + fName.substring(1, fName.length());

                try {
                    Method method;

                    if (exField.type().equals(ExcelFieldType.INTEGER)) {
                        method = data.getClass()
                                .getDeclaredMethod(fName, Integer.class);
                        method.invoke(data, Integer.parseInt(
                                row.getCell(exField.position()).toString()));
                        
                    } else if (exField.type().equals(ExcelFieldType.BIGDECIMAL)) {
                        method = data.getClass()
                                .getDeclaredMethod(fName, BigDecimal.class);
                        BigDecimal b = new BigDecimal(
                                row.getCell(exField.position()).toString());
                        method.invoke(data, b);
                        
                    } else {
                        method = data.getClass()
                                .getDeclaredMethod(fName, String.class);
                        method.invoke(data,
                                row.getCell(exField.position()).toString());
                    }
                } catch (Exception ex) {
                    Logger.getLogger(ExcelToBeanUtil.class.getName())
                            .log(Level.SEVERE, null, ex);
                }
            }
            addDataTolist(data);
        }
        return this.dataList;
    }
}
 เอาหละ ทำไมมันได้ยาวแบบนี้ ... อย่าคิดมาก copy มาล้วน ๆ เดียวว่าง ๆ 
 ค่อยมาปรับแต่งให้ดูดี
 
  การทำงานของตัว util  นี้ไม่มีอะไรมาก มันแค่ไปหาว่า @Annotation ที่เราสร้าง
 ขึ้นนั้น ไปประกาศบน field ใดบ้าง หลังจากนั้น มันก็จะเอา field ไป เพิ่ม .set 
 เพื่อทำการบันทึกข้อมูลให้ field นั้น ... เอ่อ อย่าคิดมาก 
 วิธีการดึงข้อมูลก็ใช้แบบเรียกตรงไปเลย
 
  อีกส่วนเป็นการกำหนด ให้เราเริ่มอ่าน excel ได้ตั้งแต่บนนทัดใหน startRowAt อันนี้
 ผมลักไก่ไปก่อน ha ใช้วิธีวนทิ้งตามจำนวน

 เอาเป็นว่า เรามาเขียน test กันดีกว่า ว่ามันใช้งานได้จริงๆนะ
 เรามาเริ่มดูข้อมูลที่สมมติขึ้นมาก่อน 
PidNameSurname
3777777777777kanswatchupong
4888888888888ดรีมทีม
4888888888889ซูเปอร์ทีม
 Code Test ก็จะประมาณนี้ (อันนี้ gen test file จาก netbean 
 นะครับเขียนเองก็ได้)
package x.y.z.util.excelupload;

import java.util.List;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

public class ExcelToBeanUtilTest {

    public ExcelToBeanUtilTest() {
    }

    @BeforeClass
    public static void setUpClass() {
    }

    @AfterClass
    public static void tearDownClass() {
    }

    @Before
    public void setUp() {
    }

    @After
    public void tearDown() {
    }

    @Test
    public void testExecute() throws Exception {
        System.out.println("execute");
        
        ExcelToBeanUtil instance = new ExcelToBeanUtil(TestBean.class)
                .startRowAt(1);
        
        List beanList = instance
                .execute("/Users/kanswa/Desktop/profiletest.xlsx");

        for (TestBean bean : beanList) {
            System.out.println(" RESULT : "
                    + ToStringBuilder.reflectionToString(
                            bean, ToStringStyle.MULTI_LINE_STYLE
                    )
            );
        }
    }
} 
 ผลการ run ถ้าไม่มีอะไรนอกเหนือจากนี้นะจะเป็นดังนี้

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running z.y.z.util.excelupload.ExcelToBeanUtilTest
execute
 RESULT : x.y.z.util.excelupload.TestBean@66b697de[
  pid=3777777777777
  name=kan
  surname=swatchupong
]
 RESULT : x.y.z.util.excelupload.TestBean@7163ff8b[
  pid=4888888888888
  name=ดรีม
  surname=ทีม
]
 RESULT : x.y.z.util.excelupload.TestBean@5c1e38d5[
  pid=4888888888889
  name=ซูเปอร์
  surname=ทีม
]
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 5.525 sec
  จาก code test นี้จะเห็นว่า เวลาเขียนใช้งาน เราก็แค่โยน Entity ที่เรากำหนด
 ประเภทจาก @ExcelField แล้วเข้าไป ก่อนทำการเลือก file ที่ต้องการอ่าน 
 
  ข้อดีของการเขียนแบบนี้คือ เมื่อเรามีการเพิ่ม field เพื่อรับข้อมูลจาก Excel file  
 ที่มีการเปลี่ยนแปลง ก็จะสามารถทำได้โดยงาน 

  ข้อเสียคือ ... ขี้เกียจอธิบายให้คนอื่นเข้าใจ -_-" 

  ลงท้ายนี้ ประเภทข้อมูลผมยังเขียนไม่หมดนะแต่หวังว่าคงจะพอเห็นแนวทางเผื่อจะ
 ลองนำไปปรับใช้งานก็ปรับแต่งได้ตามใจชอบเลยครับ  

  เอาจริง ๆ คือ entity ที่ผมกำหนดนี้ก็เดี๋ยวใส่ @Annotation ของ Hibernate 
 แล้วจับ save เลยเนี่ยแหละครับ 

  วันนี้หมดแรงและ หวังว่าจะเป็น ประโยชน์ต่อผู้ใดก็ตามไม่มาก ก็น้อยที่ที่จะมาเขียนฮา ๆ กันนะครับ 

 ขอบคุณครับ ^^

ไม่มีความคิดเห็น: