[Java]自製ResultSet轉Object映射器

前言

有時候不想要使用ORM框架,但又想要自動映射的功能怎麼辦?

所以今天就來自製JAVA版本的簡單映射器吧

其實映射器有很多種方式可以實現

但必要前提是必須要知道資料表中的欄位要怎麼和物件中的欄位對應

  1. 下註解

  2. 寫XML文件

  3. 強制物件中欄位必須要和資料表中的欄位相同

但今天我們使用第一種方式(下註解)

這邊的註解不是平常寫代碼的那種註解(Comment、Mark)

而是 Annotation ,那就開始實作吧

建立 Annotation

到底什麼是 Annotation 呢,簡單一點的說就是用來輔助編譯器的

在編譯的過程中可以取得更多的資訊

平常常用的幾種 Annotation

  1. @Override 複寫父類別方法

  2. @SuppressWarnings 隱蔽一些編譯器警告

但其實 Annotation 也可以自己寫,一個簡單的範例如下

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.LOCAL_VARIABLE})
public @interface Column {
    //default "" 可以省略
    String name() default "";
}

我們自定義了一個 Annotation 叫做 Column ,來對內容做一些說明

  1. 聲明一個 Annotation 必須使用 @interface

  2. 我們定義了一個屬性名稱 name ,預設值是空字串

  3. @Retention 表示了這個 Annotation 會保留到什麼時候,相關參數如下
    3.1. RetentionPolicy.SOURCE 保留在java文件中,編譯過後就丟棄
    3.2. RetentionPolicy.CLASS 保留在class文件中,JVM加載後就丟棄
    3.3. RetentionPolicy.RUNTIME 保留在程式運行中,可利用反射獲取註解(這也是我們要的效果)

  4. @Target 表示了這個 Annotation 可以被使用在哪邊,相關參數如下
    4.1. ElementType.TYPE 用於類別
    4.2. ElementType.FIELD 用於常量
    4.3. ElementType.METHOD 用於方法
    4.4. ElementType.PARAMETER 用於方法上的參數
    4.5. ElementType.CONSTRUCTOR 用於建構式
    4.6. ElementType.LOCAL_VARIABLE 用於區域變數
    4.7. ElementType.ANNOTATION_TYPE 用於標註型態
    4.8. ElementType.PACKAGE 用於套件
    4.9. ElementType.TYPE_PARAMETER 用於泛型宣告,JDK8新增
    4.10. ElementType.TYPE_USE 用於各種型態,JDK8新增

如果是使用 JDK8 又嫌麻煩的話,可以直接使用 ElementType.TYPE_USE

建立 Class

現在來寫一個示範用的類別

public class TestModel {
    private int id;

    public int getId() {
        return id;
    }

    @Column(name = "id")
    public void setId(int id) {
        this.id = id;
    }
}

加入 Annotation ,name 屬性填入相對應數據表欄位名稱

PS 變數類型要與數據表中相同

那為什麼會把 Annotation 下在方法上而不是變數上呢?

原因有兩點

  1. 可以對資料做操作

  2. 若是類別有繼承時會方便一些(這點下方會說明)

