美文网首页Java实战Java
自定义数据库持久层框架kd-jdbc

自定义数据库持久层框架kd-jdbc

作者: 不孤独的字符串 | 来源:发表于2021-03-11 14:32 被阅读0次

一、前言

本文参考Mybatis设计思想,自定义了数据库持久层框架kd-jdbc,从jdbc存在问题的角度出发,实现参数映射,sql解析,面向接口编程等功能。

二、JDBC存在的问题

public static void main(String[] args) throws SQLException {
    Connection connection = null;
    PreparedStatement preparedStatement = null;
    ResultSet resultSet = null;
    try {
        Class.forName("com.mysql.jdbc.Driver");
        connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/kd-jdbc", "root", "123456");
        String sql = "select * from user where username = ?";
        preparedStatement = connection.prepareStatement(sql);
        preparedStatement.setString(1, "xian");
        resultSet = preparedStatement.executeQuery();
        ArrayList<User> users = new ArrayList<User>();
        while (resultSet.next()) {
            User user = new User();
            int id = resultSet.getInt("id");
            String username = resultSet.getString("username");
            user.setId(id);
            user.setUsername(username);
            users.add(user);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }finally {
        resultSet.close();
        preparedStatement.close();
        connection.close();
    }
}

以上是一段常见的原生jdbc查询的代码,可以发现实现是很不优雅的,主要存在的问题有:
1. 频繁的创建数据库连接,造成系统资源浪费
2. SQL语句硬编码在代码中,与业务代码耦合
3. PreparedStatement参数绑定的过程存在硬编码,对于一些where动态条件,硬编码不利于代码维护
4. 对SQL结果集映射存在硬编码

三、问题的解决思路

对于以上的问题,数据库频繁创建、释放资源可以通过接入线程池处理;硬编码问题,可以将需要频繁改动的代码抽离成xml文件,使用dom4j去解析节点;而对于结果集映射,则可以通过对象的反射和内省去完成。同时,要求使用端提供核心配置文件SqlMapConfig.xml和Mapper.xml,而对于框架则需要读取相应的配置信息,完成数据源的构建,SQL语句的解析以及结果集的映射。

四、代码实现

4.1 使用端核心配置文件

SqlMapConfig.xml

<configuration>
    <!--数据库配置信息-->
    <dataSource>
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="username" value="root"></property>
        <property name="jdbcUrl" value="jdbc:mysql:///kd-jdbc"></property>
        <property name="password" value="123456"></property>
    </dataSource>
    <!--存放mapper.xml的全路径-->
    <mapper resource="UserMapper.xml"></mapper>
</configuration>

UserMapper.xml

<mapper namespace="com.keduw.dao.UserDao">
    <!-- 查询所有用户 -->
    <select id="findAll" resultType="com.keduw.pojo.User" >
        select * from user
    </select>
    <!-- 根据用户id查询信息 -->
    <select id="findById" resultType="com.keduw.pojo.User" paramterType="com.keduw.pojo.User">
        select * from user where id = #{id}
    </select>
</mapper>

4.2 封装数据源信息和查询语句

定义一个Configuration用于保存解析出来的数据源信息和执行语句,执行语句保存在mappedStatementMap中,每一条执行语句就代表一个MappedStatement,而id则是由namespace.id组成。

public class Configuration {
    private DataSource dataSource;
    private Map<String, MappedStatement> mappedStatementMap = new HashMap<>();    
    /** 省略get,set方法 **/
}

封装一个工厂类SqlSessionFactoryBuilder用于数据解析和生成SqlSession,主要使用dom4j解析配置文件,将解析出来的内容封装到Configuration中,同时创建SqlSessionFactory对象,生产SqlSession,拿到SqlSession后就可以为所欲为了。


public class SqlSessionFactoryBuilder {

    public SqlSessionFactory build(InputStream in) throws DocumentException, PropertyVetoException {
        // 使用dom4j解析配置文件
        XmlConfigBuilder xmlConfigBuilder = new XmlConfigBuilder();
        Configuration configuration = xmlConfigBuilder.parseConfig(in);
        // 创建sqlSessionFactory对象
        DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration);
        return defaultSqlSessionFactory;
    }
}

