自己动手实现一个ORM框架
点击关注公众号,Java干货及时送达

作者 | 汪伟俊
出品 | Java技术迷(ID:JavaFans1024)
引言
本篇文章我们来自己动手实现一个ORM框架,我们先来看一下传统的JDBC代码:
static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";  
static final String JDBC_URL = "jdbc:mysql:///user";
static final String USER_NAME = "root";
static final String PASS_WORD = "123456";
public static void main(String[] args) {
    Class.forName(JDBC_DRIVER);
    Connection conn = DriverManager.getConnection(JDBC_URL, USER_NAME, PASS_WORD);
    Statement stmt = conn.createStatement();
    String sql = "SELECT * FROM user";
    ResultSet rs = stmt.executeQuery(sql);
    while(rs.next()){
        int id  = rs.getInt("id");
        int age = rs.getInt("age");
        System.out.println("ID: " + id);
        System.out.println("Age: " + age);
    }
    rs.close();
}
以上代码通过JDBC实现了对数据表的查询操作,不过这里有一些明显的问题,对于数据库的配置信息是硬编码在代码中的,想要修改配置信息还得来修改代码,我们可以将其抽取成一个配置文件;对于sql的编写也是硬编码在代码中,也可以考虑将其抽取出去;然后是对结果集的封装,每次都需要通过循环解析结果集也非常麻烦。综上所述,我们借鉴MyBatis来实现一个自己的ORM框架。
ORM框架整体架构
我们先来梳理一下框架的整体架构,首先我们需要解析一下配置文件,正如MyBatis框架那样,我们需要使用到两种配置文件,一个是框架的全局配置文件,一个是Mapper配置文件,定义格式如下:
<configuration>
configuration>
<mapper>
mapper>
那么首先框架的第一步就是读取配置文件,全局配置文件中应该包含数据源配置信息和Mapper配置文件所在位置,如下所示:
<configuration>
    
    <dataSource>
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
        <property name="jdbcUrl" value="jdbc:mysql:///user"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
    dataSource>
    
    <mapper resource="UserMapper.xml"/>
configuration>
对该配置文件进行解析后,我们可以将这些数据封装成一个Java实体,该实体包含了所有的配置信息,由于全局配置文件中可能含有多个Mapper文件的配置,所以将其封装成一个Map集合:
Map
集合的key为String类型,value为MapperStatement类型,MapperStatement是对Mapper配置文件的一个封装:
<mapper namespace="user">
    <select id="selectList" resultType="com.wwj.pojo.User">
        select * from e_user
    select>
mapper>
这里需要注意一点,框架会将整个项目中的Mapper配置文件都封装成一个MapperStatement并保存到Map中,这就需要对每个MapperStatement进行区分,区分的关键就是Mapper配置文件中的namespace和id,我们将其拼接起来作为statementId。到这里,配置文件的解析就完成了,然后我们提供对应的查询方法,该查询方法的作用是对sql语句进行解析并调用JDBC查询数据库,通过内省封装结果集。以上是框架的一个整体思路,大家可能现在还没有理解到,没关系,接下来是对实现过程的一个详细概述。
解析配置文件
新建一个类Resources,该类负责将一个文件转换成输入流:
public class Resources {
    /**
     * 根据配置文件的路径将配置文件加载成字节输入流
     *
     * @param path
     * @return
     */
    public static InputStream getResourceAsStream(String path) {
        return Resources.class.getClassLoader().getResourceAsStream(path);
    }
}
接下来我们需要一个SqlSessionFactoryBuilder对象,该对象会提供一个build方法来生成SqlSessionFactory:
public class SqlSessionFactoryBuilder {
    public SqlSessionFactory build(InputStream inputStream) throws DocumentException, PropertyVetoException {
        // 使用dom4j解析配置文件,将解析出来的内容封装到Configuration中
        XmlConfigBuilder builder = new XmlConfigBuilder();
        Configuration configuration = builder.parseConfig(inputStream);
        // 创建SqlSessionFactory对象
        SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory(configuration);
        return sqlSessionFactory;
    }
}
SqlSessionFactory是一个接口,我们创建它的默认实现类DefaultSqlSessionFactory,该类需要传入一个Configuration类型对象,这个Configuration就是对全局配置文件的一个封装:
public class Configuration {
    private DataSource dataSource;
    /**
     *  key:statementId
     *  value:封装好的MapperStatement对象
     */
    private Map mappedStatementMap = new HashMap<>();
}
 那么现在的关键就是对全局配置文件的解析了,我们提供一个类XmlConfigBuilder,该类的parseConfig方法可以将输入流转换为Configuration对象,实现如下:
