数据库访问从原理到实践-java版
在项目的开发过程中,很难不用到数据库访问。但是一直是只知其然,却不知其所以然。这几天有了点时间,就对数据库访问的原理进行了一番调查,写下来以便今后查阅。
对数据库的访问是一个非常常用的功能,市面上的实现可谓琳琅满目,不胜枚举。但是万变不离其宗,他们都是对DataSource这个java标准类库中的类进行了包装,在JDBC API的基础上根据项目的不同着眼点对其进行了扩展和增强。因此,本文从DataSource开始写起,描述其基本原理和特性,然后以UCP和Goovy Sql为例,介绍了成熟的实现的特性和一些用法。
DataSource概览
在连接数据源的时候,优先使用Datasource。DataSource为数据库连接提供了连接池和分布式的transaction管理【1】。实现了DataSource的类的实例可以代表数据源,例如一个特定的DBMS,甚至是一个文件。
DataSource是JDBC2.0的新特性【2】。与JDBC1.0 的DriverManager不同,DataSource不需要注册Driver,而是通过lookup以JNDI的方式注册DataSource,其接口由驱动程序供应商实现。
JNDI
在这里,有必要介绍一下JNDI。在java生态中,JNDI主要用于Jakarta EE(其前身为J2EE,J2EE是一个古老的称呼,至少在2005年以后,J2EE便更名为Java EE【3】,而在2018年3月正式更名为Jakarta EE【4】)应用的开发和部署。在Jakarta EE应用的开发和部署的过程中,主要有以下4种角色【5】:
- 组件提供者
这个角色负责创建 Jakarta EE组件,Jakarta EE组件可以是 Web 应用程序、企业级 JavaBean(EJB)组件,或者是应用程序客户机(例如基于 Swing 的 GUI 客户机应用程序)。组件提供者包括:HTML 设计师、文档编程人员以及其他开发人员角色。大多数 Jakarta EE开发人员在组件提供者这一角色上耗费了相当多的时间。 - 应用程序组装者
这个角色将多个 Jakarta EE模块捆绑成一个彼此结合的、可以部署的整体:企业归档(EAR)文件。应用程序组装者要选择组件,分清它们之间的交互方式,配置它们的安全性和事务属性,并把应用程序打包到 EAR 文件中。许多 IDE,例如 WebSphere® Studio、IDEA、JBuilder、WebLogic Workshop 和其他 IDE,都可以帮助应用程序组装者以交互方式配置 EAR 文件。 - 部署人员(Deployer)
这个角色负责部署,这意味着将 EAR 安装到 Jakarta EE容器(应用服务器)中,然后配置资源(例如数据库连接池),把应用程序需要的资源绑定到应用服务器中的特定资源上,并启动应用程序。 - 系统管理员(System Administrator)
这个角色负责保证容器需要的资源可用于容器。
Jakarta EE组件(例如WAR文件和EJB JAR文件)必须在他们的部署单元之外声明他们在资源上的依赖性。在JNDI被广泛使用之前,开发人员也必须涉足部署人员的领地,并且还要准备配置数据源等外部资源。JNDI提供了一套机制,通过将JNDI API映射为特定的命名服务和目录系统【6】,将外部资源(例如datasource)注册到系统中,为部署单元提供服务。
对于每个引用,部署人员都需要把新组件按特定的名称(比如说 ejb/ProcessOrders/1.1)绑定到全局树中,对于需要 EJB 组件的其他每个组件,还要为组件在部署描述符中解析 EJB 引用。依赖于 V1.0 以前的应用程序不需要进行任何修改,也不需要重新测试,这缩短了实现的时间、降低了成本并减少了复杂性。部署人员的工作就是创建 DataSource(或者是创建一个 Object 对象,让 foo 指向它,在我们的 Java 语言示例中就是这样)。从而,开发人员从他们不熟悉的部署工作中解脱了出来。
绑定资源:
Context ctx = new InitialContext();
ctx.bind("jdbc/billingDB", ds);
使用资源:
Context ctx = new InitialContext();
DataSource ds = (DataSource)ctx.lookup("jdbc/billingDB");
如果说我们只是在J2SE的环境下用DataSource连接数据库,其实无需考虑如何注册数据源。
UDP:在项目中使用DataSource:
UCP[7],连接池是数据连接对象的缓存。在运行时,应用会从连接池中请求一个连接。如果连接池内存在可以满足请求的连接,他就把连接返回给应用。如果没有的话,就会创建一个并返回给应用。
通常情况下,创建一个连接的代价是比较高的,通过对连接池进行合理的参数配置,可以有效的利用资源。JDBC UCP 提供了连接池的实现,以缓存JDBC连接,从而提高应用的性能并合理利用系统资源。一个UCP JDBC连接池可以用JDBC driver创建物理连接,并在池中对其进行维护。
UCP JDBC连接池的架构:
jdbc_arch.png
在这里有两个问题需要注意
- JDBC数据库连接池connection关闭后Statement和ResultSet未必关闭【8】
- 注意ResultSet读取的方式。
- 对于问题1,可以看到Java Spec的说明:
Releases this ResultSet object's database and JDBC resources immediately instead of >waiting for this to happen when it is automatically closed.
Note: A ResultSet object is automatically closed by the Statement object that generated it ?>when that Statement object is closed, re-executed, or is used to retrieve the next result from a >sequence of multiple results. A ResultSet object is also automatically closed when it is >garbage collected.
规范说明:
1.垃圾回收机制可以自动关闭它们;
2.Statement关闭会导致ResultSet关闭;
3.Connection关闭不一定会导致Statement关闭。
当然,UCP此时的关闭Connection,并不是真正关闭,而是将其放回连接池的空闲队列中。
解决建议:
- 由于垃圾回收的线程级别是最低的,为了充分利用数据库资源,有必要显式关闭它们,尤其是使用Connection Pool的时候;
- 最优经验是按照ResultSet,Statement,Connection的顺序执行close;
- 为了避免由于java代码有问题导致内存泄露,需要在rs.close()和stmt.close()后面一定要加上rs = null和stmt = null;
public class OracleDBConnectionManager {
private static final String CONNECTION_CLASS_NAME = "oracle.jdbc.pool.OracleDataSource";
private static final String JDBC_URL = "jdbc:oracle:thin:@//host:1521/xe";
private static final int POOL_SIZE_INITIAL = 1;
private static final int POOL_SIZE_MIN = 1;
private static final int POOL_SIZE_MAX = 3;
private static final int POOL_CONNECTION_REUSE_COUNT_MAX = 1000;
private static final int POOL_CONNECTION_REUSE_TIME_MAX = 150;
private static final int POOL_CONNECTION_WAIT_TIMEOUT_MAX = 300;
private static final boolean USE_CONNECTION_POOL = true;
public static DataSource getDataSource() {
Connection testConn = null;
Statement stmt = null;
ResultSet rs = null;
try {
PoolDataSource pds = PoolDataSourceFactory.getPoolDataSource();
pds.setConnectionFactoryClassName(CONNECTION_CLASS_NAME);
pds.setURL(JDBC_URL);
pds.setUser("sys as SYSDBA");
pds.setPassword("oracle");
pds.setInitialPoolSize(POOL_SIZE_INITIAL);
pds.setMinPoolSize(POOL_SIZE_MIN);
pds.setMaxPoolSize(POOL_SIZE_MAX);
pds.setMaxConnectionReuseCount(POOL_CONNECTION_REUSE_COUNT_MAX);
pds.setMaxConnectionReuseTime(POOL_CONNECTION_REUSE_TIME_MAX);
pds.setValidateConnectionOnBorrow(USE_CONNECTION_POOL);
pds.setConnectionWaitTimeout(POOL_CONNECTION_WAIT_TIMEOUT_MAX);
Properties connProps = new Properties();
//auto-commit should always be false
connProps.setProperty("autoCommit", "true");
pds.setConnectionProperties(connProps);
testConn = pds.getConnection();
stmt = testConn.createStatement();
rs = stmt.executeQuery("select * from STUDENT");
while(rs.next()){
System.out.println("===========");
System.out.println(rs.getString("SNO"));
System.out.println(rs.getString("SNAME"));
System.out.println(rs.getString("SSEX"));
System.out.println(rs.getDate("SBIRTHDAY"));
System.out.println(rs.getString("CLASS"));
}
return pds;
} catch (SQLException e) {
e.printStackTrace();
}finally{
try{
rs.close();
}catch (Exception e){
// do sth
}finally {
rs = null;
}
try{
stmt.close();
}catch (Exception e){
// do sth
}finally {
stmt = null;
}
try{
testConn.close();
}catch (Exception e){
// do sth
}finally {
testConn = null;
}
}
return null;
}
}
当然这种做法显得很笨拙,你可以对其进行封装或者直接采用已有的方案【9】。
- 对于问题2
如果一定要传递ResultSet,应该使用RowSet,RowSet可以不依赖于Connection和Statement。Java传递的是引用,所以如果传递ResultSet,你会不知道Statement和Connection何时关闭,不知道ResultSet何时有效。
成熟的类库对JDBC API的封装
实现类库非常的多,这里仅仅介绍Groovy Sql。Groovy Sql 在JDBC API 的基础上,以外观(Facade)模式对资源管理和resultset操作进行了很大程度上的简化。隐藏了获得数据库连接,构造和配置statement,同数据库连接的交互,关闭资源和记录日志。此外,还提供了其他的一些特性,例如操作ResultSet提供了更加丰富的类似Collection的 API,支持闭包,批处理,transaction等等。
public class SampleDao {
private static OracleDBConnectionManager oracleDBConnectionManager;
public static void main(String...strings){
select("select * from STUDENT");
}
public static void select(String query) {
Sql querySql = null;
try {
oracleDBConnectionManager = new OracleDBConnectionManager();
querySql = new Sql(oracleDBConnectionManager.getDataSource());
GroovyRowResult result = querySql.firstRow(query);
System.out.println("Get the first row of the results");
System.out.println((String)result.get("SNO"));
List<GroovyRowResult> groovyRowResults = querySql.rows(query);
for (GroovyRowResult groovyRowResult1: groovyRowResults) {
List<String> inner = new ArrayList<String>();
Set<String> keys = groovyRowResult1.keySet();
for (String k: keys) {
if(groovyRowResult1.get(k) instanceof String) {
System.out.println("Value for String " + k + " is " + (String) result.get(k));
}
}
}
} catch (SQLException e) {
} finally {
if(querySql!=null){
querySql.close();
}
}
}
}
详细的操作请参照【10】【11】。
【1】sqldatasources
【2】两种创建DBConnection1以调用JDBC API的方式
【3】j2ee
【4】Jakarta_EE
【5】JNDI
【6】百度百科 JNDI
【7】UDP
【8】JDBC数据库连接池connection关闭后Statement和ResultSet未关闭的问题
【9】在Java中关闭数据库连接
【10】groovy Sql
【11】groovy-databases
【12】Resultset的用法
【13】Statement