Entity 映射機制實現原理

本文是《輕量級 Java Web 框架架構設計》的系列博文。 java

爲了開發一款輕量級的 Java Web 開發框架,我不惜放棄了我最深愛的 Hibernate。我很是感謝它這麼多年來教會了我許多知識,讓我不會再走彎路。既然沒有彎路,那麼什麼纔是捷徑呢?那就是儘量的簡單,經過慣例(Convention)而不是配置(Configuration)。說通來說就是徹底不須要任何的配置,就連註解都不須要! 程序員

下面這個 Entity,我想足以讓你們明白。 sql

public class Product extends BaseEntity {
    private long productTypeId;
    private String productName;
    private String productCode;
    private int price;
    private String description;

    public long getProductTypeId() {
        return productTypeId;
    }

    public void setProductTypeId(long productTypeId) {
        this.productTypeId = productTypeId;
    }
...
}

不須要 @Entity、@Table、@Column 這樣的註解,同時也放棄了 @OneToMany 這類具備爭議的功能。僅僅繼承了 BaseEntity。對於 BaseEntity 以及有關 Entity 的架構思路,請參考我另一篇博文《對 Entity 的初步構思》。 數據庫

那麼框架又是怎樣加載 classpath 中全部的 Entity 呢?仍是用代碼來講明一切吧。 架構

public class EntityHelper {
    private static final Map<Class<?>, Map<String, String>> entityMap = new HashMap<Class<?>, Map<String, String>>();

    static {
        // 獲取並遍歷全部 Entity 類
        List<Class<?>> entityClassList = ClassHelper.getClassList(BaseEntity.class);
        for (Class<?> entityClass : entityClassList) {
            // 獲取並遍歷該 Entity 類中的全部方法(不包括父類中的方法)
            Field[] fields = entityClass.getDeclaredFields();
            if (ArrayUtil.isNotEmpty(fields)) {
                // 建立一個 Field Map(用於存放 Column 與 Field 的映射關係)
                Map<String, String> fieldMap = new HashMap<String, String>();
                for (Field field : fields) {
                    String fieldName = field.getName();
                    String columnName = StringUtil.toUnderline(fieldName); // 將駝峯風格替換爲下劃線風格
                    // 若 Field 與 Column 不一樣,則須要進行映射
                    if (!fieldName.equals(columnName)) {
                        fieldMap.put(columnName, fieldName);
                    }
                }
                // 將 Entity 類與 Field Map 放入 Entity Map 中
                if (MapUtil.isNotEmpty(fieldMap)) {
                    entityMap.put(entityClass, fieldMap);
                }
            }
        }
    }

    public static Map<Class<?>, Map<String, String>> getEntityMap() {
        return entityMap;
    }
}

這個 EntityHelper 類會自動初始化 Entity 類與 Column/Field 的映射關係,並放入 entityMap中。此後,就能夠經過 getEntityMap() 這個靜態方法隨時獲取這些映射關係了,這個也就實現了所謂的 ORM(固然是輕量級的了)。 框架

以上這些都是框架爲咱們提供的基礎支持,還有一個成員不得不說,那就是 DBHelper 類,有了它以後,程序員就能夠無需面對 JDBC 返回的 ResultSet 了,而是面對 JavaBean(或者說是 Entity),也就是說,自動將 ResultSet 轉換爲 JavaBean。在這裏我得感謝 Apache Commons DbUtils 類庫,它真的是太好了,很是輕量級,我只用了一次就愛上它了! ide

public class DBHelper {
    private static final BasicDataSource ds = new BasicDataSource();
    private static final QueryRunner runner = new QueryRunner(ds);

    static {
        ds.setDriverClassName(ConfigHelper.getProperty("jdbc.driver"));
        ds.setUrl(ConfigHelper.getProperty("jdbc.url"));
        ds.setUsername(ConfigHelper.getProperty("jdbc.username"));
        ds.setPassword(ConfigHelper.getProperty("jdbc.password"));
        ds.setMaxActive(Integer.parseInt(ConfigHelper.getProperty("jdbc.max.active")));
    }

    // 獲取數據源
    public static DataSource getDataSource() {
        return ds;
    }

    // 執行查詢語句(返回一個對象)
    public static <T> T queryBean(Class<T> cls, String sql, Object... params) {
        Map<String, String> map = EntityHelper.getEntityMap().get(cls);
        return DBUtil.queryBean(runner, cls, map, sql, params);
    }

