ORM框架設計及實現

一 對象記錄映射

  本質上來說,ORM框架需要處理的就是如何將JAVA對象與數據表記錄進行關聯,便於JAVA對象的持久化,以及將表記錄自動轉換爲JAVA對象。

  這就需要存在一箇中間態的描述信息,描述信息必須能夠指明JAVA對象和表記錄的映射關係,包括但不限於JAVA對象類型和表的關係,對象成員和字段的關係。

  市面上成熟的ORM框架很多,但總體設計思路一定離不開中間描述信息,典型的如Mybaties,它通過配置文件來描述,又如Spring通過配置或者註解來描述。

  我們稱這種與平臺、語言無關的描述信息爲IDL,依賴於這種設計,實際上對配置的要求變高了,而且當數據表量激增時很難維護。

二 效率和性能

  無論是通過文件配置還是通過註解的方式來描述對象記錄的映射關係,都沒辦法直接將SQL字段類型直接轉換爲JAVA對象的成員類型,而且爲了能夠準確的定位到對象成員,必須使用大量的反射才能實現。

  只要涉及反射,那麼效率和性能就很難得到保障。那麼有沒有一種辦法能夠避開這種低效率高消耗的映射實現呢?是可以做到的,我們可以賦予DAO對象一種具備自我描述的能力,無需再通過讀取配置文件或者通過註解反射來實現對象和記錄的映射關係。

  這也是本文設計的出發點,儘可能的通過對象自身的信息來完成映射轉換,避免使用反射等低效高消耗的操作。

三 數據訪問描述信息

  要賦予DAO對象自我描述的能力,需要設計一組可供採集信息的接口定義:

public interface DataAccessDescription {
	public String getTableName();

	public String[] getPrimaryKeyArray();

	public String[] getIndexArray();

	public Map<String, SqlData> getFieldMap();
}

  本文僅提供設計思路,並不完全實現所有功能,上述接口定義已經可以滿足示意的需求。一個DAO對象可提供自身對應的表名,主鍵集合、索引集合以及表字段和DAO對象成員的映射關係,這些信息足以應對簡單的CRUD操作。

  注意getFieldMap方法返回的Map對象中,其Value類型是SqlData,關於SqlData的說明見下文。

四 字段類型轉換

  映射關係解決之後,還有一項非常重要的事情,如何將SQL字段值賦予對象成員。

  首先我們無法保證SQL返回的類型一定和成員類型匹配,比如說表字段爲varchar類型,那麼SQL返回的應該是String類型,而成員類型很可能是int。

  第二點,ORM在將記錄轉化爲JAVA對象的時候,必然要對成員賦值,這就涉及到如何訪問對象成員的問題,我相信大家的第一反應一定是反射,然而不通過反射我們一樣可以做到。

  我們需要賦予DAO對象成員能夠對SQL返回值自動轉化並賦予成員的能力,定義接口SqlData來表示所有的DAO成員:

public interface SqlData {
	Object getData();

	void setSqlData(Object value);
}

  接口定義完畢後,需要對其進行實現,以滿足DAO成員類型的需求,這裏以String作爲樣例供大家參考:

public class SqlString implements SqlData {

	private String value = null;

	public SqlString() {
		this.value = null;
	}

	public SqlString(String value) {
		this.value = value;
	}

	public String getValue() {
		return value;
	}

	public void setValue(String value) {
		this.value = value;
	}

	public Object getData() {
		return this.value;
	}

	public void setSqlData(Object value) {
		if (value instanceof String) {
			setValue((String) value);
		} else {
			setValue(String.valueOf(value));
		}
	}
}

五 數據訪問基類

  目前我們已經賦予了DAO自我描述,以及成員的自動轉換賦值的能力,爲了便於應用的使用,我們還需要在DAO基類中加入常規的CRUD操作,如此的話其定義如下:

public abstract class Dao implements DataAccessDescription {

    public boolean select() throws DataAccessException {
        return false;
    }

    public boolean update() throws DataAccessException {
        return false;
    }

    public boolean insert() throws DataAccessException {
        return false;
    }

    public boolean delete() throws DataAccessException  {
        return false;
    }
}

  注意所有的CRUD方法都可能拋出DataAccessException異常,它的介紹見下文。

六 統一異常處理

  爲了便於異常處理,我們需要定義一個受檢查的異常類型,用以描述所有歸屬與ORM框架的異常:

public class DataAccessException extends Exception {

	private static final long serialVersionUID = 1L;

	public DataAccessException() {
		super();
	}

	public DataAccessException(String message) {
		super(message);
	}

	public DataAccessException(Throwable t) {
		super(t);
	}

	public DataAccessException(String message, Throwable t) {
		super(message, t);
	}

}

七 數據訪問框架

  DAO相關設計基本上結束了,接下來需要處理和數據庫交互的問題了。我們把所有和數據庫交互的功能都封裝在數據訪問框架中,它僅關注向數據庫提交的SQL指令:

public class DataAccessFramework implements ConnectionManager {

    private static final String DATA_SOURCE_TYPE = "type";

    private static class DataAccessFrameworkHolder {
        static DataAccessFramework daf = new DataAccessFramework();
    }

    public static DataAccessFramework getInstance() {
        return DataAccessFrameworkHolder.daf;
    }

    private DataAccessFramework() {

    }

    private DataSource dataSource = null;

    public Connection getConnection() throws DataAccessException {
        try {
            if (dataSource == null) {
                Properties properties = PropertiesUtils.loadProperties("ds.properties");
                dataSource = DataSourceFactory.getDataSource(properties.getProperty(DATA_SOURCE_TYPE), properties);
            }
            return dataSource.getConnection();
        } catch (Exception e) {
            throw new DataAccessException(e);
        }
    }

    public boolean execute(String command) throws DataAccessException {
        return execute(command, null);
    }

    /**
     * 用於提交非查詢類SQL指令
     *
     * @param command SQL指令
     * @param params  預編譯指令參數
     * @return 執行結果
     * @throws DataAccessException
     */
    public boolean execute(String command, Object[] params) throws DataAccessException {
        return execute(command, params, null) > 0 ? true : false;
    }

    /**
     * 用於提交查詢類SQL指令
     *
     * @param command SQL指令
     * @param params  預編譯指令參數
     * @param rets    查詢結果集
     * @return 當SQL指令爲更新刪除等操作時,返回值爲數據庫受影響的行數;當SLQ指令爲查詢語句時返回值爲結果條目數
     * @throws DataAccessException
     */
    public int execute(String command, Object[] params, Object[] rets) throws DataAccessException {
        Connection conn = getConnection();
        if (conn == null) {
            throw new DataAccessException("無法獲取數據庫連接");
        }
        PreparedStatement stmt = null;
        ResultSet rs = null;
        try {
            stmt = conn.prepareStatement(command);
            if (params.length > 0) {
                for (int i = 0; i < params.length; i++) {
                    stmt.setObject(i + 1, params[i]);
                }
            }
            System.out.println(stmt);
            if (rets != null) {
                rs = stmt.executeQuery();
                int count = 0;
                while (rs.next()) {
                    for (int i = 0; i < rets.length; i++) {
                        rets[i] = rs.getObject(i + 1);
                    }
                    ++count;
                }
                return count;
            } else {
                return stmt.executeUpdate();
            }

        } catch (SQLException e) {
            throw new DataAccessException(e);
        } finally {
            DataAccessUtils.close(conn, stmt, rs);
        }
    }
}

  實際上和數據庫進行交互時,無非就三種場景需要考慮:

  1. 直接提交SQL指令
  2. 提交帶有預編譯參數的SQL指令
  3. 提交帶有預編譯參數的SQL查詢指令

  所以數據訪問框架的核心接口就三個execute方法。

  注意數據訪問框架不僅需要解決與數據庫交互的問題,還需要維護數據庫連接,那麼如果採用連接池組件,各類數據源的維護就變得複雜起來,所以在數據訪問框架看來,它只關注DataSource,而DataSource實例的創建則被封裝在DataSourceFactory內部,見下文。

八 數據源創建工廠

  爲了支持不同的數據庫連接池組件,以及各類數據源組件,我們將DataSource對象的創建過程封裝起來,通過配置來應對不同的實現,這裏以常見的連接池組件爲例,示意實現過程如下:

public final class DataSourceFactory {

	private static final String DEFAULT_DATA_SOURCE = "default";
	private static final String C3P0_DATA_SOURCE = "c3p0";
	private static final String DRUID_DATA_SOURCE = "druid";
	private static final String HIKARI_DATA_SOURCE = "hikari";