public Configuration parseConfig(InputStream inputStream) throws DocumentException, PropertyVetoException {
        Document document = new SAXReader().read(inputStream);
        // 
        Element rootElement = document.getRootElement();
        // 全局查找标签 
        List propertyList = rootElement.selectNodes("//property");
        Properties properties = new Properties();
        propertyList.forEach(element -> {
            // 获取到标签中的name和value属性
            String name = element.attributeValue("name");
            String value = element.attributeValue("value");
            properties.setProperty(name, value);
        });
        // 创建数据源
        ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
        comboPooledDataSource.setDriverClass(properties.getProperty("driverClass"));
        comboPooledDataSource.setJdbcUrl(properties.getProperty("jdbcUrl"));
        comboPooledDataSource.setUser(properties.getProperty("username"));
        comboPooledDataSource.setPassword(properties.getProperty("password"));
        configuration.setDataSource(comboPooledDataSource);
        // 解析mapper.xml文件
        List mapperList = rootElement.selectNodes("//mapper");
        for (Element element : mapperList) {
            String mapperPath = element.attributeValue("resource");
            InputStream mapperAsStream = Resources.getResourceAsStream(mapperPath);
            XmlMapperBuilder xmlMapperBuilder = new XmlMapperBuilder(configuration);
            xmlMapperBuilder.parse(mapperAsStream);
        }
        return configuration;
    }
  借助dom4j可以很容易地实现解析,将每个标签中的属性和属性值读取出来,进行对应的封装即可,对于Mapper配置文件的解析也是如此,通过resource属性可以得到Mapper文件位置,然后将其转为输入流并解析:
public class XmlMapperBuilder {
    private Configuration configuration;
    public XmlMapperBuilder(Configuration configuration) {
        this.configuration = configuration;
    }
    public void parse(InputStream inputStream) throws DocumentException {
        Document document = new SAXReader().read(inputStream);
        // 得到根标签
        Element rootElement = document.getRootElement();
        String namespace = rootElement.attributeValue("namespace");
        // 得到所有
        StringBuilder sb = new StringBuilder();
        List selectList = rootElement.selectNodes("//select");
        selectList.forEach(element -> {
            String id = element.attributeValue("id");
            String resultType = element.attributeValue("resultType");
            String parameterType = element.attributeValue("parameterType");
            String sql = element.getTextTrim();
            // 封装MapperStatement对象
            MapperStatement mapperStatement = new MapperStatement();
            mapperStatement.setId(id);
            mapperStatement.setResultType(resultType);
            mapperStatement.setParameterType(parameterType);
            mapperStatement.setSql(sql);
            // 将MapperStatement对象保存到Configuration中
            sb.append(namespace).append(".").append(id);
            configuration.getMappedStatementMap().put(sb.toString(), mapperStatement);
            sb.setLength(0);
        });
    }
}
 同样地读取每个配置的属性名和属性值,对于MappedStatementMap的封装,其Map的key为namespace + id。