    // 執行查詢語句(返回多個對象)
    public static <T> List<T> queryBeanList(Class<T> cls, String sql, Object... params) {
        Map<String, String> map = EntityHelper.getEntityMap().get(cls);
        return DBUtil.queryBeanList(runner, cls, map, sql, params);
    }

    // 執行更新語句(包括 UPDATE、INSERT、DELETE)
    public static int update(String sql, Object... params) {
        return DBUtil.update(runner, sql, params);
    }
}

如今只是提供了最爲典型的數據庫操做,也就是 SELECT、UPDATE、INSERT、DELETE 了。未來或許還會有一些平常使用的方法,在這個 DBHelper 類裏面進行擴展吧。 單元測試

細心的讀者可能已經看到了,在 DBHelper 中還有一個 DBUtil,這個類又是什麼呢?難道就是對 Apache Commons DbUtils 的封裝類?對頭! 測試

public class DBUtil {
    // 查詢(返回 Array)
    public static Object[] queryArray(QueryRunner runner, String sql, Object... params) {
        Object[] result = null;
        try {
            result = runner.query(sql, new ArrayHandler(), params);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return result;
    }

    // 查詢(返回 ArrayList)
    public static List<Object[]> queryArrayList(QueryRunner runner, String sql, Object... params) {
        List<Object[]> result = null;
        try {
            result = runner.query(sql, new ArrayListHandler(), params);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return result;
    }

    // 查詢(返回 Map)
    public static Map<String, Object> queryMap(QueryRunner runner, String sql, Object... params) {
        Map<String, Object> result = null;
        try {
            result = runner.query(sql, new MapHandler(), params);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return result;
    }

    // 查詢(返回 MapList)
    public static List<Map<String, Object>> queryMapList(QueryRunner runner, String sql, Object... params) {
        List<Map<String, Object>> result = null;
        try {
            result = runner.query(sql, new MapListHandler(), params);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return result;
    }

    // 查詢(返回 Bean)
    public static <T> T queryBean(QueryRunner runner, Class<T> cls, Map<String, String> map, String sql, Object... params) {
        T result = null;
        try {
            if (MapUtil.isNotEmpty(map)) {
                result = runner.query(sql, new BeanHandler<T>(cls, new BasicRowProcessor(new BeanProcessor(map))), params);
            } else {
                result = runner.query(sql, new BeanHandler<T>(cls), params);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return result;
    }

    // 查詢(返回 BeanList)
    public static <T> List<T> queryBeanList(QueryRunner runner, Class<T> cls, Map<String, String> map, String sql, Object... params) {
        List<T> result = null;
        try {
            if (MapUtil.isNotEmpty(map)) {
                result = runner.query(sql, new BeanListHandler<T>(cls, new BasicRowProcessor(new BeanProcessor(map))), params);
            } else {
                result = runner.query(sql, new BeanListHandler<T>(cls), params);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return result;
    }

    // 查詢指定列名的值(單條數據)
    public static <T> T queryColumn(QueryRunner runner, String column, String sql, Object... params) {
        T result = null;
        try {
            result = runner.query(sql, new ScalarHandler<T>(column), params);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return result;
    }

    // 查詢指定列名的值(多條數據)
    public static <T> List<T> queryColumnList(QueryRunner runner, String column, String sql, Object... params) {
        List<T> result = null;
        try {
            result = runner.query(sql, new ColumnListHandler<T>(column), params);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return result;
    }

    // 查詢指定列名對應的記錄映射
    public static <T> Map<T, Map<String, Object>> queryKeyMap(QueryRunner runner, String column, String sql, Object... params) {
        Map<T, Map<String, Object>> result = null;
        try {
            result = runner.query(sql, new KeyedHandler<T>(column), params);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return result;
    }

    // 更新(包括 UPDATE、INSERT、DELETE,返回受影響的行數)
    public static int update(QueryRunner runner, String sql, Object... params) {
        int result = 0;
        try {
            result = runner.update(sql, params);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return result;
    }
}

在 DBUtil 中,已經對 Apache Commons DbUtils 作了大量的封裝,其彷佛這個類對於程序員們來說,不到萬不得已,儘可能不要用,最好仍是用 DBHelper 吧。若是 DBHelper 中的方法不夠用了,不妨給你的架構師提一個需求,讓他去擴展 DBHelper。  this

那麼 Service 層又是如何調用 DBHelper 的呢? 來看看這個 ProductService 吧。

public interface ProductService {
    Product getProduct(long productId);
}
以上只是一個接口而已,實現類應該如何編寫呢?彆着急,往下看。
public class ProductServiceImpl extends BaseService implements ProductService {
    @Override
    public Product getProduct(long productId) {
        String sql = SQLHelper.getSQL("select.product.id");
        return DBHelper.queryBean(Product.class, sql, productId);
    }
}

這裏先從 SQLHelper 中拿到 SQL 語句(帶有佔位符「?」的),而後調用 DBHelper 的 queryBean() 方法,傳入須要轉換的 Java 類(Product.class)、SQL 語句(sql)、填充 SQL語句佔位符的參數(productId)。返回值就是 Product 對象了。是否是很簡單呢? 不對!這裏爲何沒有數據庫鏈接呢? 這個問題留做讀者本身思考吧。

有必要看看 SQLHelper 類是如何實現的,雖然它很簡單,只是讀取 properties 文件,並經過 key 獲取 value(也便是 SQL)而已。

public class SQLHelper {
    private static final Properties sqlProperties = FileUtil.loadPropertiesFile("sql.properties");

    public static String getSQL(String key) {
        String value = "";
        if (sqlProperties.containsKey(key)) {
            value = sqlProperties.getProperty(key);
        } else {
            System.err.println("Can not get property [" + key + "] in sql.properties file.");
        }
        return value;
    }
}

以上代碼真的很簡單吧?那麼,sql.properties 的內容還有懸念嗎?

select.product.id = select * from product where id = ?

程序員們要作的就是寫這個 sql.properties,還有 XxxService,以及 XxxServiceImpl 了。

萬事俱備,只欠東風。最後來一個單元測試吧!

public class ProductServiceTest {
    private ProductService productService = new ProductServiceImpl();

    @Test
    public void loginTest() {
        long productId = 1;
        Product product = productService.getProduct(productId);
        Assert.assertNotNull(product);
    }
}

這裏沒有對 productService 進行依賴注入,是否須要依賴注入呢?應該如何進行依賴注入呢?又是如何實現依賴注入的呢?這些問題留做下一篇博文與你們分享。

來一張高清無碼類圖吧。

灰色虛線圈出來得三個類是程序員要作的。灰色是 Base 類,黃色是 Helper 類,藍色是 Util 類,還有一個 sql.properties 也是程序員要寫的。

請你們不要吝嗇,隨便點評!


補充(2013-09-04)

很是感謝網友們的評價!對與 Entity 的結構有必要稍做修改,細節以下:

若是列名是 Java 關鍵字(例如:列名爲 class),那麼在 Entity 中必定不能定義一個名稱爲 class 的字段。因此,我仍是採用了 @Column 註解,將列名寫在註解中。

public class Product extends BaseEntity {
    private long productTypeId;

    private String productName;

    private String productCode;

    private int price;

    private String description;

    @Column("class")
    private int clazz;

    public long getProductTypeId() {
        return productTypeId;
    }

    public void setProductTypeId(long productTypeId) {
        this.productTypeId = productTypeId;
    }
...
}
在 Product 實體的 clazz 字段上定義了一個 @Column("class") 註解,這就覺得着將列名 class 映射爲字段名 clazz。並且,若是使用了 @Column 註解,那麼就不會採用默認的映射規則(用「下劃線風格」的列名映射「駝峯風格」的字段名)。那麼這又是如何實現的呢?對 EntityHelper 稍做修改便可實現。
...
for (Field field : fields) {
    String fieldName = field.getName();
    String columnName;
    // 若該字段上存在 @Column 註解,則優先獲取註解中的列名
    if (field.isAnnotationPresent(Column.class)) {
        columnName = field.getAnnotation(Column.class).value();
    } else {
        columnName = StringUtil.toUnderline(fieldName); // 將駝峯風格替換爲下劃線風格
    }
    // 若字段名與列名不一樣,則須要進行映射
    if (!fieldName.equals(columnName)) {
        fieldMap.put(columnName, fieldName);
    }
}
...
經過一個 if...else... 就輕易地解決了此問題。

那麼確定會有人要問:若是數據庫的表名是 Java 關鍵字,那麼必定會與 Entity 的名稱相沖突,那是否應該在 Entity 上標註一個 @Table("表名") 註解呢?

我我的認爲是不須要這樣作的,爲何呢?由於 SQL 語句都是程序員本身編寫的,並統一寫在 sql.properties 文件中,在 SQL 語句上用的就是表名,咱們須要映射的是列名,僅此而已。

至於 Entity 中有些字段是否必定要與列名對應?這個問題,我還要細想一下,也請廣大網友給出建議。

相關文章
相關標籤/搜索