	private DataSourceFactory() {

	}

	public static DataSource getDataSource(String dataSourceType, Map<?, ?> properties) throws DataAccessException {
		try {
			if (dataSourceType.equals(DEFAULT_DATA_SOURCE)) {
				return getDefaultDataSource(properties);
			} else if (dataSourceType.equals(C3P0_DATA_SOURCE)) {
				return getC3P0DataSource(properties);
			} else if (dataSourceType.equals(DRUID_DATA_SOURCE)) {
				return getDruidDataSource(properties);
			} else if (dataSourceType.equals(HIKARI_DATA_SOURCE)) {
				return getHikareDataSource(properties);
			} else {
				throw new DataAccessException("錯誤的數據源類型");
			}
		} catch (Exception e) {
			throw new DataAccessException("無法獲取數據源");
		}
	}

	private static DataSource getDefaultDataSource(Map<?, ?> properties) {
		// TODO Auto-generated method stub
		return null;
	}

	private static DataSource getC3P0DataSource(Map<?, ?> properties) {
		// TODO Auto-generated method stub
		return null;
	}

	private static DataSource getDruidDataSource(Map<?, ?> properties) throws Exception {
		return DruidDataSourceFactory.createDataSource(properties);
	}

	private static DataSource getHikareDataSource(Map<?, ?> properties) {
		// TODO Auto-generated method stub
		return null;
	}
}

九 SQL指令解析

  現在DAO的映射描述問題,以及數據庫交互問題都解決了,那麼怎麼將DAO對象的信息轉變爲SQL指令呢?我們根據常見的數據訪問場景來封裝一個SQL指令的解析器,由它來將一個DAO對象轉變爲不同訪問操作的SQL指令:

public class SqlCommandParser {

    public static String getSelectCommand(String tableName, String[] queryFields, String[] paramFields) {
        StringBuilder builder = new StringBuilder("SELECT ");

        for (int i = 0; i < queryFields.length; i++) {
            builder.append(queryFields[i]);
            if (i + 1 < queryFields.length) {
                builder.append(",");
            }
        }

        builder.append(" FROM ").append(tableName).append(" WHERE ");

        for (int i = 0; i < paramFields.length; i++) {
            builder.append(paramFields[i]);
            builder.append("=?");
            if (i + 1 < paramFields.length) {
                builder.append(" AND ");
            }
        }

        return builder.toString();
    }

    public static String getInsertCommand(String tableName, String[] fieldNames) {
        StringBuilder builder = new StringBuilder("INSERT INTO " + tableName + " (");
        StringBuilder preparedParams = new StringBuilder();

        for (int i = 0; i < fieldNames.length; i++) {
            builder.append(fieldNames[i]);
            preparedParams.append("?");
            if (i + 1 < fieldNames.length) {
                builder.append(",");
                preparedParams.append(",");
            } else {
                builder.append(") VALUES (");
                builder.append(preparedParams.toString());
                builder.append(")");
            }
        }

        return builder.toString();
    }

    public static void main(String[] args) {
        String sql = getInsertCommand("TEST", new String[]{"field1", "field2", "field3"});
        System.out.println(sql);
    }
}

十 補充DAO接口實現

  現在所有的準備工作都完畢了,但是DAO基類中還有CRUD方法沒有實現,這裏就利用上述基礎設施來實現它,以select和insert爲例:

public abstract class Dao implements DataAccessDescription {

    public boolean select() throws DataAccessException {
        String[] primaryKeyArray = getPrimaryKeyArray();
        if (primaryKeyArray == null || primaryKeyArray.length == 0) {
            throw new DataAccessException("未設置主鍵");
        }

        Map<String, SqlData> fieldMap = getFieldMap();
        if (fieldMap == null || fieldMap.size() == 0) {
            throw new DataAccessException("未設置字段成員映射");
        }

        String command = SqlCommandParser.getSelectCommand(getTableName(),
                getFieldMap().keySet().toArray(new String[getFieldMap().size()]), primaryKeyArray);

        Object[] params = new Object[primaryKeyArray.length];
        for (int i = 0; i < primaryKeyArray.length; i++) {
            params[i] = fieldMap.get(primaryKeyArray[i]).getData();
        }

        return DataAccessFramework.getInstance().execute(command, params, fieldMap.values().toArray()) > 0;
    }