执行查询
读取完配置文件之后,我们就得到了一个DefaultSqlSessionFactory对象,该对象需要提供一个openSession方法来获得SqlSession对象:
public class DefaultSqlSessionFactory implements SqlSessionFactory {
    private Configuration configuration;
    public DefaultSqlSessionFactory(Configuration configuration) {
        this.configuration = configuration;
    }
    @Override
    public SqlSession openSession() {
        return new DefaultSqlSession(configuration);
    }
}
我们返回SqlSession接口的默认实现DefaultSqlSession:
public class DefaultSqlSession implements SqlSession {
    private Configuration configuration;
    public DefaultSqlSession(Configuration configuration) {
        this.configuration = configuration;
    }
    @Override
    public  List selectList(String statementId, Object... params) throws Exception {
        Executor executor = new SimpleExecutor();
        List 在该对象中,我们需要实现查询操作,同样地,我们借助一个SimpleExecutor类来实现具体的查询,这里只需调用一下即可,想象一下,查询操作需要哪些参数。首先configuration一定需要,里面封装的是数据源和MapperStatement信息,其次,需要具体的MapperStatement对象,当然了,MapperStatement也可以在方法内部取,最后是查询的一些参数信息,这样就能够实现查询了。
实现查询
public class SimpleExecutor implements Executor {
    @Override
    public  List query(Configuration configuration, MapperStatement mapperStatement, Object... params) throws Exception  {
        Connection connection = configuration.getDataSource().getConnection();
        // select * from e_user where id = #{id} and name = #{name}
        String sql = mapperStatement.getSql();
        // 将sql中的 #{} 替换为 ?
        ReplaceSql replaceSql = getReplaceSql(sql);
        PreparedStatement preparedStatement = connection.prepareStatement(replaceSql.getSql());
        // 获取到参数的全限定类名
        String parameterType = mapperStatement.getParameterType();
        Class> parameterClass = getClassType(parameterType);
        // 设置参数
        List parameterMappingList = replaceSql.getParameterMappingList();
        for (int i = 0; i < parameterMappingList.size(); i++) {
            ParameterMapping parameterMapping = parameterMappingList.get(i);
            String content = parameterMapping.getContent();
            // 反射设置值
            Field field = parameterClass.getDeclaredField(content);
            field.setAccessible(true);
            Object o = field.get(params[0]);
            preparedStatement.setObject(i + 1, o);
        }
        // 执行sql
        ResultSet resultSet = preparedStatement.executeQuery();
        String resultType = mapperStatement.getResultType();
        Class> resultClass = getClassType(resultType);
        List  整个框架的核心部分就是这个SimpleExecutor类了,我们知道,JDBC中的preparedStatement类执行的sql是以?作为占位符的,所以我们把#{}替换成?,并将#{id}里面的属性名取出来,这就是查询的一些参数信息。将参数类型和返回类型均通过反射内省技术进行值的封装,即可得到最终结果。
测试一下
通过以上步骤便实现了一个简单的ORM框架,项目结构如下:
com.wwj.config
        -ReplaceSql
        -XmlConfigBuilder
        -XmlMapperBuilder
com.wwj.io
        -Resources
com.wwj.pojo
        -Configuration
        -MapperStatement
com.wwj.sqlSession
        -Executor
        -SqlSession
        -SqlSessionFactory
        -SqlSessionFactoryBuilder
com.wwj.sqlSession.impl
                   -DefaultSqlSession
                   -DefaultSqlSessionFactory
                   -SimpleExecutor
com.wwj.utils
        -GenericTokenParser
        -ParameterMapping
        -ParameterMappingTokenHandler
        -TokenHandler     
接下来我们测试一下,首先创建一个项目,引入自定义框架:
<dependency>
  <groupId>com.wwjgroupId>
  <artifactId>My-MyBatisartifactId>
  <version>1.0-SNAPSHOTversion>
dependency>
编写全局配置文件:
<configuration>
    
    <dataSource>
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
        <property name="jdbcUrl" value="jdbc:mysql:///test"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
    dataSource>
    
    <mapper resource="UserMapper.xml"/>
configuration>
编写UserMapper配置文件:
<mapper namespace="user">
    <select id="selectList" resultType="com.wwj.pojo.User">
        select * from e_user
    select>
mapper>
编写测试代码:
@Test
public void test() throws Exception {
    InputStream inputStream = Resources.getResourceAsStream("sqlMapperConfig.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    SqlSession sqlSession = sqlSessionFactory.openSession();
    User user = new User();
    user.setId(1);
    user.setName("lisi");
    List userList = sqlSession.selectList("user.selectList");
    System.out.println(userList);
}
 执行结果:
[User{id=1, name='lisi', password='admin'}]
往 期 推 荐 
1、我在产品上线前不小心删除了7 TB的视频 2、程序员最硬大佬,你绝对想不到!!! 3、IntelliJ IDEA快捷键大全 + 动图演示 4、打不过就加入?微软强推“亲儿子”上位,还是中国特供版 5、活久见!NVIDIA正式开源其Linux GPU内核模块 点分享
点收藏
点点赞
点在看