4.3 引入数据库连接池

在解析SqlMapConfig.xml过程中,引入数据库连接池创建数据源信息,将数据源交由数据库连接池管理,解决数据库连接频繁创建连接和释放资源的问题。


public Configuration parseConfig(InputStream inputStream) throws DocumentException, PropertyVetoException {
    Document document = new SAXReader().read(inputStream);
    Element rootElement = document.getRootElement();
    List<Element> list = rootElement.selectNodes("//property");
    Properties properties = new Properties();
    for (Element element : list) {
        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 />标签
    List<Element> mapperList = rootElement.selectNodes("//mapper");
    for (Element element : mapperList) {
        String resource = element.attributeValue("resource");
        InputStream resourceAsStream = Resource.getResourceAsStream("/" + resource);
        XmlMapperBuilder xmlMapperBuilder = new XmlMapperBuilder(configuration);
        xmlMapperBuilder.parse(resourceAsStream);
    }
    return configuration;
}

4.4 SQL解析

构建一个XmlMapperBuilder实现对mapper中各种数据节点的解析,将解析出来的数据封装在MappedStatement中,MappedStatement主要包含id标识、返回值类型、参数值类型以及SQL语句。

public class MappedStatement {
    //id标识
    private String id;
    //返回值类型
    private String resultType;
    //参数值类型
    private String paramterType;
    //sql语句
    private String sql;

    /** 省略一些set,get方法 **/
}

这里做了一些简化,实际上Mybatis会将SQL语句解析成一个个的Node节点,因为要支持一些动态查询,不过我们这里出于简单考虑,就忽略动态查询,直接用String去保存。

public void parse(InputStream inputStream) throws DocumentException {
    Document document = new SAXReader().read(inputStream);
    Element rootElement = document.getRootElement();
    String namespace = rootElement.attributeValue("namespace");

    // 解析所有的<select />标签
    List<Element> list = rootElement.selectNodes("//select");
    for (Element element : list) {
        String id = element.attributeValue("id");
        String resultType = element.attributeValue("resultType");
        String paramterType = element.attributeValue("paramterType");
        String sqlText = element.getTextTrim();
        MappedStatement mappedStatement = new MappedStatement();
        mappedStatement.setId(id);
        mappedStatement.setResultType(resultType);
        mappedStatement.setParamterType(paramterType);
        mappedStatement.setSql(sqlText);
        String key = namespace + "." + id;
        configuration.getMappedStatementMap().put(key,mappedStatement);
    }
}

4.5 查询,封装结果集

基于模板方法设计模式,统一定义查询接口Executor.query(),定义一个简单查询执行器实现具体的查询方法,完成对变量#{}的解析工作,设置参数,执行SQL语句,同时封装返回结果集。具体实现如下:

public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception {
    //注册驱动, 获取连接
    Connection connection = configuration.getDataSource().getConnection();
    //获取sql语句
    String sql = mappedStatement.getSql();
    BoundSql boundSql = getBoundSql(sql);
    PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());
    //设置参数
    String paramterType = mappedStatement.getParamterType();
    Class<?> clazz = getClassType(paramterType);
    List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
    for (int i = 0; i < parameterMappingList.size(); i++) {
        ParameterMapping parameterMapping = parameterMappingList.get(i);
        String content = parameterMapping.getContent();
        //反射获取当前对应的属性
        Field declaredField = clazz.getDeclaredField(content);
        declaredField.setAccessible(true);
        Object o = declaredField.get(params[0]);
        preparedStatement.setObject(i+1, o);
    }

    //执行sql
    ResultSet resultSet = preparedStatement.executeQuery();
    String resultType = mappedStatement.getResultType();
    Class<?> resultTypeClass = getClassType(resultType);

    //封装返回结果集
    ArrayList<Object> objects = new ArrayList<>();
    while (resultSet.next()){
        Object o = resultTypeClass.newInstance();
        //获取元数据
        ResultSetMetaData metaData = resultSet.getMetaData();
        for (int i = 1; i <= metaData.getColumnCount(); i++) {
            //字段名
            String columnName = metaData.getColumnName(i);
            //字段的值
            Object value = resultSet.getObject(columnName);
            //使用内省,根据数据库表和实体的对应关系,完成封装
            PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultTypeClass);
            //得到属性的写方法,为属性赋值
            Method writeMethod = propertyDescriptor.getWriteMethod();
            writeMethod.invoke(o,value);
        }
        objects.add(o);
    }
    return (List<E>) objects;
}

