自定义ORM框架(三)实践篇,ORM框架代码设计开发,执行SQL测试

前言

前面两篇文章介绍了JDBC模式开发持久层的不足之处,并且做了下自定义ORM框架设计步骤的思考,约定了配置文件格式。这篇文章主要介绍如何一步步编写一个简单的类似MyBatis的自定义ORM框架,并且对其功能进行测试。

实现功能:

  1. 解析Mapper.xml配置执行相应SQL
  2. 根据配置文件初始化c3p0连接池
  3. 结果集自动封装
  4. 参数自动解析
  5. 根据Dao对象映射相应的Mapper.xml配置,来通过反射调用Mapper配置里的文件

项目代码地址

代码设计

首先,我们使用c3p0连接池进行数据源连接池的管理,使用dom4j进行配置文件的解析。引入Maven依赖

<dependencies>
    <!-- MySQL连接驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.17</version>
    </dependency>
    <!-- 配置文件解析依赖 -->
    <dependency>
        <groupId>org.dom4j</groupId>
        <artifactId>dom4j</artifactId>
        <version>2.1.1</version>
    </dependency>
    <!-- 数据库连接池 -->
    <dependency>
        <groupId>c3p0</groupId>
        <artifactId>c3p0</artifactId>
        <version>0.9.1.2</version>
    </dependency>
    <dependency>
        <groupId>jaxen</groupId>
        <artifactId>jaxen</artifactId>
        <version>1.1.6</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.10</version>
    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.12</version>
    </dependency>
</dependencies>

第一步,加载配置文件,创建数据源DataSource

  • 配置文件加载器Resource.java:提供getResourceAsStream(String path)方法,传入配置文件路径,把配置文件以字节输入流的形式加载到内存中。

    
    /**
     * 配置文件加载器
     */
    public class Resource {
        /**
         * 根据配置文件的路径,把配置文件加载成字节输入流,放到内存中
         * @param path  配置文件路径
         * @return  字节输入流
         */
        public static InputStream getResourceAsStream(String path){
            InputStream resourceAsStream = Resource.class.getClassLoader().getResourceAsStream(path);
            return resourceAsStream;
        }
    }
    
  • 创建配置文件容器对象Configuration.javaMappedStatement.java,用来存储配置信息

    
    public class Configuration {
        /**
         * 数据库连接
         */
        private DataSource dataSource;
        /**
         * SQL映射配置存储
         * key:由xxxMapper里的namespace+.+具体SQL映射配置的id组成
         * value:SQL映射配置
         */
        private Map<String, MappedStatement> mappedStatementMap=new HashMap<String, MappedStatement>();
    
        public DataSource getDataSource() {
            return dataSource;
        }
    
        public void setDataSource(DataSource dataSource) {
            this.dataSource = dataSource;
        }
    
        public Map<String, MappedStatement> getMappedStatementMap() {
            return mappedStatementMap;
        }
    
        public void setMappedStatementMap(Map<String, MappedStatement> mappedStatementMap) {
            this.mappedStatementMap = mappedStatementMap;
        }
    }
    
    /**
     * 存储xxxMapper.xml内的信息,每个SQL语句对应一个MappedStatement对象
     */
    public class MappedStatement {
        private String id;
        private Class<?> paramType;
        private Class<?> resultType;
        private String sql;
        private String queryType;
    
        public String getId() {
            return id;
        }
    
        public void setId(String id) {
            this.id = id;
        }
    
        public Class<?> getParamType() {
            return paramType;
        }
    
        public void setParamType(Class<?> paramType) {
            this.paramType = paramType;
        }
    
        public Class<?> getResultType() {
            return resultType;
        }
    
        public void setResultType(Class<?> resultType) {
            this.resultType = resultType;
        }
    
        public String getSql() {
            return sql;
        }
    
        public void setSql(String sql) {
            this.sql = sql;
        }
    
        public String getQueryType() {
            return queryType;
        }
    
        public void setQueryType(String queryType) {
            this.queryType = queryType;
        }
    }
    
    
  • 配置文件装配器XMLConfigBuilder.javaXMLMapperBuilder.java:提供parseConfig(InputStream xmlInputStream)方法,解析配置文件字节输入流,并根据配置内容创建数据库连接,装填Mapper配置内容到容器对象中

    
    /**
     * 配置文件装配器
     */
    public class XMLConfigBuilder {
        private final Configuration configuration;
    
        public XMLConfigBuilder() {
            this.configuration = new Configuration();
        }
    
        /**
         * 将配置文件字节输入流,解析封装成Configuration对象
         * @param xmlInputStream 配置文件字节输入流
         * @return  {@link Configuration} 核心配置文件容器对象
         */
        public Configuration parseConfig(InputStream xmlInputStream) throws DocumentException, PropertyVetoException, ClassNotFoundException {
            // 把字节输入流使用dom4j解析成Document对象
            Document document = new SAXReader().read(xmlInputStream);
            // 拿到配置文件的根元素对象
            Element rootElement = document.getRootElement();
            // 取出配置文件内<dataSource>所有的<property>标签元素
            Element dataSourceElement = rootElement.element("dataSource");
            List<Element> dataSourceElements = dataSourceElement.elements();
            Properties configProperties=new Properties();
            for (Element property : dataSourceElements) {
                configProperties.setProperty(property.attribute("name").getStringValue(),property.getText());
            }
            // 使用c3p0连接池创建数据源对象
            ComboPooledDataSource dataSource = new ComboPooledDataSource("c3p0");;
            dataSource.setDriverClass(configProperties.getProperty("driverClass"));
            dataSource.setJdbcUrl(configProperties.getProperty("url"));
            dataSource.setUser(configProperties.getProperty("user"));
            dataSource.setPassword(configProperties.getProperty("pass"));
            this.configuration.setDataSource(dataSource);
            // 解析xxxMapper.xml
            Element mappersElement = rootElement.element("mappers");
            List<Element> mapperElementList = mappersElement.elements();
            // 取出每一个配置文件的Mapper文件的路径
            for (Element mapperElement : mapperElementList) {
                // 获取Mapper配置文件的字节输入流
                InputStream mapperInputStream = Resource.getResourceAsStream(mapperElement.attribute("xml").getStringValue());
                // 解析Mapper配置文件
                XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(this.configuration);
                xmlMapperBuilder.parse(mapperInputStream);
            }
            return this.configuration;
        }
    }
    
    public class XMLMapperBuilder {
        private final Configuration configuration;
    
        public XMLMapperBuilder(Configuration configuration) {
            this.configuration=configuration;
        }
        public void parse(InputStream mapperInputStream) throws DocumentException, ClassNotFoundException {
            // 转换Document
            Document mapperDocument = new SAXReader().read(mapperInputStream);
            // 获取根元素
            Element rootElement = mapperDocument.getRootElement();
            // 获取namespace
            String namespace = rootElement.attribute("namespace").getStringValue();
            // 获取每一个子级别元素(SQL映射配置元素)
            List<Element> mappedStatementElement = rootElement.elements();
            for (Element element : mappedStatementElement) {
                String id = element.attribute("id").getStringValue();
                String resultType = element.attribute("resultType")!=null?element.attribute("resultType").getStringValue():null;
                String paramType = element.attribute("paramType")!=null?element.attribute("paramType").getStringValue():null;
                String sql = element.getText();
                String sqlQueryType = element.getName();
                MappedStatement mappedStatement = new MappedStatement();
                mappedStatement.setId(id);
                if (paramType!=null) {
                    mappedStatement.setParamType(Class.forName(paramType));
                }
                if (resultType!=null) {
                    mappedStatement.setResultType(Class.forName(resultType));
                }
                mappedStatement.setSql(sql);
                mappedStatement.setQueryType(sqlQueryType);
                this.configuration.getMappedStatementMap().put(namespace+"."+id,mappedStatement);
            }
        }
    }
    