建立 Mapper

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Mapper {
    @SuppressWarnings(value = { "unchecked" })
    public <T> List<T> ResultSetToObject(ResultSet resultSet, Class<?> outputClass) 
            throws IllegalAccessException, IllegalArgumentException, 
            InvocationTargetException, NullPointerException, Exception {

        if (resultSet == null) {
            throw new NullPointerException("ResultSet is null.");
        }

        List<T> outputList = new ArrayList<T>();
        Map<String, Method> methods = new HashMap<String, Method>();
        ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
        int columnCount = resultSetMetaData.getColumnCount() + 1;

        for (Method method : outputClass.getMethods()) {
            if (method.isAnnotationPresent(Column.class)) {
                methods.put(((Column) method.getAnnotation(Column.class)).name().toLowerCase(), method);
            }
        }

        if (methods.size() == 0) {
            throw new Exception("There is no method available.");
        }

        T model = null;
        while (resultSet.next()) {
            model = (T) outputClass.newInstance();

            for (int i = 1; i < columnCount; i++) {
                String columnName = resultSetMetaData.getColumnName(i).toLowerCase();
                Object columnValue = resultSet.getObject(i);
                
                if (methods.containsKey(columnName)) {
                    try {
                        methods.get(columnName).invoke(model, columnValue);
                    } catch (Exception e) {
                        throw new Exception(
                                "Argument type mismatch, column is "
                                        + columnName
                                        + ", value is "
                                        + columnValue
                                        + ", value type is "
                                        + columnValue.getClass().getSimpleName());
                    }
                }
            }

            outputList.add(model);
        }

        return outputList;
    }
}

以下來對內容做一些說明

if (resultSet == null) {
    throw new NullPointerException("ResultSet is null.");
}

簡單的檢查 ResultSet 是不是 null

List<T> outputList = new ArrayList<T>();
Map<String, Method> methods = new HashMap<String, Method>();
ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
int columnCount = resultSetMetaData.getColumnCount() + 1;

宣告之後會用到的變數

outputList 儲存結果

methods 存放類別中有 Annotation 的方法

resultSetMetaData 獲取資料表欄位

columnCount 資料表欄位數量

for (Method method : outputClass.getMethods()) {
    if (method.isAnnotationPresent(Column.class)) {
        methods.put(((Column) method.getAnnotation(Column.class)).name().toLowerCase(), method);
    }
}

if (methods.size() == 0) {
    throw new Exception("There is no method available.");
}

事先濾過一遍類別中有加入 Annotation 的方法,儲存在Map中

這樣可以省下多餘的迴圈

這邊先針對 getMethods() 這個方法說明一下

要獲取到類別的變數有兩種方法

  1. getMethods() 取得所有公開的方法(包含繼承來的公開方法)

  2. getDeclaredMethods() 取得所有方法(包含私有方法,但不包含繼承來的方法)

取得變數的 getFields()getDeclaredFields() 也是一樣的

所以類別有繼承的話,把 Annotation 加入到公開方法中會比較方便一點

T model = null;
while (resultSet.next()) {
    model = (T) outputClass.newInstance();
    
    for (int i = 1; i < columnCount; i++) {
        String columnName = resultSetMetaData.getColumnName(i).toLowerCase();
        Object columnValue = resultSet.getObject(i);
        
        if (methods.containsKey(columnName)) {
            try {
                methods.get(columnName).invoke(model, columnValue);
            } catch (Exception e) {
                throw new Exception(
                        "Argument type mismatch, column is "
                                + columnName
                                + ", value is "
                                + columnValue
                                + ", value type is "
                                + columnValue.getClass().getSimpleName());
            }
        }
    }

    outputList.add(model);
}

這邊就比較單純了,每一次迴圈對 Class 進行實例化

比對欄位名稱,如果存在於 methods 中,就執行方法賦值

最後把蒐集好的物件回傳

使用方法

public class Test {
    public static void main(String[] args) throws Exception{
        Connection connection = null;
        Statement statement = null;
        ResultSet resultSet = null;

        try {
            connection = (new DBFactory()).getConnection();
            statement = connection.createStatement();
            resultSet = statement.executeQuery(
                "SELECT id FROM table WHERE 1=1"
            );
            List<TestModel> list = (new Mapper()).ResultSetToObject(resultSet, TestModel.class);
        } catch(Exception e){
            
        }
    }
}

結論

一個簡單的映射器就完成了

對於一些簡單的應用上來說已經很夠用了,也不用去引入一堆JAR包

當然其中可以完善的部分不少,比如類別轉型

也可以加入 XML 轉 Object、Map 轉 Object,原理都是一樣的