[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,原理都是一樣的

Read more

[LeetCode] #12 Integer to Roman 解題

題目連結 題型解說 這是一題難度為普通的題目 需要設計一個方法,此方法會傳入一個整數 num 要求是把整數轉換成羅馬字母,轉換清單如下 I => 1 V => 5 X => 10 L => 50 C => 100 D => 500 M => 1000 但羅馬字母有一些特殊規則 4 並非 IIII 而是 IV,9 並非 VIIII 而是 IX 這規則同樣可以套用到 40 90 400 900 解題思路 既然知道特殊規則是一樣的,變得是使用的符號,那麼先從 num 取個位數開始 轉換完成後,把 num 除上 10,消除個位數,

By Michael

[LeetCode] #11 Container With Most Water 解題

題目連結 題型解說 這是一題難度為中等的題目 需要設計一個方法,此方法會傳入一個數字陣列 height 陣列中的元素代表每一個柱子的高度 現在需要計算出,該陣列中以某兩隻柱子為邊界,最多可以裝多少水 以範例來說 height = [1,8,6,2,5,4,8,3,7] 最多可以裝 7 * 7 = 49 單位的水 解題思路 計算面積就是底乘上高 底的計算方式為 「右邊柱子的 index」 減去 「左邊柱子的 index」 高就取最短的那一根柱子高度 拿題目給的例子來當範例 建立三個變數 result、left、right left、right 代表左右兩邊的 index result 代表目前最大容量,初始值 0 第一步,找出最短的柱子高度,

By Michael

[LeetCode] #941 Valid Mountain Array 解題

題目連結 題型解說 這是一題難度為簡單的題目 需要設計一個方法,此方法會傳入一個數字陣列 arr 判斷陣列中的元素是不是由低到高再從高到低(山形)的排序,且不連續一個以上數字 比如說 [1,2,3,2] 就是一個山形陣列,但 [1,2,2,3,2] 不是,因為有兩個 2 [1,2,3,4,5] 和 [5,4,3,2,1] 也不算是山形陣列,前者只有往上沒有往下,後者相反 解題思路 準備一個數字變數(temp)和布林變數(asc),跑一次迴圈,有可能遇到如下狀況 1. 某個數字與前一個數字相同,這時候直接回傳 false

By Michael

[LeetCode] #944 Delete Columns to Make Sorted 解題

題目連結 題型解說 這是一題難度為簡單的題目 需要設計一個方法,此方法會傳入一個字串陣列 strs 這個陣列中每個字串的長度都相同,字串內容都是小寫英文 需要檢查每個元素的第 N 個字元是不是由小至大排列,並回傳有幾個錯誤排列 比如傳入的陣列長這樣 ["cba","daf","ghi"] 取第一個字元 = cdg 取第二個字元 = bah 取第三個字元 = afi 其中第二組的結果(bah)並不是由小至大排列,故回傳 1 解題思路 這一題就用兩個迴圈各別把字元取出來,並比較是否比上一個字元大(Java 中的字元可以直接比較),如果不是就將結果+1 程式碼 Java class Solution { public int minDeletionSize(String[] strs) { int result = 0; for (int i = 0,

By Michael