13|JDBC访问框架:如何抽取JDBC模板并隔离数据库
你好,我是郭屹,今天我们继续手写MiniSpring。从这节课开始我们进入MiniSpring一个全新的部分:JdbcTemplate。
到现在为止,我们的MiniSpring已经成了一个相对完整的简易容器,具备了基本的IoC和MVC功能。现在我们就要在这个简易容器的基础之上,继续添加新的特性。首先就是 数据访问的特性,这是任何一个应用系统的基本功能,所以我们先实现它。这之后,我们的MiniSpring就基本落地了,你真的可以以它为框架进行编程了。
我们还是先从标准的JDBC程序开始探讨。
JDBC通用流程
在Java体系中,数据访问的规范是JDBC,也就是Java Database Connectivity,想必你已经熟悉或者至少听说过,一个简单而典型的JDBC程序大致流程是怎样的呢?我们一步步来看,每一步我也会给你放上一两个代码示例帮助你理解。
第一步,加载数据库驱动程序。
1
|
Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");
|
第一步,创建数据库驱动实例。在Java中,我们可以通过直接new Driver()来创建驱动实例,因为JDBC只是提供了访问数据库的API,具体的数据库访问工作是由不同厂商提供的数据库driver来实现的。例如,对于MySQL数据库,我们可以使用com.mysql.cj.jdbc.Driver作为驱动类。
第二步,获取数据库连接。通过调用DriverManager.getConnection(url, user, password)方法,我们可以获取到数据库连接。这里的url是数据库的URL,user是数据库用户名,password是密码。
第三步,执行SQL语句。通过数据库连接对象,我们可以创建Statement或PreparedStatement对象,然后执行SQL语句,如查询、更新等操作。
第四步,处理结果集。对于查询操作,我们需要处理返回的结果集ResultSet。可以通过遍历结果集来获取每一行的数据。
第五步,关闭资源。在完成数据库操作后,我们需要关闭ResultSet、Statement和Connection等资源,以释放数据库连接。
此外,Java的这种设计非常巧妙,它通过桥接模式将应用程序的API与具体厂商的SPI分隔开了,使得它们可以独立进化。这种设计模式的应用效果会在本课程中有所体现。
1
|
con = DriverManager.getConnection("jdbc:sqlserver://localhost:1433;databasename=DEMO;user=testuser;password=test;");
|
数据库连接操作步骤
第一步:建立数据库连接
在这一步中,我们使用getConnection()
方法来建立与数据库的连接。该方法接受三个参数:
第二步:注意连接性能
建立和断开数据库连接是一个耗时的过程。为了提高性能,通常会采用数据库连接池技术,这样可以复用已经建立的连接,减少连接和断开连接的开销。
第三步:创建Statement对象
在成功建立连接之后,我们可以通过Connection
对象来创建Statement
对象,用于执行SQL语句。以下是创建Statement
对象的示例代码:
1
2
|
Statement statement1 = connection.createStatement();
Statement statement2 = connection.createStatement();
|
这样,我们就可以通过Statement
对象来执行SQL查询和更新操作了。
1
|
stmt = con.createStatement(sql);
|
1
|
stmt = con.prepareStatement(sql);
|
Statement是对一条SQL命令的包装。
第四步,使用Statement执行SQL语句,还可以获取返回的结果集ResultSet。
1
|
rs = stmt.executeQuery();
|
第五步,操作ResultSet结果集,形成业务对象,执行业务逻辑。
1
2
3
4
5
6
|
User rtnUser = null;
if (rs.next()) {
rtnUser = new User();
rtnUser.setId(rs.getInt("id"));
rtnUser.setName(rs.getString("name"));
}
|
第六步,回收数据库资源,关闭数据库连接,释放资源。
1
2
3
|
rs.close();
stmt.close();
con.cloase();
|
这个数据访问的套路或者定式,初学Java的程序员都比较熟悉。写多了JDBC程序,我们会发现Java里面访问数据的程序结构都是类似的,不一样的只是具体的SQL语句,然后还有一点就是执行完SQL语句之后,每个业务对结果的处理是不同的。只要稍微用心思考一下,你就会想到应该把它做成一个模板,方便之后使用,自然会去抽取JdbcTemplate。
抽取JdbcTemplate
抽取的基本思路是 动静分离,将固定的套路作为模板定下来,变化的部分让子类重写。这是常用的设计模式,基于这个思路,我们考虑提供一个JdbcTemplate抽象类,实现基本的JDBC访问框架。
以数据查询为例,我们可以在这个框架中,让应用程序员传入具体要执行的SQL语句,并把返回值的处理逻辑设计成一个模板方法让应用程序员去具体实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
package com.minis.jdbc.core;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public abstract class JdbcTemplate {
public JdbcTemplate() {
}
public Object query(String sql) {
Connection con = null;
PreparedStatement stmt = null;
ResultSet rs = null;
Object rtnObj = null;
try {
Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");
con = DriverManager.getConnection("jdbc:sqlserver://localhost:1433;databasename=DEMO;user=sa;password=Sql2016;");
stmt = con.prepareStatement(sql);
rs = stmt.executeQuery();
//调用返回数据处理方法,由程序员自行实现
rtnObj = doInStatement(rs);
}
catch (Exception e) {
e.printStackTrace();
}
finally {
try {
rs.close();
stmt.close();
con.close();
} catch (Exception e) {
}
}
return rtnObj;
}
protected abstract Object doInStatement(ResultSet rs);
}
|
通过上述代码,我们可以看到,query()里面的代码都是模式化的,SQL语句作为参数传进来,最后处理SQL返回数据的业务代码,留给应用程序员自己实现,就是这个模板方法doInStatement()。这样就实现了动静分离。
比如说,我们数据库里有一个数据表User,程序员可以用一个数据访问类UserJdbcImpl进行数据访问,你可以看一下代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
package com.test.service;
import java.sql.ResultSet;
import java.sql.SQLException;
import com.minis.jdbc.core.JdbcTemplate;
import com.test.entity.User;
public class UserJdbcImpl extends JdbcTemplate {
@Override
protected Object doInStatement(ResultSet rs) {
//从jdbc数据集读取数据,并生成对象返回
User rtnUser = null;
try {
if (rs.next()) {
rtnUser = new User();
rtnUser.setId(rs.getInt("id"));
rtnUser.setName(rs.getString("name"));
rtnUser.setBirthday(new java.util.Date(rs.getDate("birthday").getTime()));
} else {
}
} catch (SQLException e) {
e.printStackTrace();
}
return rtnUser;
}
}
|
应用程序员在自己实现的doInStatement()里获得SQL语句的返回数据集并进行业务处理,返回一个业务对象给用户类。
而对外提供服务的UserService用户类就可以简化成下面这样。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
package com.test.service;
import com.minis.jdbc.core.JdbcTemplate;
import com.test.entity.User;
public class UserService {
public User getUserInfo(int userid) {
String sql = "select id, name,birthday from users where id="+userid;
JdbcTemplate jdbcTemplate = new UserJdbcImpl();
User rtnUser = (User)jdbcTemplate.query(sql);
return rtnUser;
}
}
|
在软件开发中,为了简化JDBC程序流程并减少重复代码,我们可以采用模板模式。以下是如何通过模板模式和回调模式来简化JDBC操作的详细步骤和解释。
模板模式简化JDBC操作
用户可以通过创建一个UserJdbcImpl
对象并调用其query()
方法来简化JDBC程序流程。这种方式将JDBC操作的固定步骤抽象化,使得应用程序员只需关注SQL语句的编写和返回数据的处理。
模板模式的优点
-
固化流程:将JDBC程序流程固化,减少出错的可能性。
-
分离变化:分离出变化的部分,使得应用程序员只需管理SQL语句和处理数据。
-
简化实现:减少每个数据表访问都需要手写对应实现类的繁琐工作。
通过Callback模式进一步简化
尽管模板模式已经简化了JDBC操作,但每个实体类仍需手写一个类似UserJdbcImpl
的类。为了避免这种重复劳动,我们可以采用Callback模式。
Callback模式简介
Callback模式允许将一个需要被调用的函数作为参数传递给另一个函数。以下是实现Callback模式的基本步骤:
-
定义回调接口:创建一个回调接口,用于定义需要被调用的方法。
-
实现回调接口:为每个具体的操作实现回调接口,提供具体的SQL语句和数据处理逻辑。
-
传递回调函数:在调用模板方法时,将具体的回调函数作为参数传递。
通过这种方式,我们可以避免为每个实体类编写单独的JdbcImpl实现类,从而进一步简化业务实现类。
1
2
3
|
public interface Callback {
void call();
}
|
有了这个Callback接口,任务类中可以把它作为参数,比如下面的业务任务代码。
1
2
3
4
5
6
|
public class Task {
public void executeWithCallback(Callback callback) {
execute(); //具体的业务逻辑处理
if (callback != null) callback.call();
}
}
|
这个任务类会先执行具体的业务逻辑,然后调用Callback的回调方法。
用户程序如何使用它呢?
1
2
3
4
5
6
7
8
9
|
public static void main(String[] args) {
Task task = new Task();
Callback callback = new Callback() {
public void call() {
System.out.println("callback...");
}
};
task.executeWithCallback(callback);
}
|
先创建一个任务类,然后定义具体的回调方法,最后执行任务的同时将Callback作为参数传进去。这里可以看到,回调接口是一个单一方法的接口,我们可以采用函数式编程进一步简化它。
1
2
3
4
|
public static void main(String[] args) {
Task task = new Task();
task.executeWithCallback(()->{System.out.println("callback;")});
}
|
上面就是Callback模式的实现,我们把一个回调函数作为参数传给了调用者,调用者在执行完自己的任务后调用这个回调函数。
现在我们就按照这个模式改写JdbcTemplate 的query()方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
public Object query(StatementCallback stmtcallback) {
Connection con = null;
Statement stmt = null;
try {
Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");
con = DriverManager.getConnection("jdbc:sqlserver://localhost:1433;databasename=DEMO;user=sa;password=Sql2016;");
stmt = con.createStatement();
return stmtcallback.doInStatement(stmt);
}
catch (Exception e) {
e.printStackTrace();
}
finally {
try {
stmt.close();
con.close();
} catch (Exception e) {
}
}
return null;
}
|
在Java的面向对象编程中,我们经常需要执行数据库操作,并且希望能够以一种更加灵活的方式来处理这些操作的结果。为了达到这一目的,在query()
方法中引入了一个新的参数:StatementCallback
。这个回调机制允许我们在不创建额外子类的情况下,直接通过传递一个实现了StatementCallback
接口的对象来定义如何使用Statement
对象。
aaaaaa StatementCallback接口的设计遵循了回调模式(Callback Pattern),它是一种常见的设计模式,用于定制某个行为的具体实现。在这个场景下,StatementCallback
接口提供了一个方法doInStatement(Statement stmt)
,该方法接收一个Statement
对象作为参数,允许开发者在其中编写自定义逻辑来处理数据库查询或更新。
通过这种方式,我们可以在调用query()
方法时传入不同的StatementCallback
实现,从而避免了为每种数据访问需求都创建一个新的子类去重写doInStatement()
方法的过程。这样不仅简化了代码结构,还提高了代码的复用性和可维护性。
总结来说,利用StatementCallback
可以让我们更加高效地进行数据库操作,同时保持代码简洁和易于管理。
1
2
3
4
5
6
7
8
|
package com.minis.jdbc.core;
import java.sql.SQLException;
import java.sql.Statement;
public interface StatementCallback {
Object doInStatement(Statement stmt) throws SQLException;
}
|
可以看出这是一个函数式接口。
现在,应用程序就只需要用一个JdbcTemplate类就可以了,不用再为每一个业务类单独做一个子类。就像我们前面说的,用户类需要使用Callback动态匿名类的方式进行改造。
代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public User getUserInfo(int userid) {
final String sql = "select id, name,birthday from users where id="+userid;
return (User)jdbcTemplate.query(
(stmt)->{
ResultSet rs = stmt.executeQuery(sql);
User rtnUser = null;
if (rs.next()) {
rtnUser = new User();
rtnUser.setId(userid);
rtnUser.setName(rs.getString("name"));
rtnUser.setBirthday(new java.util.Date(rs.getDate("birthday").getTime()));
}
return rtnUser;
}
);
}
|
在代码重构的过程中,我们观察到原本位于UserJdbcImpl
类中的业务逻辑,特别是对SQL语句执行结果的处理部分,已经被修改为匿名类的形式,并作为参数传递给query()
方法。这种方式允许query()
方法在完成数据库查询后通过回调机制来执行这个匿名类中定义的逻辑。
进一步地,采用类似的策略,我们可以支持PreparedStatement
类型,这将使得在调用相关方法时能够直接附带SQL语句所需要的参数值。这样做的好处是提高了代码的复用性和灵活性,同时使得与数据库交互的部分更加清晰、简洁。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public Object query(String sql, Object[] args, PreparedStatementCallback pstmtcallback) {
//省略获取connection等代码
pstmt = con.prepareStatement(sql);
for (int i = 0; i < args.length; i++) { //设置参数
Object arg = args[i];
//按照不同的数据类型调用JDBC的不同设置方法
if (arg instanceof String) {
pstmt.setString(i+1, (String)arg);
} else if (arg instanceof Integer) {
pstmt.setInt(i+1, (int)arg);
}
}
return pstmtcallback.doInPreparedStatement(pstmt);
}
|
在使用PreparedStatement
与普通的Statement
进行对比时,一个主要的区别在于PreparedStatement
需要对SQL语句中的参数逐一设置值。当你的SQL语句包含多个参数时,MiniSpring框架会按照这些参数在SQL语句中出现的顺序来赋值,而不是根据参数的名字。
接下来,让我们来看看为PreparedStatement
设计的Callback接口。
该Callback接口允许开发者定义如何处理PreparedStatement
实例。通过这种方式,你可以方便地设置每个参数的具体值,并执行数据库操作。这样做的好处是提高了代码的安全性和效率,因为预编译语句可以防止SQL注入攻击,并且如果多次执行相同的查询,数据库可以对其进行优化。
使用示例
假设我们有一个带有两个参数的SQL插入语句:
1
|
INSERT INTO users (name, age) VALUES (?, ?);
|
那么,在使用PreparedStatement
时,你需要按顺序设置这两个问号所代表的值,即先设置name
,再设置age
,不论它们在Java方法调用中的顺序或命名是什么样的。
注意事项
1
2
3
4
5
6
7
8
|
package com.minis.jdbc.core;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public interface PreparedStatementCallback {
Object doInPreparedStatement(PreparedStatement stmt) throws SQLException;
}
|
这也是一个函数式接口。
用户服务类代码改造如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public User getUserInfo(int userid) {
final String sql = "select id, name,birthday from users where id=?";
return (User)jdbcTemplate.query(sql, new Object[]{new Integer(userid)},
(pstmt)->{
ResultSet rs = pstmt.executeQuery();
User rtnUser = null;
if (rs.next()) {
rtnUser = new User();
rtnUser.setId(userid);
rtnUser.setName(rs.getString("name"));
}
return rtnUser;
}
);
}
|
到这里,我们就用一个单一的JdbcTemplate类实现了数据访问。
结合IoC容器
当然,我们还可以更进一步,既然我们的MiniSpring是个IoC容器,可以管理一个一个的Bean对象,那么我们就要好好利用它。由于只需要唯一的一个JdbcTemplate类,我们就可以事先把它定义为一个Bean,放在IoC容器里,然后通过@Autowired自动注入。
在XML配置文件中声明一下。
1
|
<bean id="jdbcTemplate" class="com.minis.jdbc.core.JdbcTemplate" />
|
上层用户service程序中就不需要自己手动创建JdbcTemplate,而是通过Autowired注解进行注入就能得到了。
1
2
3
4
5
6
7
8
9
10
11
|
package com.test.service;
import java.sql.ResultSet;
import com.minis.beans.factory.annotation.Autowired;
import com.minis.jdbc.core.JdbcTemplate;
import com.test.entity.User;
public class UserService {
@Autowired
JdbcTemplate jdbcTemplate;
}
|
IoC容器与MiniSpring框架的便利性
MiniSpring框架仅支持通过名称匹配来注入依赖,这意味着在UserService
类中声明的实例变量JdbcTemplate
必须与XML配置文件中定义的Bean的id
相匹配。如果不一致,程序将无法找到JdbcTemplate
实例。
通过这种方式,应用程序中与数据库访问相关的代码被完全抽象出去,应用程序只需声明其使用,而创建和管理则由MiniSpring框架负责。这展示了IoC容器带来的便利性。实际上,许多我们需要的工具,都会以Bean的形式在配置文件中声明,并由IoC容器管理。
数据源配置
尽管如此,我们注意到JdbcTemplate
中获取数据库连接信息等常规操作仍然是硬编码的(hard coded)。这意味着这些信息需要在代码中直接指定,而不是通过配置文件或其他灵活的方式管理。
1
2
|
Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");
con = DriverManager.getConnection("jdbc:sqlserver://localhost:1433;databasename=DEMO;user=sa;password=Sql2016;");
|
现在我们动手把这一部分代码包装成DataSource,通过它获取数据库连接。假设有了这个工具,上层应用程序就简单了。你可以看一下使用者的代码示例。
1
|
con = dataSource.getConnection();
|
这个Data Source被JdbcTemplate使用。
1
2
3
|
public class JdbcTemplate {
private DataSource dataSource;
}
|
而这个属性可以通过配置注入,你可以看下配置文件。
1
2
3
4
5
6
7
8
9
|
<bean id="dataSource" class="com.minis.jdbc.datasource.SingleConnectionDataSource">
<property type="String" name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
<property type="String" name="url" value="jdbc:sqlserver://localhost:1433;databasename=DEMO;"/>
<property type="String" name="username" value="sa"/>
<property type="String" name="password" value="Sql2016"/>
</bean>
<bean id="jdbcTemplate" class="com.minis.jdbc.core.JdbcTemplate" >
<property type="javax.sql.DataSource" name="dataSource" ref="dataSource"/>
</bean>
|
DataSource Bean初始化与使用
在Spring框架中,DataSource Bean的初始化和使用是一个关键步骤。以下是其详细过程:
1. DataSource Bean初始化
DataSource Bean的初始化涉及到加载JDBC Driver和注入JdbcTemplate。具体步骤如下:
2. 独立组件与IoC容器的优势
将DataSource、JdbcTemplate等组件独立抽取,并利用IoC容器进行Bean管理,可以带来以下便利:
3. DataSource接口与实现
DataSource接口定义在javax.sql
包中,名为DataSource
。为了使用这个接口,我们需要实现它。以下是实现DataSource接口的基本步骤:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
package com.minis.jdbc.datasource;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.Properties;
import java.util.logging.Logger;
import javax.sql.DataSource;
public class SingleConnectionDataSource implements DataSource {
private String driverClassName;
private String url;
private String username;
private String password;
private Properties connectionProperties;
private Connection connection;
//默认构造函数
public SingleConnectionDataSource() {
}
//一下是属性相关的getter和setter
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Properties getConnectionProperties() {
return connectionProperties;
}
public void setConnectionProperties(Properties connectionProperties) {
this.connectionProperties = connectionProperties;
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}
@Override
public int getLoginTimeout() throws SQLException {
return 0;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
@Override
public void setLogWriter(PrintWriter arg0) throws SQLException {
}
@Override
public void setLoginTimeout(int arg0) throws SQLException {
}
@Override
public boolean isWrapperFor(Class<?> arg0) throws SQLException {
return false;
}
@Override
public <T> T unwrap(Class<T> arg0) throws SQLException {
return null;
}
//设置driver class name的方法,要加载driver类
public void setDriverClassName(String driverClassName) {
this.driverClassName = driverClassName;
try {
Class.forName(this.driverClassName);
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException("Could not load JDBC driver class [" + driverClassName + "]", ex);
}
}
//实际建立数据库连接
@Override
public Connection getConnection() throws SQLException {
return getConnectionFromDriver(getUsername(), getPassword());
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return getConnectionFromDriver(username, password);
}
//将参数组织成Properties结构,然后拿到实际的数据库连接
protected Connection getConnectionFromDriver(String username, String password) throws SQLException {
Properties mergedProps = new Properties();
Properties connProps = getConnectionProperties();
if (connProps != null) {
mergedProps.putAll(connProps);
}
if (username != null) {
mergedProps.setProperty("user", username);
}
if (password != null) {
mergedProps.setProperty("password", password);
}
this.connection = getConnectionFromDriverManager(getUrl(),mergedProps);
return this.connection;
}
//通过DriverManager.getConnection()建立实际的连接
protected Connection getConnectionFromDriverManager(String url, Properties props) throws SQLException {
return DriverManager.getConnection(url, props);
}
}
|
在这节课中,我们探讨了如何通过几个关键手段来简化数据库操作并重构数据访问的程序结构。以下是本节课的重点内容总结:
数据库连接创建时机
- 即时创建:MiniSpring框架采用了一种策略,即在应用程序实际调用
dataSource.getConnection()
时才创建数据库连接。这与初始化Bean时就创建连接的方式不同。
简化数据库操作的三大手段
- 模板化
- Callback模式
- IoC容器与自动注入
数据源概念的引入
- 通过抽取数据源的概念,并对数据库连接进行包装,实现了应用程序与底层数据库之间的解耦。
当前实现的不足之处
课后思考题