zoukankan      html  css  js  c++  java
  • 通过源码理解手写简单版本MyBatis框架(十)

    一、需求分析

    1.1项目需求

    通过原始的JDBC代码来操作数据库非常的麻烦,里面存在着太多的重复代码和低下的开发效率,针对这种情况需要提供一个更加高效的持久层框架。

    1.2 核心功能

    首先来看下JDBC操作查询的代码。

    public class JdbcTest {
    
        public static void main(String[] args) {
            new JdbcTest().queryUser();
             new JdbcTest().addUser();
        }
    
        /**
         *
         * 通过JDBC查询用户信息
         */
        public void queryUser(){
            Connection conn = null;
            Statement stmt = null;
            User user = new User();
            try {
                // 注册 JDBC 驱动
                // Class.forName("com.mysql.cj.jdbc.Driver");
                // 打开连接
                conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8&serverTimezone=UTC", "root", "root");
                // 执行查询
                stmt = conn.createStatement();
                String sql = "SELECT id,user_name,real_name,password,age,d_id from t_user where id = 1";
                ResultSet rs = stmt.executeQuery(sql);
                // 获取结果集
                while (rs.next()) {
                    Integer id = rs.getInt("id");
                    String userName = rs.getString("user_name");
                    String realName = rs.getString("real_name");
                    String password = rs.getString("password");
                    Integer did = rs.getInt("d_id");
                    user.setId(id);
                    user.setUserName(userName);
                    user.setRealName(realName);
                    user.setPassword(password);
                    user.setDId(did);
    
                    System.out.println(user);
                }
                rs.close();
                stmt.close();
                conn.close();
            } catch (SQLException se) {
                se.printStackTrace();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    if (stmt != null) stmt.close();
                } catch (SQLException se2) {
                }
                try {
                    if (conn != null) conn.close();
                } catch (SQLException se) {
                    se.printStackTrace();
                }
            }
        }
    
        /**
         * 通过JDBC实现添加用户信息的操作
         */
        public void addUser(){
            Connection conn = null;
            Statement stmt = null;
            try {
                // 注册 JDBC 驱动
                // Class.forName("com.mysql.cj.jdbc.Driver");
                // 打开连接
                conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8&serverTimezone=UTC", "root", "root");
                // 执行查询
                stmt = conn.createStatement();
                String sql = "INSERT INTO T_USER(user_name,real_name,password,age,d_id)values('ww','王五','111',22,1001)";
                int i = stmt.executeUpdate(sql);
                System.out.println("影响的行数:" + i);
                stmt.close();
                conn.close();
            } catch (SQLException se) {
                se.printStackTrace();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    if (stmt != null) stmt.close();
                } catch (SQLException se2) {
                }
                try {
                    if (conn != null) conn.close();
                } catch (SQLException se) {
                    se.printStackTrace();
                }
            }
        }
    }

    通过上面的代码,可以发现问题还是比较多的。

    1.2.1 资源管理

    它需要实现对连接资源的自动管理,也就是把创建Connection、创建Statement、关闭Connection、关闭Statement这些操作封装到底层的对象中,不需要在应用层手动调用。

    rs.close();
    stmt.close();
    conn.close();

    1.2.2 SQL语句

    在代码中我们直接将SQL语句和业务代码写在了一起,耦合性太高了,我们需要把SQL语句抽离出来实现集中管理,开发人员不用在业务代码里面写SQL语句

    1.2.3 结果集映射

    在上面的代码中我们需要根据字段取出值,然后把值设置到对应对象的属性中,这个操作也是很繁琐的,所以我们也希望框架能够自动帮助我们实现结果集的转换,也就是我们指定了映射规则之后,这个框架会自动帮我们把ResultSet映射成实体类对象。

                while (rs.next()) {
                    Integer id = rs.getInt("id");
                    String userName = rs.getString("user_name");
                    String realName = rs.getString("real_name");
                    String password = rs.getString("password");
                    Integer did = rs.getInt("d_id");
                    user.setId(id);
                    user.setUserName(userName);
                    user.setRealName(realName);
                    user.setPassword(password);
                    user.setDId(did);
    
                    System.out.println(user);
                }

    1.2.4 对外API

    实现了上面的功能以后,这个框架需要提供一个API来给我们操作数据库,这里面定义了对数据库的操作的常用的方法。

    1.3 功能分解

    项目的需求我们也已经清楚了,那么我们应该要怎么来解决这些问题呢?,我们先来分析下需要哪些核心对象

    1.3.1 核心对象

    1、存放参数和结果映射关系、存放SQL语句,我们需要定义一个配置类;
    2、执行对数据库的操作,处理参数和结果集的映射,创建和释放资源,需要定义一个执行器;
    3、有了这个执行器以后,我们不能直接调用它,而是定义一个给应用层使用的API,它可以根据SQL的id找到SQL语句,交给执行器执行;
    4、如果由用户直接使用id查找SQL语句太麻烦了,干脆把存放SQL的命名空间定义成一个接口,把SQL的id定义成方法,这样只要调用接口方法就可以找到要执行的SQL。刚好动态代理可以实现这个功能。这个时候需要引入一个代理类。
    核心对象有了,第二个:分析一下这个框架操作数据库的主要流程,先从单条查询入手。

    1.3.2 操作流程

    1、定义配置类对象Configuration。里面要存放SQL语句,还有查询方法和结果映射的关系。
    2、定义应用层的API SqlSession。在SqlSession里面封装增删改查和操作事务的方法(selectOne())。
    3、如果直接把Statement ID传给SqlSession去执行SQL,会出现硬编码,所以决定把SQL语句的标识设计成一个接口+方法名(Mapper接口),调用接口的方法就能找到SQL语句。
    4、这个需要代理模式实现,所以要创建一个实现了InvocationHandler的触发管理类MapperProxy。代理类在Configuration中通过JDK动态代理创建。
    5、有了代理对象之后,调用接口方法,就是调用触发管理器MapperProxy的invoke()方法。
    6、代理对象的invoke()方法调用了SqlSession的selectOne()。
    7、SqlSession只是一个API,还不是真正的SQL执行者,所以接下来会调用执行器Executor的query()方法。
    8、执行器Executor的query()方法里面就是对JDBC底层的Statement的封装,最终实现对数据库的操作,和结果的返回。

    二、代码实现

    2.1 SqlSession

    针对不同用户的请求操作可以通过SqlSession来处理,在SqlSession中可以提供基础的操作API,我定义的名称为GhySqlSession,暂时不需要考虑其他的实现,所以先不用创建接口,直接写类。

    根据刚才总结的流程图,SqlSession需要有一个获取代理对象的方法,那么这个代理对象是从哪里获取到的呢?是从配置类里面获取到的,因为配置类里面有接口和它要产生的代理类的对应关系。
    所以,要先持有一个Configuration对象,叫GhyConfiguration,创建这个类。除了获取代理对象之外,它里面还存储了接口方法(也就是statementId)和SQL语句的绑定关系。

            在SqlSession中定义的对外的API,最后都会调用Executor去操作数据库,所以还要持有一个Executor对象,叫GhyExecutor,也创建它

    public class GhySqlSession {
        private GhyConfiguration configuration;
        private GhyExecutor executor;
    }

    除了这两个属性之外,还要定义SqlSession的行为,也就是它的主要的方法。

    第一个方法是查询方法,selectOne(),由于它可以返回任意类型(List、Map、对象类型),把返回值定义成 T泛型。selectOne()有两个参数,一个是String类型的statementId,我们会根据它找到SQL语句。一个是Object类型的parameter参数(可以是Integer也可以是String等等,任意类型),用来填充SQL里面的占位符。

        /**
         * 对外提供的查询的方法
         * @param <T>
         * @return
         */
        public <T> T selectOne(String statementId,Object parameter){
            String sql=statementId; //先用statementId代替SQL
            
            return executor.query(sql,parameter);
        }

    它会调用Executor的query()方法,所以创建Executor类,传入这两个参数,一样返回一个泛型。Executor里面要传入SQL,但是目前还没拿到,先用statementId代替。

    /**
     * SQL语句的执行器
     */
    public class GhyExecutor {
    
        public <T> T query(String sql ,Object parameter) {
            return null;
        }
    }

    SqlSession的第二个方法是获取代理对象的方法,通过这种方式去避免了statementId的硬编码。在SqlSession中创建一个getMapper()的方法,由于可以返回任意类型的代理类,所以把返回值也定义成泛型 T。我是根据接口类型获取到代理对象的,所以传入参数要用类型Class。

        //获取代理对象
        public <T> T getMapper(Class clazz){
            return configuration.getMapper(clazz );
        }

    2.2 Configuration

    代理对象不是在SqlSession里面获取到的,要进一步调用Configuration的getMapper()方法。返回值需要强转成(T)。

    /**
     * 用来保存相关的配置信息
     */
    
    public class GhyConfiguration {
    
        
        public <T> T getMapper(Class clazz){
            return null;
        }
    
    }

    2.3 MapperProxy

    我们要在Configuration中通过getMapper()方法拿到这个代理对象,必须要有一个实现了InvocationHandler的代理类(触发管理器)。创建它:GhyMapperProxy。实现invoke()方法。

    public class GhyMapperProxy  implements InvocationHandler {
     
        @Override
        public Object invoke(Object proxy,Method method,Object[] args) throws Throwable {
          return null;
        }
    }

    invoke()的实现先留着。MapperProxy已经有了,回到Configuration.getMapper()完成获取代理对象的逻辑。返回代理对象,直接使用JDK的动态代理:第一个参数是类加载器,第二个参数是被代理类实现的接口(这里没有被代理类),第三个参数是H(触发管理器)。把返回结果强转为(T):

        public <T> T getMapper(Class clazz,GhySqlSession sqlSession){
            return (T) Proxy.newProxyInstance(this.getClass().getClassLoader(),
                    new Class[]{clazz},
                    new GhyMapperProxy());
        }

    获取代理类的逻辑已经实现完了,可以在SqlSession中通过getMapper()拿到代理对象了,也就是可以调用invoke()方法了。接下来去完成MapperProxy 的invoke()方法。在MapperProxy的invoke()方法里面又调用了SqlSession的selectOne()方法。一个问题出现了:在MapperProxy里面根本没有SqlSession对象?这两个对象的关系怎么建立起来?MapperProxy怎么拿到一个SqlSession对象?很简单,我们可通过构造函数传入它。先定义一个属性,然后在MapperProxy的构造函数里面赋值

    public class GhyMapperProxy  implements InvocationHandler {
    
        private GhySqlSession sqlSession;
    
        public GhyMapperProxy(GhySqlSession sqlSession) {
            this.sqlSession = sqlSession;
        }
    
        @Override
        public Object invoke(Object proxy,Method method,Object[] args) throws Throwable {
            return null;
        }
    }

    因为修改了代理类的构造函数,这个时候Configuration创建代理类的方法getMapper()也要修改。问题:Configuration也没有SqlSession,没办法传入MapperProxy的构造函数。怎么拿到SqlSession呢?是直接new一个吗?不需要,可以在SqlSession调用它的时候直接把自己传进来(修改的地方:MapperProxy的构造函数添加了sqlSession,getMapper()方法也添加了SqlSession):

    /**
     * 用来保存相关的配置信息
     */
    
    public class GhyConfiguration {
    
       
        public <T> T getMapper(Class clazz,GhySqlSession sqlSession){
            return (T) Proxy.newProxyInstance(this.getClass().getClassLoader(),
                    new Class[]{clazz},
                    new GhyMapperProxy(sqlSession));
        }
    
    }

    那么SqlSession的getMapper()方法也要修改(红色是修改的地方):

        //获取代理对象
        public <T> T getMapper(Class clazz){
            return configuration.getMapper(clazz,this);
        }

    现在在MapperProxy里面已经就可以拿到SqlSession对象了,在invoke()方法里面会调用SqlSession的selectOne()方法。我们继续来完成invoke()方法。selectOne()方法有两个参数, statementId和paramater,这两个怎么拿到呢?statementId其实就是接口的全路径+方法名,中间加一个英文的点。paramater可以从方法参数中拿到(args[0])。因为我们定义的是String,还要把拿到的Object强转一下。把statementId和parameter传给SqlSession:

    public class GhyMapperProxy  implements InvocationHandler {
    
        private GhySqlSession sqlSession;
    
        public GhyMapperProxy(GhySqlSession sqlSession) {
            this.sqlSession = sqlSession;
        }
    
        @Override
        public Object invoke(Object proxy,Method method,Object[] args) throws Throwable {
            String mapperInterface = method.getDeclaringClass().getName();
            String methodName = method.getName();
            String statementId = mapperInterface +"." +methodName;
    
            return sqlSession.selectOne(statementId,args[0]);
        }
    }

    到了sqlSession的selectOne()方法,这里要去调用Executor的query方法,这个时候必须传入SQL语句和parameter(根据statementId获取)。怎么根据StatementId找到我们要执行的SQL语句呢?他们之间的绑定关系我们配置在哪里?为了简便,免去读取文件流和解析XML标签的麻烦,我把SQL语句放在Properties文件里面。在resources目录下创建一个sql.properties文件。key就是接口全路径+方法名称,SQL是我们的查询SQL。参数这里,因为我们要传入一个整数,所以先用一个%d的占位符代替:

    com.ghy.versionsone.mapper.UserMapper.selectOne=select * from t_user where id = %d

    在sqlSession的selectOne()方法里面,我们要根据StatementId获取到SQL,然后传给Executor。这个绑定关系是放在Configuration里面的。怎么快速地解析Properties文件?为了避免重复解析,我们在Configuration创建一个静态属性和静态方法,直接解析sql.properties文件里面的所有KV键值对:

    /**
     * 用来保存相关的配置信息
     */
    
    public class GhyConfiguration {
        // 存储属性文件的信息
        public static final ResourceBundle sqlMappings;
    
        static {
            sqlMappings = ResourceBundle.getBundle("sql");
        }
    
    
        public <T> T getMapper(Class clazz,GhySqlSession sqlSession){
            return (T) Proxy.newProxyInstance(this.getClass().getClassLoader(),
                    new Class[]{clazz},
                    new GhyMapperProxy(sqlSession));
        }
    
    }
    /**
    * 对外提供的查询的方法
    * @param <T>
    * @return
    */
    public <T> T selectOne(String statementId,Object parameter){
    String sql = GhyConfiguration.sqlMappings.getString(statementId);
    // String sql=statementId; //先用statementId代替SQL
    System.out.println ("sql:"+sql);
    if (null !=sql && !"".equals ( sql )){
    return executor.query(sql,parameter);
    }
    return null;
    }

    在SqlSession中,SQL语句已经拿到了,接下来就是Executor类的query()方法,Executor是数据库操作的真正执行者。干脆直接把以前文章中写的JDBC的代码全部复制过来,职责先不用细分。参数用传入的参数替换%d占位符,需要format一下。

    2.4 Executor

    在Executor中我们就可以直接来执行SQL的执行了

    /**
     * SQL语句的执行器
     */
    public class GhyExecutor {
    
        public <T> T query(String sql,Object parameter){
            Connection conn = null;
            Statement stmt = null;
            User user = new User();
    
            try {
                // Class.forName("com.mysql.jdbc.Driver");
    
                // 打开连接
                conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8&serverTimezone=UTC", "root", "root");
    
                // 执行查询
                stmt = conn.createStatement();
                ResultSet rs = stmt.executeQuery(String.format(sql,parameter));
    
                // 获取结果集
                while (rs.next()) {
                    user.setId(rs.getInt("id"));
                    user.setUserName(rs.getString("user_name"));
                    user.setPassword(rs.getString("password"));
                    user.setRealName(rs.getString("real_name"));
                }
                System.out.println(user);
    
                rs.close();
                stmt.close();
                conn.close();
            } catch (SQLException se) {
                se.printStackTrace();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    if (stmt != null) {
                        stmt.close();
                    }
                } catch (SQLException se2) {
                }
                try {
                    if (conn != null) {
                        conn.close();
                    }
                } catch (SQLException se) {
                    se.printStackTrace();
                }
            }
    
            return (T) user;
        }
    }

    到这儿我们就可以来写个测试类来跑下程序了

    public class Test {
    public static void main(String[] args) {
    GhySqlSession sqlSession = new GhySqlSession();
    // sqlSession.selectOne("com.ghy.versionsone.entity.User.selectOne",1);
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    User user = mapper.selectOne(1);
    System.out.println(user);
    }
    }

    三、不足总结

    1、在Executor中,对参数、语句和结果集的处理是耦合的,没有实现职责分离;
    2、参数:没有实现对语句的预编译,只有简单的格式化,效率不高,还存在SQL注入的风险;
    3、语句执行:数据库连接硬编码;
    4、结果集:还只能处理Blog类型,没有实现根据实体类自动映射。


    git源码:https://gitee.com/TongHuaShuShuoWoDeJieJu/ljx-my-baits.git

    这短短的一生我们最终都会失去,不妨大胆一点,爱一个人,攀一座山,追一个梦
  • 相关阅读:
    用Python抓取并分析了1982场英雄联盟数据,教你开局前预测游戏对局胜负!
    初级练手项目——用Python一步一步实现“智能”贪吃蛇!
    这五本Python急速入门必读的书,送给正在学习Python的你!
    探讨2018年最受欢迎的15顶级Python库!
    运用Python制作你心目中的完美女神脸!
    掌握这些Python代码技巧,编程至少快一半!
    解决Protege使用中的一个问题:从已有owl文件建立项目失败(w3china)
    http响应状态码
    云计算二 转帖
    protege中Property Domains and Ranges理解
  • 原文地址:https://www.cnblogs.com/xing1/p/15302370.html
Copyright © 2011-2022 走看看