第二步,创建数据库连接会话工厂,创建数据库连接会话

会话工厂接口

会话工厂接口SqlSessionFactory.java及实现类,负责创建数据库会话对象

public interface SqlSessionFactory {
    SqlSession openSession();
}
public class DefaultSqlSessionFactory implements SqlSessionFactory{
    private Configuration configuration;

    private DefaultSqlSessionFactory(){}
    public DefaultSqlSessionFactory(Configuration configuration){
        this.configuration=configuration;
    }

    public SqlSession openSession() {
        return new DefaultSqlSession(this.configuration);
    }
}
数据库连接会话

数据库连接会话对象SqlSessionDefaultSqlSession,负责实际SQL的调用执行

public interface SqlSession {
     /**
      * 调用S执行QL 返回单条结果
      */
     <E> E execute(String statementId)throws Exception;
     <E> E execute(String statementId,Object... params)throws Exception;
     /**
      * 调用S执行QL 返回多条结果
      */
     <E> List<E> executeReturnList(String statementId)throws Exception;
     <E> List<E> executeReturnList(String statementId,Object... params)throws Exception;
     <M> M getMapper(Class<M> mClass);
}

public class DefaultSqlSession implements SqlSession{
    private Configuration configuration;

    public DefaultSqlSession(Configuration configuration) {
        this.configuration = configuration;
    }

    public <E> E execute(String statementId) throws Exception {
        List<E> list = this.executeReturnList(statementId);
        if (list == null||list.isEmpty()){
            return null;
        }else if (list.size()>1){
            throw new RuntimeException("查询结果超过1条");
        }else {
            return list.get(0);
        }
    }

