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. ……

  整个设计并没有花费我太多的时间,但是如何解决目前普遍存在的配置文件和注解问题困扰了我很久,其实任何设计都不需要多么复杂的技术实现,只要思路是正确的,用最简单易懂的代码实现就是完美的。

  希望这篇介绍能给朋友们一些设计思路吧。

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