首页 文章详情

自己动手实现一个ORM框架

Java技术迷 | 129 2022-05-27 04:29 0 0 0
UniSMS (合一短信)

点击关注公众号,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.xml的路径 -->
    <mapper resource="UserMapper.xml"/>
</configuration>

对该配置文件进行解析后,我们可以将这些数据封装成一个Java实体,该实体包含了所有的配置信息,由于全局配置文件中可能含有多个Mapper文件的配置,所以将其封装成一个Map集合:

Map<String, MapperStatement>

集合的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配置文件中的namespaceid,我们将其拼接起来作为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<String, MapperStatement> mappedStatementMap = new HashMap<>();
}

那么现在的关键就是对全局配置文件的解析了,我们提供一个类XmlConfigBuilder,该类的parseConfig方法可以将输入流转换为Configuration对象,实现如下:

public Configuration parseConfig(InputStream inputStream) throws DocumentException, PropertyVetoException {
        Document document = new SAXReader().read(inputStream);
        // <configuration>
        Element rootElement = document.getRootElement();
        // 全局查找<property>标签
        List<Element> 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<Element> 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);
        // 得到根标签<mapper>
        Element rootElement = document.getRootElement();
        String namespace = rootElement.attributeValue("namespace");
        // 得到所有<select>标签
        StringBuilder sb = new StringBuilder();
        List<Element> 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 <T> List<T> selectList(String statementId, Object... params) throws Exception{
        Executor executor = new SimpleExecutor();
        List<Object> objectList = executor.query(configuration, configuration.getMappedStatementMap().get(statementId), params);
        return (List<T>) objectList;
    }
}

在该对象中,我们需要实现查询操作,同样地,我们借助一个SimpleExecutor类来实现具体的查询,这里只需调用一下即可,想象一下,查询操作需要哪些参数。首先configuration一定需要,里面封装的是数据源和MapperStatement信息,其次,需要具体的MapperStatement对象,当然了,MapperStatement也可以在方法内部取,最后是查询的一些参数信息,这样就能够实现查询了。

实现查询

public class SimpleExecutor implements Executor {

    @Override
    public <T> List<T> 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<ParameterMapping> 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<Object> list = new ArrayList<>();
        // 封装返回结果集
        while (resultSet.next()) {
            // 获取实体实例
            Object o = resultClass.newInstance();
            // 获取元数据
            ResultSetMetaData metaData = resultSet.getMetaData();
            for (int i = 1; i <= metaData.getColumnCount(); ++i) {
                // 获取字段名
                String columnName = metaData.getColumnName(i);
                // 获取字段值
                Object columnValue = resultSet.getObject(columnName);
                // 内省设置值,映射表和实体的关系
                PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultClass);
                propertyDescriptor.getWriteMethod().invoke(o, columnValue);
            }
            list.add(o);
        }
        return (List<T>) list;
    }

    private Class<?> getClassType(String parameterType) throws ClassNotFoundException {
        if (parameterType != null) {
            return Class.forName(parameterType);
        }
        return null;
    }

    /**
     * 解析sql
     *
     * @param sql
     * @return
     */

    private ReplaceSql getReplaceSql(String sql) {
        // 配合标记解析器完成占位符的解析
        ParameterMappingTokenHandler tokenHandler = new ParameterMappingTokenHandler();
        GenericTokenParser genericTokenParser = new GenericTokenParser("#{""}", tokenHandler);
        // sql解析
        String parseSql = genericTokenParser.parse(sql);
        // 解析#{}中的参数名称
        List<ParameterMapping> parameterMappingList = tokenHandler.getParameterMappings();
        return new ReplaceSql(parseSql, parameterMappingList);
    }
}

整个框架的核心部分就是这个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.wwj</groupId>
  <artifactId>My-MyBatis</artifactId>
  <version>1.0-SNAPSHOT</version>
</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.xml的路径 -->
    <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<User> 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内核模块

点分享

点收藏

点点赞

点在看

good-icon 0
favorite-icon 0
收藏
回复数量: 0
    暂无评论~~
    Ctrl+Enter