    public <E> E execute(String statementId, Object... params) throws Exception {
        List<E> list = this.executeReturnList(statementId, params);
        if (list == null||list.isEmpty()){
            return null;
        }else if (list.size()>1){
            throw new RuntimeException("查询结果超过1条");
        }else {
            return list.get(0);
        }
    }

    public <E> List<E> executeReturnList(String statementId) throws Exception {
        return this.executeReturnList(statementId,null);
    }

    public <E> List<E> executeReturnList(String statementId, Object... params) throws Exception {
        Executor executor = new SimpleExecutor();
        return executor.query(this.configuration, configuration.getMappedStatementMap().get(statementId), params);
    }
}
SQL执行器

SQL执行器Executor.javaSimpleExecutor.java,负责具体SQL的执行,参数解析和返回值的封装

public interface Executor {
    <E>List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception;
}
public class SimpleExecutor implements Executor{

    private Connection connection = null;

    public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception {
        // 获取数据库连接
        connection = configuration.getDataSource().getConnection();
        // 取得原生SQL语句
        String sql = mappedStatement.getSql();
        // 处理SQL#{}参数
        BoundSQL boundSQL = getBoundSQL(sql);
        // 获取要执行的SQL语句
        String finalSql = boundSQL.getSqlText();
        // 获取入参类型
        Class<?> paramType = mappedStatement.getParamType();
        //获取预编译PreparedStatement对象,预处理SQL并放入参数
        PreparedStatement preparedStatement =connection.prepareStatement(finalSql);
        List<ParameterMapping> parameterMappingList =boundSQL.getParameterMappingList();
        // 遍历处理入参
        for (int i = 0; i < parameterMappingList.size(); i++) {
            ParameterMapping parameterMapping = parameterMappingList.get(i);
            String name = parameterMapping.getContent();
            // 反射
            Field declaredField = paramType.getDeclaredField(name);
            declaredField.setAccessible(true);
            // 参数值
            Object o = declaredField.get(params[0]);
            // 给占位符赋值
            preparedStatement.setObject(i+1,o);
        }
        // 执行SQL
        ResultSet resultSet = preparedStatement.executeQuery();
        Class<?> resultType = mappedStatement.getResultType();
        // 结果集封装
        ArrayList<E> results = new ArrayList<E>();
        while (resultSet.next()) {
            ResultSetMetaData metaData = resultSet.getMetaData();
            E o = (E) resultType.newInstance();
            int columnCount = metaData.getColumnCount();
            for (int i = 1; i <= columnCount; i++) {
                // 属性名
                String columnName = metaData.getColumnName(i);
                // 属性值
                Object value = resultSet.getObject(columnName);
                // 创建读写描述器,为属性生成读写方法
                PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultType);
                Method writeMethod = propertyDescriptor.getWriteMethod();
                writeMethod.invoke(o,value);
            }
            results.add(o);
        }
        // 如果涉及数据的修改变动,需要提交事务
        if (mappedStatement.getQueryType().equals("insert")||mappedStatement.getQueryType().equals("update")||mappedStatement.getQueryType().equals("delete")) {
            connection.commit();
        }
        // 返回结果
        return results;
    }

    /**
     * 完成对#{}的解析工作:1.将#{}使用?进行代替,2.解析出#{}里面的值进行存储
     * @param sql   配置文件内原生SQL
     */
    private BoundSQL getBoundSQL(String sql) {
        // 标记处理类:用来配合通用标记解析器GenericTokenParser类完成对配置文件等的解析工作,其中TokenHandler主要完成处理
        ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
        // GenericTokenParser:通用标记解析器,完成代码片中的占位符的解析,然后根据给定的标记处理器TokenHandler来进行表达式的处理
        // 三个参数:分别为openToken(开始标记)、closeToken(结束标记)、handler(标记处理器)
        GenericTokenParser genericTokenParser = new GenericTokenParser("#{","}",parameterMappingTokenHandler);
        // 解析出来的SQL
        String parse = genericTokenParser.parse(sql);
        //#{}里面解析出来的参数名称
        List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();
        return new BoundSQL(parse, parameterMappings);
    }
}

