这是我参与2022首次更文挑战的第25天,活动详情查看:2022首次更文挑战
MyBatis
根据各种资料的介绍,都可以知道Mybatis实际上是对于JDBC的一个封装框架。
因此,在使用MyBatis之前,可以先来看看原生的JDBC是如何访问数据库的,这对于后面具体封装的细节能够有所帮助。
使用原生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做的优化都包括哪些。