    public boolean update() {
        return false;
    }

    public boolean insert() throws DataAccessException {
        String[] primaryKeyArray = getPrimaryKeyArray();

        if (primaryKeyArray == null || primaryKeyArray.length == 0) {
            throw new DataAccessException("未設置主鍵");
        }

        String[] fieldNameArray = getFieldMap().keySet().toArray(new String[getFieldMap().size()]);
        Object[] fieldValueArray = new Object[getFieldMap().size()];

        for (int i = 0; i < fieldNameArray.length; i++) {
            fieldValueArray[i] = getFieldMap().get(fieldNameArray[i]).getData();
        }

        String command = SqlCommandParser.getInsertCommand(getTableName(), fieldNameArray);

        return DataAccessFramework.getInstance().execute(command, fieldValueArray);
    }

    public boolean delete() {
        return false;
    }
}

十一 測試

  所有開發工作完畢,進入最後的測試階段,首先創建一個名爲user的表,結構如下:

user表結構

  接下來創建一個與之關聯的DAO:

public class User extends Dao {

	private SqlLong id;
	private SqlString uuid;
	private SqlString password;
	private SqlString nickname;
	private SqlString phone;
	private SqlDate creatime;
	private SqlDate updatime;

	public User() {
		this.id = new SqlLong();
		this.uuid = new SqlString();
		this.password = new SqlString();
		this.nickname = new SqlString();
		this.phone = new SqlString();
		this.creatime = new SqlDate();
		this.updatime = new SqlDate();
	}

	public long getId() {
		return id.getValue();
	}

	...//各種getter、setter,省略
	public String getTableName() {
		return "user";
	}

	public String[] getPrimaryKeyArray() {
		return new String[] { "id" };
	}

	public String[] getIndexArray() {
		return null;
	}

	public Map<String, SqlData> getFieldMap() {
		Map<String, SqlData> map = new HashMap<String, SqlData>();
		map.put("id", id);
		map.put("uuid", uuid);
		map.put("password", password);
		map.put("nickname", nickname);
		map.put("phone", phone);
		map.put("creatime", creatime);
		map.put("updatime", updatime);
		return map;
	}
}

  注意咯,所有的DAO成員類型都是SqlData,而且在構造時進行初始化,並且每個DAO類型都需要實現DataAccessDescription接口,以提供映射描述信息。

  再配置一下數據源相關的文件信息,在根目錄下加入db.properties:

type=druid
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/***?useUnicode=true&amp;characterEncoding=utf-8&useSSL=false
username=root
password=***

  通過type指定了數據庫連接池採用Druid,其他的則爲數據庫訪問的必要信息。

  最後編寫測試案例,我們創建一個user對象,將它插入到數據庫中:

public class TestAccessUser {

    public static void main(String[] args) {
        User user = new User();
        user.setUuid(UUID.randomUUID().toString().replaceAll("-", ""));
        user.setNickname("test");
        user.setPassword("123");
        user.setPhone("13275186860");

        try {
            if (user.insert()) {
                System.out.println("插入成功");
            } else {
                System.out.println("插入失敗");
            }
        } catch (DataAccessException e) {
            e.printStackTrace();
        }
    }
}

  編譯執行結果如下:

user插入結果

  到數據庫覈對一下記錄是否正確:

數據庫記錄

  在管理工具上的確看到了這條記錄,還可以編寫查詢案例來測試一下是否能從DB查到數據並轉換爲User對象,這裏不再詳細說明了。

十二 其他問題

  一個非常簡易的ORM框架就設計完畢了,這裏沒有用到任何表的配置文件,也沒有用到任何反射技術,可以說執行效率絕對是可以保證的。

  但是這樣一個簡陋的ORM框架顯然不能支撐一個大規模項目的需求,它還存在許多需要改進的地方:

  1. 支持批量的CRUD
  2. 完善的事務管理機制
  3. 支持部分字段的查詢
  4. 支持自定義過濾條件的查詢
  5. ……

  整個設計並沒有花費我太多的時間,但是如何解決目前普遍存在的配置文件和註解問題困擾了我很久,其實任何設計都不需要多麼複雜的技術實現,只要思路是正確的,用最簡單易懂的代碼實現就是完美的。

  希望這篇介紹能給朋友們一些設計思路吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章