测试功能

  1. 数据库创建sm_user表

    CREATE TABLE `sm_user` (
      `id` varchar(11) NOT NULL,
      `userName` varchar(20) DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
  2. 创建实体对象SmUser与sm_user表对应

    package com.yxh.www.testorm.entity;
    
    public class SmUser {
        private String id;
        private String userName;
    
        public String getId() {
            return id;
        }
    
        public void setId(String id) {
            this.id = id;
        }
    
        public String getUserName() {
            return userName;
        }
    
        public void setUserName(String userName) {
            this.userName = userName;
        }
    
        @Override
        public String toString() {
            return "SmUser{" +
                    "id='" + id + '\'' +
                    ", userName='" + userName + '\'' +
                    '}';
        }
    }
    
  3. 新建项目,配置数据库配置、Mapper配置

    dbConfig.xml内容:

    <!-- 配置根标签 -->
    <configuration>
        <!-- 定义数据库连接信息配置 -->
        <dataSource>
            <!-- 数据库连接驱动 -->
            <property name="driverClass">com.mysql.jdbc.Driver</property>
            <!-- 数据库连接地址 -->
            <property name="url">jdbc:mysql://127.0.0.1:3306/simple-orm</property>
            <!-- 数据库连接用户 -->
            <property name="user">root</property>
            <!-- 数据库连接密码 -->
            <property name="pass">yxh123..</property>
        </dataSource>
        <!-- 定义SQL语句映射相关信息 -->
        <mappers>
            <mapper xml="SmUserMapper.xml" classPath="com.yxh.www.testorm.dao.SmUserDao"/>
        </mappers>
    </configuration>
    

    SmUserMapper.xml内容:

    <mapper namespace="SmUserDao">
        <!-- 查询列表 -->
        <select id="listSmUser" resultType="com.yxh.www.testorm.entity.SmUser" paramType="com.yxh.www.testorm.entity.SmUser">
            select * from sm_user where id=#{id}
        </select>
        <!-- 查询单个用户 需要传参 -->
        <select id="getSmUser"  paramType="com.yxh.www.testorm.entity.SmUser" resultType="com.yxh.www.testorm.entity.SmUser">
            select * from sm_user where id=#{id} and user_name=#{userName}
        </select>
        <!-- 增加用户 -->
        <insert id="insertSmUser" paramType="com.yxh.www.testorm.entity.SmUser">
            insert into sm_user(id,user_name) values(#{id},#{userName})
        </insert>
        <!-- 修改用户 -->
        <update id="updateUser" paramType="com.yxh.www.testorm.entity.SmUser">
            update sm_user set user_name=#{userName} where id=#{id}
        </update>
        <!--  删除用户  -->
        <delete id="deleteSmUserById" paramType="java.lang.String">
            delete from sm_user where id=#{id}
        </delete>
    </mapper>
    
  4. 编写测试代码,调用listSmUser

     // 参数
    SmUser param=new SmUser();
    param.setId("2");
    // 获取配置信息
    InputStream resourceAsStream = Resource.getResourceAsStream("dbConfig.xml");
    // 创建数据库连接会话对象
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
    SqlSession sqlSession = sqlSessionFactory.openSession();
    // 调用SQL方法
    List<SmUser> smUserList = sqlSession.selectList("smUser.listSmUser", SmUser.class,param);
    for (SmUser smUser : smUserList) {
        System.out.println(smUser.toString());
    }
    

    控制台输出:

    image-20200825150837160

优化框架

发现在调用实际SQL指定statementId时还是存在硬编码问题,不够方便,考虑建立Mapper.xml配置对应的Java接口对象,通过Java反射来解决这个问题

SqlSession.java添加方法:getMapper(Class<?> mClass),用来获取代理生成对象

SqlSession接口增加方法

<M> M getMapper(Class<M> mClass);

DefaultSqlSession实现类新增方法:


    public <M> M getMapper(Class<M> mClass) {
        M m=(M) Proxy.newProxyInstance(mClass.getClassLoader(), new Class[]{mClass}, new InvocationHandler() {
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                // 获取方法名
                String methodName = method.getName();
                String className = method.getDeclaringClass().getSimpleName();
                // statementId
                String statementId=className+"."+methodName;
                Type genericReturnType=method.getGenericReturnType();
                // 判断是否实现泛型类型参数化
                if(genericReturnType instanceof ParameterizedType){
                    return executeReturnList(statementId,args);
                }
                return execute(statementId,Object.class,args);
            }
        });
        return m;
    }

建立SmUserDao接口,与Mapper.xml配置之间映射

public interface SmUserDao {
    List<SmUser> listSmUser(SmUser smUser);
}

修改测试代码,使用代理调用并执行SQL

// 参数
SmUser param=new SmUser();
param.setId("2");
// 获取配置信息
InputStream resourceAsStream = Resource.getResourceAsStream("dbConfig.xml");
// 创建数据库连接会话对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
// 使用代理方式调用SQL
SmUserDao smUserDao=sqlSession.getMapper(SmUserDao.class);
List<SmUser> smUserList = smUserDao.listSmUser(param);
for (SmUser smUser : smUserList) {
    System.out.println(smUser.toString());
}

控制台输出:

image-20200825151553560

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

清晨先生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值