[MyBatis] 0 - 前置知识:JDBC

这是我参与2022首次更文挑战的第25天,活动详情查看:2022首次更文挑战

MyBatis

根据各种资料的介绍,都可以知道Mybatis实际上是对于JDBC的一个封装框架。

因此,在使用MyBatis之前,可以先来看看原生的JDBC是如何访问数据库的,这对于后面具体封装的细节能够有所帮助。

参考:深入浅出MyBatis:JDBC和MyBatis介绍 - 掘金 (juejin.cn)

使用原生JDBC访问数据库

这里随便往上找一找代码都可以,贴一下一些tips。

首先,java原生是不带JDBC连接包的,因此需要找到一个JDBC连接器的包来进行使用,否则

Clss.forName("驱动全限定名")

这里就会报错。

这里我使用的是maven,依赖可以在

Maven Repository: Search/Browse/Explore (mvnrepository.com)

这里找。

我这里使用的maven依赖坐标是:

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.28</version>
    </dependency>
复制代码

代码也很好写,代码如下:

public static void main(String[] args) throws ClassNotFoundException, SQLException {
    //加载驱动
    Class.forName("com.mysql.cj.jdbc.Driver");
    //连接信息
    String url = "链接地址";
    String username = "账号";
    String password = "密码";
    //连接数据库
    Connection connection = DriverManager.getConnection(url,username,password);
    //创建数据库操作对象
    Statement statement = connection.createStatement();
    //执行sql语句,返回结果
    String sql = "select * from Employee";
    ResultSet resultSet = statement.executeQuery(sql);
    while (resultSet.next()){
        System.out.println(resultSet.getInt("id"));
    }

    resultSet.close();
    statement.close();
    connection.close();
}
复制代码

这个驱动是新的版本,老版本的com.mysql.jdbc.Driver已经不再建议使用。

这里可以看到,使用这种方式进行数据库连接的话,步骤如下:

graph LR
加载驱动-->连接数据库-->创建数据库操作对象-->使用数据库操作对象执行SQL-->从数据库操作对象获取结果集

打断点可以看到,这里和数据库实际操作的类,都是由驱动的包来实现的,方便驱动向前扩展。

例如,这里的实现就是由com.mysql.cj.jdbc.Driver提供的。

加载驱动

一个驱动的加载,需要被添加到DriverManager的可知范围内。

例如:上文中提到的com.mysql.cj.jdbc.Driver,在类初始化中就需要进行注册,代码如下:

static {
    try {
        DriverManager.registerDriver(new Driver());
    } catch (SQLException var1) {
        throw new RuntimeException("Can't register driver!");
    }
}
复制代码

而这里的类初始化,是根据我们手写的代码中的Class.forName()进行触发的。

这里的registerDriver实现也简单,就是把注册的驱动,丢到静态类维护的**线程安全的list(CopyOnWriteList)**里。

连接数据库

在这一行代码中

Connection connection = DriverManager.getConnection(url,username,password);

根据上面的加载驱动,也不难猜测:实际上就是将对应的URL,身份信息,拿现在已维护的驱动挨个试一试,成功了就返回成功的连接。

这里的核心代码就是下面的这段:

//前面防止类没初始化再初始化类
//这里的registeredDrivers就是通过上面加载驱动塞进去的
for(DriverInfo aDriver : registeredDrivers) {
    // If the caller does not have permission to load the driver then
    // skip it.
    if(isDriverAllowed(aDriver.driver, callerCL)) {
        try {
            println("    trying " + aDriver.driver.getClass().getName());
            Connection con = aDriver.driver.connect(url, info);
            if (con != null) {
                // Success!
                println("getConnection returning " + aDriver.driver.getClass().getName());
                return (con);
            }
        } catch (SQLException ex) {
            if (reason == null) {
                reason = ex;
            }
        }

    } else {
        println("    skipping: " + aDriver.getClass().getName());
    }

}
复制代码

再往下就是这个连接包的实现了,往下挖还是比较复杂的,毕竟涉及到原生的连接方式等等,总之:

  • 这里会对host进行一个判断,比如是负载均衡啊,分片啊,单例啊等等的,随后根据这些类型进行初始化。

这部分内容可以看这个博主的:Mysql Connector/J 源码分析(综述)_yyb_gz的专栏-CSDN博客

这一步结束之后,获取的就是一个连接了,到这里算是把管子怼到数据库上了。

获取执行对象

管子怼上去了,那么按照正常流程,得找个地方能跑SQL,那么这里的Statement,就属于这个跑SQL的东西。

这里需要指出的是:

  • 上面的Connection是有实际的物理意义的:指代从本地服务器目标数据库的连接

  • 而这里的statement,更大程度上是针对于执行的SQL窗口的一个抽象。

    • 在本文涉及的包里,这个Statement实际上是对于查询所需的一系列参数的设置集合,包括连接、session,以及一些和执行相关的外围配套设施的设置。

执行SQL、获取结果集

对于每个不同的连接包,这些实现都是不同的。

在本文所指的连接包中,实际上在执行SQL的时候,是会通过Connection内置的对象进行Sync上锁的:

  • public java.sql.ResultSet executeQuery(String sql) throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
        //...
        }
        }
    复制代码

而具体执行SQL,代码是这里:

this.results = ((NativeSession) locallyScopedConn.getSession()).execSQL(this, sql, this.maxRows, null, createStreamingResultSet(),
        getResultSetFactory(), cachedMetaData, false);
复制代码

因此,这里对于Connection的上锁,本质上是对于session的上锁。

这里通过这个也可以知道:

  • 以这种方式进行连接的基于同一个Connection创建的Statement,彼此的运行是串行的。

而这里的results就是这个execSQL的结果了。需要注意的是,这里可以指定反参的格式,这个反参格式在连接驱动包中被称为ColumnDefinition;如果这个值不是空的,则会按照这个定义进行初始化。

在Connection层(具体的类是ConnectionImpl)中,对于相同的SQL,如果这个**ColumnDefinition(在这一层的实现是CachedResultSetMetaData)**不为null,则会在Connection层保存下来。

总结

大致看了一下原生JDBC的一次执行操作的方式,显然有以下的不便:

  • 如果需要配置多种驱动,每一次都要来一个Class.forName()来初始化驱动吗?

  • 连接数据库使用的Connection,需要建立Session,比较耗时;并且,这个东西还是同步的,那么对于对相同URL发起连接的Connection,可以使用池化技术,来减少频繁建立Connection的成本。

    • 这里需要考虑的问题:由于并行的每一次SQL执行都是加锁的,那么要如何设计才能减少最大使用的Connection使用数?
  • SQL在这个驱动包中实际上是一个外部参数,但是如果是很复杂的SQL,在java代码里手写也很不方便。

  • 这个ResultSet的实现还是过于宽泛了,如果不知道对应SQL具体查出来哪一些column,实际上是很难从代码层面看出来查询出来的结果的。

目前看到的可改进的,包括:

  • 这个驱动的可否使用配置化来指定?

  • 使用池化技术来优化连接

  • 对于SQL,有没有方便一些的方式来做映射?

  • ResultSet在语义上过于模糊,可否对于每个SQL都做特殊的映射?

带着这些问题,后续看MyBatis代码的时候,也能够更清楚地分析出MyBatis做的优化都包括哪些。

Guess you like

Origin juejin.im/post/7068200773372346399