使用方式:

public <E> List<E> selectList(String statementId, Object... params) throws Exception {
    SimpleExecutor simpleExecutor = new SimpleExecutor();
    MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
    List<Object> list = simpleExecutor.query(configuration, mappedStatement, params);

    return (List<E>) list;
}

4.6 面向接口编程

上面的使用方式其实还比较麻烦,可以基于动态代理实现面向接口编程。要求定义的类和方法要跟xml中声明的一样,那样才能生成statementId,获取对应的SQL语句。

/**
 * 使用JDK动态代理来为Dao接口生成代理对象,并返回
 * @param mapperClass
 * @param <T>
 * @return
 */
@Override
public <T> T getMapper(Class<?> mapperClass) {
    Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            String methodName = method.getName();
            String className = method.getDeclaringClass().getName();
            String statementId = className + "." + methodName;
            // 获取被调用方法的返回值类型
            Type genericReturnType = method.getGenericReturnType();
            // 判断是否进行了泛型类型参数化,有则说明返回是集合
            if(genericReturnType instanceof ParameterizedType){
                List<Object> objects = selectList(statementId, args);
                return objects;
            }
            return selectOne(statementId,args);
        }
    });
    return (T) proxyInstance;
}

使用方式:

UserDao userDao = sqlSession.getMapper(UserDao.class);
List<User> users = userDao.findAll();
for (User info : users) {
    System.out.println(info);
}

运行效果:

运行效果

五、结尾

到此,就基本实现了我们自定义的数据库持久层框架,整体是参考Mybatis的设计思想,对完整代码感兴趣的可以关注公众号"不孤独的字符串",私信获取!

相关文章

  • 自定义数据库持久层框架kd-jdbc

    一、前言 本文参考Mybatis设计思想,自定义了数据库持久层框架kd-jdbc,从jdbc存在问题的角度出发,实...

  • 自定义持久层框架 -- 功能优化

    一、自定义持久层框架问题分析 Dao层在使用自定义持久层框架的时候,存在代码重复,整个操作的过程模板重复(加载配置...

  • 自定义持久层框架思路

    使用端:(项目):引入自定义持久层框架的jar包 自定义持久层框架本身:(工程):本质就是对JDBC代码进行了封装

  • 手写Mybatis

    JDBC问题分析 自定义持久层框架设计思路 使用者(项目):引入自定义持久层框架的jar包 提供两部分配置信息数据...

  • 持久层框架(一)自定义持久层框架

    文章内容来源:拉勾教育Java高薪训练营(侵权联系删除) 1 JDBC 存在的问题分析 JDBC 问题总结:原始...

  • 自定义持久层框架

    1.自定义持久层框架思路分析 JDBC问题分析:1.数据库配置信息存在硬编码问题。2.频繁创建释放数据库链接3.s...

  • 自定义持久层框架

    源码地址[https://gitee.com/cwhenshuo/IPersistence] 前言:其实之前学习m...

  • springboot集成elasticsearch地理位置

    项目简介:框架:springboot数据库:mysql持久层:jpa搜索引擎:elasticsearch 1 地理...

  • Hibernate(01)

    首先认识Hibernate框架是ORM关系映射框架, 工作在持久(dao)层,用对象的方式操作sql数据库 优点:...

  • 持久层框架设计实现及Mybatis源码分析

    自定义持久层框架 本质是对JDBC代码封装 加載配置文件 创建 javaBean○ Configuration 类...

网友评论

    本文标题:自定义数据库持久层框架kd-jdbc

    本文链接:https://www.haomeiwen.com/subject/gianqltx.html