持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第 24 ,点击查看活动详情
在上期课程中,我们完成了 Executor 接口创建以及 query() 方法的定义和创建 Executor 接口的实现类 SimpleExecutor,并重新了 query() 方法,但是 query() 方法具体实现没有完成。在本期课程中,完成 query() 方法的具体实现。
这个 query() 方法该如何进行实现呢?根据之前的设计思路分析,我们介绍过 query() 方法执行的就是原生 JDBC 代码。同时,也介绍过自定义持久层框架的本质就是对原生 JDBC 代码进行了封装,所以底层执行的还是原生 JDBC 代码。
这个原生 JDBC 代码究竟在哪个方法中进行执行呢?其实就是要在 query() 方法中进行执行。所以基于这个思路,我们就将要在 query() 方法中来编写原生 JDBC 代码。
那对于原生 JDBC 代码的编写相对较固定,具体步骤如下:
注册驱动,获取连接
我们要去注册驱动,注册驱动获取链接。在 query() 方法中要获取到一个数据库链接。想一下我们可以怎么获取呢?
在我们之前初始化的时候,已经使用 dom4j 技术对 sql-mapper-config.xml 配置文件进行了解析。在解析的过程中,我们去获取到了配置文件中的数据库的配置信息,并借助数据库的配置信息创建了一个 ComboPooledDataSource 连接池对象,并把连接池对象存入到了Configuration 类中的 dataSource 属性上。我们想获取一个数据库链接,直接从 Configuration 对象中获取。
获取 SQL 语句
SQL 语句该如何获取呢,它封装在哪儿呢?
SQL 语句封装在 MapperStatement 个对象中。我们在使用 dom4j 技术对配置文件进行解析时,我们不仅解析了 sql-mapper-config.xml 核心配置文件,同时还解析了 mapper.xml 映射配置文件。在解析 mapper.xml 映射配置文件的时候,把每一个 select 标签里面的内容都去解析,并将封装成了一个又一个的 MapperStatement 对象。要获取到标签里面的 SQL 语句。我们只需要去获取 MapperStatement 对象中 sql 属性即可。因为把所有的 SQL 语句封装在 MapperStatement 对象中 sql 属性中。
通知 mapperStatement.getSql() 可以获取到 SQL 语句,在 xml 映射配置文件,我们 SQL 语句格式是: SELECT * FROM t_user WHERE id={#id} and username={#username}。
现在思考一个问题,JDBC 能够识别 SELECT * FROM t_user WHERE id={#id} and username={#username} 这样的 SQL 语句吗?
其实是肯定不能,因为在这一条搜狗语句中,包含着 # 号和 {} 号。 # 号和 {} 号我们自定义的一个占位符。对于 JDBC 来说,它并不认识这个占位符,它所能识别的只能 ? 号形式的占位符。
所以针对这种情况,在获取到 SQL 语句之后呢,要完成任务就是转换 SQL 语句,需要把它转换成 SELECT * FROM t_user WHERE id=? and username=? 这种形式的 SQL 语句,只有带着 ? 号占位符 SQL 才能被 JDBC 所识别。同时在转换的过程中,还需要对 # 号和 {} 号里面的值进行解析存储。
我们井号大括号里面,这是有值的啊。大家注意有这个i d 啊,有这个user name,我以它来举例啊,有这个i d,有这个user name。我们现在还需要把这个值解析出来并存储啊。ok 那说到这儿啊,
介绍到这里,可能有的人会有疑惑,目前我们获取到 SQL 语句,并且还要进行转换,这样做很麻烦。在 mapper.xml 映射配置文件中编写 SQL 语句时,为什么不直接的把占用符写成 ? 号。
在这里再强调一下,写成 # 号和 {} 号这种形式,主要是为了实现一种需求。这种需求就是当传递过来的参数类型是实体类型时,我们要根据 # 号和 {} 号里面的这个参数名称,来找到实体参数里面对应的一个属性值,来完成对占位符的一个赋值。
比如,在上面的 SQL 语句中 id=#{id},它就是要去找到 User 实体类中 id 的属性值来给这个占位符进行赋值。其他也是同样。使用 # 号和 {} 号的目的就是实现这样的效果。使用 ? 号是无法实现,因此才选用 # 号和 {} 号,同时 # 号和 {} 号中的值不能随便写,它应该与传递过来实体面的这个属性名保持一致,这样才能找到对应的一个属性。
接着,我们完成 SQL 语句转换。首先,我们来创建 BoundSql 类,目前还没有这个类,先来创建它。在这个 BoundSql 类中要存放转换过后的 SQL 语句,以解析过程中,对 # 号和 {} 号解析出来里面的参数名称。我们要对这些内容进行个存储。至于 BoundSql 类中具体的属性值后面再来完成。
实现 getBoundSql 方法
直接,我们来完成 getBoundSql 方法代码编写,在该方法中,需要完成两个功能,第一,将 # 号和 {} 号使用 ? 号代替。第二,解析出 # 号和 {} 号里面的值进行存储。
要实现这两个功能,在 getBoundSql() 方法中如何进行操作呢?此时就要去借助一些工具类,具体工具类如下图所示:
在 GenericTokenParser 类中定义了 3 个变量,其中,openToken 表示开始标记,closeToken 表示结束标记,handler 表示标记处理器。同时在这个类中,仅提供了一个有参构造。也就意味着每一次创建 GenericTokenParser 的时候,都需要为它定义出来的 3 变量进行赋值。
这个类对象我们拿到了,接下来调用它的 parse() 方法完成对占位符它的解析转换。
parse() 方法主要的作用就是去实现了对配置文件脚本等片段中占位符的解析处理工作,最终返回需要的数据。在这个 parse() 方法中,实现的代码非常多,其实在它的代码中有很多部分都是来判断传递进来的这个 SQL 语句是否是空值,或者是否包含了开始标签,也就是是否包含了占位符,以及是否包含有转移字符等这样的判断。
这里,我们重点来看一下下面这行代码。
builder.append(handler.handleToken(expression.toString()));
这个 handler 对象是什么呢?其实它就是标记处理类,我们就要调用这个类中的 handleToken() 方法,这个方法其实进行了参数处理。在这个方法中,它去返回了 ? 号作为参数符。
public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return "?";
}
在这个 handleToken() 方法中接收一个参数 content。这个 content 就是参数名称,也就是“#{}”占位符里面的内容,就相当于前面例子中 id 和 username。获取到 “#{}” 占位符里面内容之后,又去调用了 buildParameterMapping() 方法,把接收到的 content 值进行了传递。
在这个 buildParameterMapping()build 方法中,创建了ParameterMapping 对象,把这个参数封装成一个对象,并进行返回。那么返回的这个对象最终被封装到 ParameterMappingTokenHandler 里面的这个 parameterMappings 的这个 List 集合中。
在这个 handleToken() 方法中,返回 ? 号,也就是去进行了占位符的替换。
到目前,SQL 语句转换完成了,
完成 BoundSql
前面,我们已经把 BoundSql 类创建出来,但是,在这个类中没有定义任何的属性。根据 getBoundSql() 方法的分析和实现,在 BoundSql 类中需要的属性与实现的方法。
public class BoundSql {
// 解析之后的 SQL
private String slqText;
private List<ParameterMapping> parameterMappingList= new ArrayList<>();
public BoundSql(String slqText, List<ParameterMapping> parameterMappingList) {
this.slqText = slqText;
this.parameterMappingList = parameterMappingList;
}
//getter 和 setter 略
}
此时我们就可以在 getBoundSql() 方法中调用 BoundSql 中的有参构造方法,到这里,我们把 BoundSql 也封装好。最终把封装好的 BoundSql 对象进行返回。
private BoundSql getBoundSql(String sql) {
//...
BoundSql boundSql = new BoundSql(parseSql,parameterMappings);
return boundSql;
}
获取预处理对象
如何获取 prepareStatement 预处理对象。
PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSlqText());
获取到 prepareStatement 预处理对象之后,接下要完成功能是设置参数,如果 SQL 语句有占位符的话,需要给这些占位符进行赋值。如果把设置参数完成了,下一步执行 SQL 语句