在以java application server应用为主的平台是,JDBC的最高级应用是DataSource的实现,其它的,JDO,webcache,hibernate等
不过是一种封装,所以看数据库应用的性能,主要看据库连结的利用率,所以,DataSource可以说是J2EE最最重要的应
用.
对于主流的数据库,如mysql,mssqlserver,oracle,从其通用的性能来看,我建议在JAVA平台上应用oracle和mysql,MS的数
据库在JAVA平台上的性能只能达到上述两种数据库的60-70%,在相同硬件的基础是,做同样的操作,测试了这三种数
据库对并发的支持,MYSQL最高为430,ORACLE为400,SQLSERVER只有250.
如果你不得不用SQLSERVER作为JAVA平台的数据库,那么一个合适的JDBC是最最重要的,MS自己提供的JDBC到目前为
止并不能真正支持DataSource,不过bea的weblogic8中已经内置了type4的ms sqlserver的JDBC,这是一个真正支
持DataSource的JDBC,如果你不得不用sqlserver,你可以把这个JDBC从BEA的WLS中驳离出来.
另外一点,如果你的J2EE应用主要集中在jsp/servlet,而后台数据库采用oracle的话,那么你有福了.你不要采用其它
的WEB容器,在oracle8.17以后的版本中都内置了servlet引擎.
如果你基于这个引擎,你会获得其它应用组合所无法比拟的性能,因为ORACLE的WEB容器是工作在数据库的地址空
间,想一想,这意味着什么?
这就意味着,容器对数据库的访问是"本地文件存取方式",虽然oracle也提供了标准的JDBC接口让调用者调用,但事实
上这时的Connection对象只相当于一个文件句柄,而其它的JDBC连结都是封装了Socket,通过Socket和数据库进行通讯,
如果你不理解这里的区别,那我打一个比方:就好象两个人面对面谈话和通过国际长途谈话的区别,这其间的性能怎
一个高字了得.
mysql从来就是被设计为"网络数据库",针对数据安全性不是太重要的应用.它的最大优点是通用,性能高,速度快,安
全性较低,对于不是机密要件的应用,用MYSQL肯定没错.
MS SQLSERVER,除了在JAVA平台上性能表现不好外,没有什么可说的,如果换在WIN平台上,它是顶级首选项,没有任何
数据库在WIN平台上可以和它做任何方面的比较.
ORACLE,JAVA平台上的老大,除去成本因素,在JAVA平台上没有理由不选择它.
本来不想写这部份入门级的内容,但既然栏目定为JDBC专栏,还是简单写一些吧.
来,我们认识一下!
JDBC,JAVA平台的DATABASE的连通性.白话一句,什么意思啊?
就是JAVA平台上和数据库进行连结的"工具".
还是先一起来回顾一下接口吧:从下向上,接口是对"案例"的抽象,由一个案例抽象出一些规则.
反过来,从上向下,被抽象出来的接口是对案例的一种承诺和约束.
也就是说,只要你实现我规定的接口,你的类就已经具有了接口对外承诺的方法,只要"客户"会
操作接口,不需要重新学习就会操作实现了该接口的新类!
好了,用行话来说:
1.通过接口可以实现不相关的类的相同行为.
2.通过接口可以指明多个类需要实现的方法.
3.通过接口可以了解对象的交互方法而不需要了解对象所对应的类蓝本.
这几句话很明白吧?好象有一本什么模式的书把这段话用了30多页写出来,结果别人看了还不如
我这几句话明白,不过我明白了为什么有些人要写书了.
搞懂了以上这东西,JDBC就好明白了.
为了通用,JAVA中要求有一种机制,在操作不同厂商数据库时有相同的方法去操作,而不是每接
触一种数据库就要学习新的方法.完成这种机制的"东西"就叫"JDBC"了.
简单地分,JDBC有两部分组成,JDBC API和JDBC Driver Interface.
JDBC API就是提供给"客户"(就是象你我这种菜鸟级程序员来用的,如果是高手都自己写JDBC了,
哈哈)的一组独立于数据库的API,对任何数据库的操作,都可以用这组API来进行.那么要把这些通用的API
翻译成特定数据库能懂的"指令",就要由JDBC Driver Interface来实现了,所以这部份是面向JDBC驱动程
序开发商的编程接口,它会把我们通过JDBC API发给数据库的通用指令翻译给他们自己的数据库.
还是通过实际操作来看看JDBC如何工作的吧.
因为JDBC API是通用接口,那么程序是如何知道我要连结的是哪种数据库呢?所以在和数据库连
结时先要加载(或注册可用的Driver),其实就是JDBC签名.加载驱动程序和好多方法,最常用的就是先把驱
动程序类溶解到内存中,作为"当前"驱动程序.注意"当前"是说内存中可以有多个驱动程序,但只有现在加
载的这个作为首选连结的驱动程序.
Class.forName("org.gjt.mm.mysql.Driver");
Class.forName方法是先在内存中溶解签名为"org.gjt.mm.mysql.Driver"的Driver类,Driver类
就会把相应的实现类对应到JDBC API的接口中.比如把org.gjt.mm.mysql.Connection的实例对象赋给
java.sql.Connection接口句柄,以便"客户"能通过操作java.sql.Connection句柄来调用实际的
org.gjt.mm.mysql.Connection中的方法.之于它们是如果映射的,这是厂商编程的,"客户"只要调用
Class.forName("org.gjt.mm.mysql.Driver");方法就可以顺利地操作JDBC API了.
一个普通数据库的连结过程为:
1.加载驱动程序.
2.通过DriverManager到得一个与数据库连结的句柄.
3.通过连结句柄绑定要执行的语句.
4.接收执行结果.
5.可选的对结果的处理.
6.必要的关闭和数据库的连结.
因为是基础篇,所以还是对每一步骤简单说明一下吧:
前面说是,注册驱动程序有多方法,Class.forName();是一种显式地加载.当一个驱
动程序类被Classloader装载后,在溶解的过程中,DriverManager会注册这个驱动类的实例.
这个调用是自动发生的,也就是说DriverManager.registerDriver()方法被自动调用了,当然
我们也可以直接调用DriverManager.registerDriver()来注册驱动程序,但是,以我的经验.
MS的浏览中APPLET在调用这个方法时不能成功,也就是说MS在浏览器中内置的JVM对该方法的
实现是无效的.
另外我们还可以利用系统属性jdbc.drivers来加载多个驱动程序:
System.setProperty("jdbc.drivers","driver1:driver2:.....:drivern");多个驱动程序之
间用":"隔开,这样在连结时JDBC会按顺序搜索,直到找到第一个能成功连结指定的URL的驱动
程序.
在基础篇里我们先不介绍DataSource这些高级特性.
在成功注册驱动程序后,我们就可以用DriverManager的静态方法getConnection来得
到和数据库连结的引用:
Connection conn = DriverManager.getConnection(url);
如果连结是成功的,则返回Connection对象conn,如果为null或抛出异常,则说明没有
和数据库建立连结.
对于getConnection()方法有三个重载的方法,一种是最简单的只给出数据源即:
getConnection(url),另一种是同时给出一些数据源信息即getConnection(url,Properties),
另外一种就是给出数据源,用户名和密码:getConnection(url,user,passwod),对于数据源信息.
如果我们想在连结时给出更多的信息可以把这些信息压入到一个Properties,当然可以直接压
入用户名密码,别外还可以压入指定字符集,编码方式或默认操作等一些其它信息.
在得到一个连结后,也就是有了和数据库找交道的通道.我们就可以做我们想要的操
作了.
还是先来介绍一些一般性的操作:
如果我们要对数据库中的表进行操作,要先缘故绑定一个语句:
Statement stmt = conn.createStatement();
然后利用这个语句来执行操作.根本操作目的,可以有两种结果返回,如果执行的查询
操作,返回为结果集ResultSet,如果执行更新操作,则返回操作的记录数int.
注意,SQL操作严格区分只有两个,一种就是读操作(查询操作),另一种就是写操作(更
新操作),所以,create,insert,update,drop,delete等对数据有改写行为的操作都是更新操作.
ResultSet rs = stmt.executeQuery("select * from table where xxxxx");
int x = stmt.executeUpdate("delete from table where ......");
如果你硬要用executeQuery执行一个更新操作是可以的,但不要把它赋给一个句柄,
当然稍微有些经验的程序员是不会这么做的.
至于对结果集的处理,我们放在下一节讨论,因为它是可操作的可选项,只有查询操作
才返回结果集,对于一次操作过程的完成,一个非常必要的步骤是关闭数据库连结,在你没有了
解更多的JDBC知识这前,你先把这一步骤作为JDBC操作中最最重要的一步,在以后的介绍中我会
不断地提醒你去关闭数据库连结!!!!!!!!!!!
按上面介绍的步骤,一个完成的例子是这样的:(注意,为了按上面的步骤介绍,这个例
子不是最好的)
try{
Class.forName("org.gjt.mm.mysql.Driver");
}catch(Exception e){
System.out.println("没有成功加载驱动程序:"+e.toString());
return;
}//对于象我这样的经验,可以直接从e.toString()的简单的几个字判断出异常原因,
//如果你是一个新手应该选捕获它的子类,如何知道要捕获哪几个异常呢?一个简单
//的方法就是先不加try{},直接Class.forName("org.gjt.mm.mysql.Driver");,编
//译器就会告诉你要你捕获哪几个异常了,当然这是偷机取巧的方法,最好还是自己
//去看JDK文档,它会告诉你每个方法有哪些异常要你捕获.
Connection conn = null;
try{
conn = DriverManager.getConnection(
"jdbc:mysql://host:3306/mysql",
"user",
"passwd");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select * from table");
//rs 处理
[rs.close();]
[stmt.close();]
}
catch(Exception e){
System.out.println("数据库操作出现异常:"+e.toString());
}
finally{
try{conn.close();}catch(Exception){}
}//不管你以前是学习到的关于数据库流程是如何操作的,如果你相信我,从现在开始,
//请你一定要把数据库关闭的代码写到finally块中,切切!
关于Statement对象:
前面说过,Statement对象是用来绑定要执行的操作的,在它上面有三种执行方法:
即用来执行查询操作的executeQuery(),用来执行更新操作的executeUpdate()和用来执行
动态的未知的操作的execute().
JDBC在编译时并不对要执行的SQL语句检测,只是把它看着一个String,只有在驱动
程序执行SQL语句时才知道正确与否.
一个Statement对象同时只能有一个结果集在活动.这是宽容性的,就是说即使没有
调用ResultSet的close()方法,只要打开第二个结果集就隐含着对上一个结果集的关闭.所以
如果你想同时对多个结果集操作,就要创建多个Statement对象,如果不需要同时操作,那么可
以在一个Statement对象上须序操作多个结果集.
这里我不得不特别说明一下,很多人会用一个Statement进行嵌套查询,然后就来问
我说为什么不能循环?道理上面已经说清楚了.我们来详细分析一下嵌套查询:
Connection conn = null;
Statement stmt = null;
conn = .......;
stmt = conm.createStatement(xxxxxx);
ResultSet rs = stmt.executeQuery(sql1);
while(rs.next()){
str = rs.getString(xxxxx);
ResultSet rs1 = stmt.executeQuery("select * from 表 where 字段=str");
}
当stmt.executeQuery("select * from 表 where 字段=str");赋给rs1时,这时隐含的操作
是已经关闭了rs,你还能循环下去吗?
所以如果要同时操作多个结果集一定要让它他绑定到不同的Statement对象上.好在一个connection
对象可以创建任意多个Statement对象,而不需要你重新获取连结.
关于获取和设置Statement的选项:只要看看它的getXXX方法和setXXX方法就明白了,这儿
作为基础知识只提一下以下几个:
setQueryTimeout,设置一个SQL执行的超时限制.
setMaxRows,设置结果集能容纳的行数.
setEscapeProcessing,如果参数为true,则驱动程序在把SQL语句发给数据库前进行转义替
换,否则让数据库自己处理,当然这些默认值都可以通过get方法查询.
Statement的两个子类:
PreparedStatement:对于同一条语句的多次执行,Statement每次都要把SQL语句发送给数据
库,这样做效率明显不高,而如果数据库支持预编译,PreparedStatement可以先把要执行的语句一次发
给它,然后每次执行而不必发送相同的语句,效率当然提高,当然如果数据库不支持预编译,
PreparedStatement会象Statement一样工作,只是效率不高而不需要用户工手干预.
另外PreparedStatement还支持接收参数.在预编译后只要传输不同的参数就可以执行,大大
提高了性能.
PreparedStatement ps = conn.prepareStatement("select * from 表 where 字段=?");
ps.setString(1,参数);
ResultSet rs = ps.executeQuery();
CallableStatement:是PreparedStatement的子类,它只是用来执行存储过程的.
CallableStatement sc = conn.prepareCall("{call query()}");
ResultSet rs = cs.executeQuery();
关于更高级的知识我们在JDBC高级应用中介绍.
作为基础知识的最后部分,我们来说一说结果集的处理,当然是说对一般结果集的处理.
至于存储过程返回的多结果集,我们仍然放在高级应用中介绍.
SQL语句如何执行的是查询操作,那就要返回一个ResultSet对象,要想把查询结果最后
明白地显示给用户,必须对ResultSet进行处理.ResultSet返回的是一个表中符合条件的记录,对
ResultSet的处理要逐行处理,而对于每一行的列的处理,则可以按任意顺序(注意,这只是JDBC规
范的要求,有些JDBC实现时对于列的处理仍然要求用户按顺序处理,但这是极少数的).事实上,虽
然你可以在处理列的时候可以按任意顺序,但如果你按从左到右的顺序则可以得到较高的性能.
这儿从底层来讲解一下ResultSet对象,在任何介绍JDBC的书上你是不会获得这样的知
识的,因为那是数据库厂商的事.ResultSet对象实际维护的是一个二维指针,第一维是指向当前
行,最初它指向的是结果集的第一行之前,所以如果要访问第一行,就要先next(),以后每一行都
要先next()才能访问,然后第二维的指针指向列,只要当你去rs.getXXX(列)时,才通过
Connection再去数据库把真实的数据取出来,否则没有什么机器能真的把要取的数据都放在内
存中.
所以,千万要记住,如果Connection已经关闭,那是不可能再从ResultSet中取到数据的.
有很多人问我,我可不可以取到一个ResultSet把它写到Session中然后关闭Connection,这样就
不要每次都连结了.我只能告诉你,你的想法非常好,但,是错误的!当然在javax.sql包中JDBC高
级应用中有CacheRow和WebCacheRow可以把结果集缓存下来,但那和我们自己开一个数据结构把
ResultSet的行集中所有值一次取出来保存起来没有什么两样.
访问行中的列,可以按字段名或索引来访问.下面是一个简单的检索结果的程序:
ResultSet rs = stmt.executeQuery("select a1,a2,a3 from table");
while(rs.next()){
int i = rs.getInt(1);
String a = rs.getString("a2");
..............
}
对于用来显示的结果集,用while来进行next()是最普通的,如果next()返回false,则
说明已经没有可用的行了.但有时我们可能连一行都没有,而如果有记录又不知道是多少行,这时
如果要对有记录和没有记录进行不同的处理,应该用以下流程进行判断:
if(rs.next()){
//因为已经先next()了,所经对记录应该用do{}while();来处理
do{
int i = rs.getInt(1);
String a = rs.getString("a2");
}while(rs.next());
}
esle{
System.out.println("没有取得符合条件的记录!");
}
类型转换:
ResultSet的getXXX方法将努力把结果集中的SQL数据类型转换为JAVA的数据类型,事实
大多数类型是可以转换的,但仍然有不少糊弄是不能转换的,如你不能将一个SQL的float转换成
JAVA的DATE,你无法将 VARCHAR "我们"转换成JAVA的Int.
较大的值:
对于大于Statement中getMaxFieldSize返回值的值,用普通的getBytes()或getString()
是不能读取的,好在JAVA提供了读取输入流的方法,对于大对象,我们可以通过rs.getXXXStream()
来得到一个InputStream,XXX的类型包括Ascii,Binay,Unicode.根据你存储的字段类型来使用不
同的流类型,一般来说,二进制文件用getBinayStream(),文本文件用getAsciiStyream(),对于
Unicode字符的文本文件用getUnicodeStream(),相对应的数据库字段类型应该为:Blob,Clob和
Nlob.
获取结果集的信息:
大多数情况下编程人员对数据库结构是了解的,可以知道结果集中各列的情况,但有时并
不知道结果集中有哪些列,是什么类型.这时可以通过getMetaData()来获取结果集的情况:
ResulSetMetaData rsmd = rs.getMetaData();
rsmd.getColumnCount()返回列的个数.
getColumnLabel(int)返回该int所对应的列的显示标题
getColumnName(int)返回该int所对应的列的在数据库中的名称.
getColumnType(int)返回该int所对应的列的在数据库中的数据类型.
getColumnTypeName(int)返回该int所对应的列的数据类型在数据源中的名称.
isReadOnly(int)返回该int所对应的列是否只读.
isNullable(int)返回该int所对应的列是否可以为空
在了解JDBC基础知识以后,我们先来写一个数据库操作的类(Bean)以后我们会
在这个类的基础上,随着介绍的深入不断提供优化的方案.
要把一个数据库操作独立到一个类(Bean)中,至少要考虑以下几个方面:
1.对于不同层次的应用,应该有不同的得到连结的方法,如果得到连结的方法要随
着应用层次的不同而改变,我们就应该把他独立成一个专门的类中,而把在任何应用层次
中都通用的处理方法封装到一个(类)Bean中.
2.既然考虑到既作为javaBean使用又可以用为一个普通类调用,要考虑到javaBean
的规范和普通类的灵活性.
3.对于特定的数据库操作不应封装到共性的(类)Bean中,而应该成为它的扩展类.
以上几点是充分考虑JAVA的面象对象的思想,经过深入的抽象形成的层次,下面我
们就按这个思想来设计:
一:定义一个用于连结的Bean,以后如果要在不同的应用中,如可以在J2EE中从
DataSource中得到连结,或从普通的连结池中得到连结,以及直接从DriverManager中得到
连结,只需修改本类中的得到连结的实现方法.
package com.imnamg.axman.beans;
import java.sql.*;
import ..................
public class ConnectionFactory{
protected Connection conn;
ConnectionFactory() throws SQLException
{ //构造方法中生成连结
//无论是从DataSource还是直接从DriverManager中取得连结.
//先初始化环境,然后取得连结,本例作为初级应用,从
//DriverManager中取得连结,因为是封装类,所以要把异常抛
//给调用它的程序处理而不要用try{}catch(){}块自选处理了.
//因为要给业务方法的类继承,而又不能给调用都访问,所以
//conn声明为protected
conn = DriverManager.getConnection(url,user,passwd);
}
/**
在多线程编程中,很多时候有可能在多个线程体中得到同一连
结的引用,但如果在一个线程中关闭了连结,则另一个得到相同
引用的线程就无法操作了,所以我们应该加一个重新建立连结
的辅助方法,有人问为什么既然有这个辅助方法不直接调用这个
辅助而要在构造方法中生成连结?因为这样可以增加效率,如果
在构造时不能生成连结则就不能生成这个对象了,没有必要在
对象生成后再测试能不能生成连结.
*/
public void makeConnection(){
//此处的代码同构造方法,无论以后如果实现连结,都将构造方
//法的代码复制到此处.
conn = DriverManager.getConnection(url,user,passwd);
}
}
这个类就封装到这里,当然你可以在这儿增加业务方法,但如果要修改连结的实现,
整个类都要重新编译,因为业务方法和应用层次无关,代码一经生成不易变动,所以独立封装.
以下我们实现业务方法:
package com.imnamg.axman.beans;
import java.sql.*;
import ..................
public class DBOperater extends ConnectionFactory{
//private Statement stmt;
//private ResultSet rs;
//为什么要注释成员变量stmt和rs,基础部分已经说过,如果声明为成员变量,
//在关闭conn时可以显示地先关闭rs和stmt,别的没有任何好处,而显示关
//闭只是说明你编程风格好,但综合考虑,我们要生成多个stmt或不是类型的
//stmt就不能声明为成员方法,否则引用同一对象,所以我们要业务方法中生
//成stmt对象.不仅可以同时处理多个结果集,还可以提高性能和灵活性.
public ResultSet executeQuery(String sql) throws SQLException{
if(conn==null || conn.isClosed())
makeConnection();
Statement stmt = con.createStatement(
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_READ_ONLY);
//对于一般的查询操作,我们只要生成一个可流动的结果集就行了.
//而对于在查询时要更新记录,我们用另一个业务方法来处理,这样,
//这样可以在普通查询时节省回滚空间.
ResultSet rs = stmt.executeQuery(sql);
return rs;
}
public ResultSet executeUpdatabledQuery(String sql) throws SQLException{
if (con == null || con.isClosed())
makeConnection();
Statement stmt = con.createStatement(
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_UPDATABLED);
//可更新的结果结要更大的回滚空间,普通查询时不要调用这个方法
ResultSet rs = stmt.executeQuery(sql);
return rs;
}
/**
基于同上的原因,在执行更新操作是我们根本不要任何回滚空间,所以建立
一个基本类型的stmt,实现如下
*/
public int executeUpdate(String sql) throws SQLException{
if (con == null || con.isClosed())
makeConnection();
Statement stmt = con.createStatement();
//这个stmt在执行更新操作时更加节省内存,永远记住,能节省的时候要节省
//每一个字节的内存,虽然硬件设备可能会有很大的物理内存,但内存是给用
//户用的而不是给程序员用的(!!!!!!!!!!!!!!!!!!)
int s = stmt.executeUpdate(sql);
return s;
}
//以上实现了常用功能,还有两个通用的功能也是"共性"的,我们一起在这个封装类
//中实现:
public PreparedStatement getPreparedStmt(String sql) throws SQLException{
if (con == null || con.isClosed())
makeConnection();
PreparedStatement ps = con.prepareStatement(sql);
return ps;
}
public CallableStatement getCallableStmt(String sql) throws SQLException{
if (con == null || con.isClosed())
makeConnection();
PreparedStatement ps = con.prepareCall(sql);
return ps;
}
//记住:对于封装类而言预编译语句和存储过程调用应该从连结中返PreparedStatement
//和CallableStatement供调用者处理而不是返回它们的处理结果.也就是说封装类只封
//装了它们的连结过程.最后再次声明,一定要有一个close()方法供调用者调用,而且告
//诉调用者无论如果要调用这个方法:
public void close() throws SQLException{
if(conn != null && !conn.isClosed())
conn.close();
}
//这个方法最好放在ConnectionFactory中,这样可以直接调用来只测试连结.而不用再
调用子类来关闭
}
OK,我们已经实现了数据库常用操作的封装,注意这些业务方法都是把异常抛给调用者而没有用
try...catch来处理,你如果在这里处理了那么调用者则无法调试了.对于特定的数据库的特殊操作,不要封
装到此类中,可以再从这个类继承,或直接从ConnectionFactory类继承,当然最好是从这个业务类中继承,
这样不仅可以调用特殊方法也可以调用共性的业务方法,兴一个例子,我在应用Oracle时要把XML文件直接
存到数据数和把数据直接读取为XML文件,那么这两个方法只对Oracle才用到,所以:
package com.inmsg.axman.beans;
import java.sql.*;
import oracle.xml.sql.query.OracleXMLQuery;
import oracle.xml.sql.dml.OracleXMLSave;
public class OracleDBOperater extends DBOperater{
public OracleXMLQuery getOXQuery(String sql,String table) throws Exception
{
OracleXMLQuery qry = new OracleXMLQuery(con,sql);
qry.setRowsetTag(table);
qry.setRowTag("RECORD");
return qry;
}
public int insertXML(String path,String table) throws Exception
{
OracleXMLSave sav = new OracleXMLSave(con,table);
URL url = sav.createURL(path);
sav.setRowTag("RECORD");
int x = sav.insertXML(url);
sav.close();
return x;
}
}
现在,有了这样的几个"东西"在手里,你还有什么觉得不方便的呢?
虽然本处作为初级应用,但设计思想已经是JAVA高手的套路了,是不是有些自吹自擂了啊?
好的,休息一下吧.
上面有一位朋友问了,如果在已经连结的情况下,知道当前连结的库的表的情况呢?
其实只你已经连结了,你就能知道这个库中所以情况而不仅仅上表的情况:
有时(我到目前只见到过一次),我们对一种新的数据库根本不知道它的结构或者是
其中的内容,好坏么我们如何来获取数据库的情况呢?
真实的例子是这样的,我的朋友的公司接到了一个单子,对方使用的数据库是叫什么
"titanium"的,说实话由于本人的孤陋寡闻,在此之前从来不知道还有这种数据库,更别说如何
访问了,现在朋友要看里面有什么"东西",当然是一筹莫展.所以只好找我.
接到电话后,我先问他是什么平台上跑的,如果连结的,他说是在windows下可以建立
ODBC数据源,哈哈,就是说可以用java建立Connection了,OK
只能建立一下Connection,那么就可以得到这个数据库的所有元信息:
DatabaseMetadata dbmd = conn.getMetadata();然后你可以从这个对象获取以下信
息:
getUrl(); //返回与这个数据库的连结的URL,当然是已知的,要不你怎么连上去
getUserName(); //返回与这个数据库的连结的用户,同上
isReadOnly();数据库是否为只读
getDatabaseProduceName();//数据库产品名称
getDatabaseProduceVersion();//版本号
getDriverName();//驱动程序
getDriverVersion();//驱动程序版本
以上内容没有什么意义
ResultSet getTables(String catalog,
String schemaPattern,
String tableNamePattern,
String[] types)
可以得到该库中"表"的所有情况,这里的表包括表,视图,系统表,临时空间,别名,同义词
对于各参数:
String catalog,表的目录,可能为null,"null"匹配所有
String schemaPattern,表的大纲,同上
String tableNamePattern,表名,同上
String[] types,表的类型,"null"匹配所有,可用的类型为:
TABLE,VIEW,SYSEM TABLE,GLOBAL TEMPORARY,LOCAL TEMPORARY,ALIAS,SYNONYM
例如:
DatabaseMetaData dbmd = conn.getMetaData();
ResultSet rs = dbmd.getTables(null,null,null,null);
ResultSetMetaData rsmd = rs.getMetaData();
int j = rsmd.getColumnCount();
for(int i=1;i<=j;i++){
out.print(rsmd.getColumnLabel(i)+"/t");
}
out.println();
while(rs.next()){
for(int i=1;i<=j;i++){
out.print(rs.getString(i)+"/t");
}
out.println();
}
对于更详细的表中的列的信息,可以用dbmd(不是rsmd).getColumns(
String catalog,
String schemaPattern,
String tableNamePattern,
String columnNamePattern
)
不仅可以获得rsmd中的信息,还可以获得列的大小,小数位数,精度,缺省值,列在表中
的位置等相关信息.
还有两个方法,调用和获取表信息一样,可以获得存储过程和索引的信息:
ResultSet getProcedures(
String catalog,
String schemaPattern,
String procedurePattern
);
ResultSet getIndexINFO(
String catalog,
String schemaPattern,
String table,
boolean unique,boolean approximate
);
JDBC中鲜为人知的"中级应用"
在没有进入JDBC的高级应用前,我先来介绍一下一些"鲜为人知"的中级应用,说它是中
级应用,因为对JDBC有所了解的人都应该知道,说它"鲜为人知",我却没有在任何人的代
码中或我所见过的任何讲JDBC的书中有提到过,非常奇怪,这本来是JDBC规范啊.
select * from table where datetimecol < now();这在mysql中可以正确运行.
可是在另的数据库呢?
select * from table where datatimecol < TO_DATE('yyyyMMMdd');
这在oracle中可以,在mysql中可以吗?是什么让这些SQL语句不能通用?是数据库
专用涵数,那么我们要在改变数据库后除了个性Driver还要改代码吗?
有没有通用的数据库涵数?有,只是我没见过几个人用过..................
到目前为止,JDBC其实都实现了 SQL-2 ENTRY Level,而SQL-2 Transitional Level
也基本实现(我没有用过没有实现的).只有实现上面的规范,JDBC就实现以下"通用"功能.它
会把这些通用的语义,标题,存储过程,涵数转化为数据库对应的功能,所以你只要掌握这些通
用语法在JDBC中应用,就不要再为特定数据库编写特定代码:
转义:{keyword parameter}
还记得oracle中插入?吗?
insert into table values ('aaa?aaa');什么?当然参数?不得不这样
insert into table values ('aaa' | '?' | 'aaa');把?专门作为一个字符再和其
它字符连结.用通用转义:
insert into table values ('aaa/?aaa') {escape /};
存储过程(这个例是有人用过)
{call procedure_Name[args]}
对于有返回值的存储过程
{? = call procedure_Name[args]}
时间和日期:
日期:{d 'yyyy-mm-dd'}
在任何数据库中,都会把你给定的yyyy-mm-dd转化为该数据的日期形式
insert into table values ('axman',{d '2003-07-28'});
在oracle中会转换成:Jul 28,2003
时间{t 'hh:mm:ss'},时间戳:{ts 'yyyy-mm-dd hh:mm:ss.f...'}
标量涵数:
{fn function([args])}
这个可要记住了,再也不要调用数据库特定涵数了,只要
insert into table values('axman',{fn CONCAT('1970-07-','11')});就可以在所有
实现SQL-2 Transitional Level的JDBC的数据库中运行,这些涵数要主包括:
数学涵数,字符串涵数,日期涵数,系统涵数,转换涵数,因为涵数大多,我没法一个个列
出来,请你用上面刚学习的DatabaseMetadata类调用getNumericFunctions(),getStringFunctions()
getSystemFunctions(),getTimeDateFunctions().
关于数据库连结
我们所说有JDBC高级应用,并不是说它的技术含量很高(也许JAVA平台上不存在什么"技术含量"的说
法,因为JAVA是给大家用的而不是给某些人用的).说它是高级应用,是因为它是对于JDBC基础应用来
说的扩展,也就是可以优化你的应用性能,或方便于应用的实现.所以说它是一种高级应用而不叫高级
技术.
JDBC中,java.sql包是基础的,也是核心的功能,javax.sql包则是高级的,扩展的功能.所以
为了交流的方便,我们把它们区分为core API和optional API.
但是仍然然有一些core API中的功能,我把它归纳到高级应用中.象存储过程的调用,多结果集
的处理,我之所以要把这些东西拿出来说明,是目前你在网上找不到任何一份详细的文档和例程,可以
说有95%以上的开发人员都不知道真正如何处理这些工作.所以我会在回上海后详细写这一段的内容.
现在我们还是来看看optional API给我们带来的好处:
我们已经了解,执行一个SQL语句,要经过如下几步:
1.Connction
2.Statement
3.Statement.executeXXXXX();
4.可选的对结果集的处理
5.必要的Connction的关闭.(再次提醒如果你想成为中级水平以上的程序员,请你把关闭语
句写在finally块中,在通过下面的介绍后我介绍一个方法可以用来验证你的程序是否有连结
泄漏)
这其中,生成Connction对象是最最重要的工作,也是最消耗资源的,因为连结对象要驱动底层
的SOCKET,调用物理连结和数据库进行通信,所以生成,关闭,再生成这种连结对象就相当于我们在二十
世纪八十年代(1980年以后出身的不了解吧?)喝易拉罐饮料一样.你买一瓶饮料是一块二角钱,你可知道
那罐子(Connection)值一块零八分.而你喝下去的东西只值一角二分钱,这是我们那儿一个饮料厂的真实
数据.
在javax.sql包出来以前,我们只能买这样的饮料来喝,除非你不喝.也有一些人不服气自己生
产饮料(poolman),可是消费者很快发现,它只是把原来单卖的易拉罐现在打包卖给了我们,因为它还是
用原来的包装原料来生产的,poolman这种类型的连结池,其根本是从DriverManager中getConnection
出来的.真正的效率如果不是你心理作用的话,也许比单个连结还要低!!!以及一些江湖好汉的"杰作",
都无法跳出这个框框.我自己在那一段时间也曾醉心于研究这些"连结池",因为谁都可以把别人的原码
读过后,再加上其他人的优点写出一个更好的来,可是,大家可以看到,我写出了好用的Upload,DownLoad,
HtmlUtil,Encoder,Decoder等一系列工具.可是我没有写出成功的连结池.......
我们再来深入一步,为什么DriverManager生成的连结和基于它的连结池不能真正提高性能.
DriverManager对象中,绝大多数的JDBC是封装了一个物理连结,也就是它抓住了一个和数据库通信的
Socket,当你使用DriverManager.getConnection()时也就是有一个和数据库连结的Socket让你占用了.
而且这个方法是同步的.大家知道这样的物理连结对于任何系统是有限制的,比如一个WEB服务器一般
最大并发是150到250之间,数据库服务器也是这样的道理,你不仅要考虑你的程序不能用光连结,还要
考虑不同Runtime中或其它应用程序也在同时和你一起使用这些物理连结,如果一台服务器上有JAVA
WEB SERVER,还有一个C的应用程序也访问数据库,你不能那么无礼地要求人家C程序员他的程序必须
等你的JAVA调用空闲才能访问数据库吧.所以物理连结是极其宝贵的.
基于DriverManager.getConnection()的连结池只不过是预先生成这样的物理连结放在一个
pool中,然后编号等你调用,它省略的是生成这样的连结的时间.注意你得到的连结在你没有释放之前,
它无法处理别的工作,因为连结句柄在用户手中,另外这种连结池调用时是由程序调用者初始化的,
每一次调用都必须有初始化工作,而调用者是否以优化的方法去运行它,完成还要看每个人的编程水
平.另一方面,如果这种连结池是如果用于WEB容器管理,那简单是垃圾,因为它强迫使用静态变量来
保持连结,容器根本无法做到访问控制.而且它不能在不同Runtime中被调用.
而javax.sql的实现采用了在用户连结和物理连结中间加一个缓冲的中间层,它虽然也只生
成30个物理连结,但用户本身不能访问它,DataSource返回给用户的是一个JAVA抽象对象,客户程序把
连结请求放回缓冲中由DataSource统一调度物理连结来处理,这样可以最大程序利用宝贵的物理连结
也许现在的30个物理连结仍然不够负载,你仍然需要修改实际连结数,但我们知道,我们现在的这种连
结方式已经不是DriverManager.getConnection()能比的了.也就是说,在同样多的物理连结下,
DataSource可以给我们更多(是多得多的)的调用机会,其实,正常情况下一个从DriverManager中
getConnection()出来物理连结的负载量只有百分之几,就是因为你的调用抓住了它的句柄而不能让
它很好地工作.另外程序员只能返回它而不能关闭它,因为传统连结池中连结对象一旦由用户关闭,
就要再次重新生成物理新的连结,所以用户只能释放,对于非连结池和连结池得到的连结对象,
要用不同的代码编程,简单是一种痛苦.一旦没有注意,在处理完数据后不是释放而是关闭,这个错误
到底是谁的过错?????????????
我上面说java.sql实现绝大多数是得到物理连结,也有例外,但不是那些以前的连结池,而是
OSE,就是oracle的servlet环境,它是把servlet服务器实现在数据库的地址空间上,servlet服务去调
用数据库根本没有通过传统的连结,因为数据是"敞开"的,这就象通过网络访问其它计算机文件和访问
本地文件的区别. 虽然它也提供标准的JDBC接口让你调用,但它底层根本不是用JDBC封装的.
DataSource的另外一个优点就是它完全实现数据库和应用程序的分离,如何配置服务器生成
DataSource的引用和程序开发无关,你在程序开发中,只要通过JDNI查找DataSource的逻辑名称就行.
而DriverManager.getConnection()中,你不得不把数据库驱动程序名,访问地址,用户,密码写在你的
应用程序中,即使可以从配置文件中读取这些属性串,而不同的服务器配置文件的路径你都要修改.而
DataSource提供了标准的配置.
说来说去,如何用DataSource来连结数据库?
非常简单:
DataSource ds = (DataSource)new InitialContext().lookup("jdbc/mydb");
Connection conn = ds.getConnection();
当然我是为了说明它简单故意把它的异常给忽略了.实际应用中.你应该捕获它的异常.
如果你还不明白什么是JDNI,我劝你先找一些这方面的资料看看,这可以是网编程方面的
基础协议啊.
关于DataSource ds = (DataSource)new InitialContext().lookup("jdbc/mydb");有几点
需要说明的是,你只要配置好你的服务器(tomcat,resin,weblogic)这些服务器中都有一个例子,如果
你不是很懂,你先把那个例子改动几个字就行了.用一段时间你就会慢慢理解它们代表什么了.
然后你在容器环境下调用new InitialContext().lookup("jdbc/mydb")容器就会自动找到那个
DataSource对象给你调用,这个过程对用户来说是透明的.
那边那个聪明的朋友已经问了,因为DataSource的属性是已经配置好的放在容器中的,那我
不在容器环境下,比如一个独立的application,我如何能取到DataSource呢?
其实,如果你能知道new InitialContext()时,容器调用了哪些默认的配置,你就可以把
这些配置参数手工加进去而不依赖容器环境了.好在InitialContext可以getEnvironment() ,在生
成这个对象后你可以get一下看看,把这些参数记下来,以后在没有这些参数的环境下put进去.
这里多几句话,谈一下学习方法,我在国内主持几个论坛(不多,两三个),从没有问过别人什
么问题,java技术又不是我发明的,不可能我什么都懂,一是问了好象有损于"高手"风范(哈哈,其实
真正的高手还是要问别人的,只有我这种假高手才不会问别人).另一方面是我根本不必问别人,比如
象application下,连结不同厂家的DataSource要put什么东西进去呢?一是去他们的网站看资料,虽然
我的英语水平只有大家的10%,但我上英文网站的次数可能比你们多.二是看有没有什么共用的API能
得到,好在可以getEnvironment(),但假如没有这个方法呢?这就要看你的学习态度了,有人会这论坛
叫"高手球命",还有什么"急用,在线等待"什么的.而我,会把new InitialContext()反编译出来看看
它调用了什么(孔子说,为了学习的目的,反编译是允许的,甚至说是伟大的,光明的,正确的思想,是有
道德的,脱离了低级趣味的,有益于人民的行为!!!----孔子语录补集第123页第4行,1989年10月版)
如果一个对象在构造时要求有参数,而它又有一个没有参数的重载的构造方法,你想想它肯定在没有
参数的构造方法中调用了默认参数,你要做的就是把它们打印出来.有多少人是这样做的?
jdbc optional API的其它扩展功能:
javax.sql不仅仅是在性能上的提高,而且它还支持分布式事务,在传统的连结过程中,我们
可以在一个连结过程中,setAutoCommit()为fasle,然后通过rollback()或commit()那回滚和提交事
务,这种在一个连结过程中的事务称为本地事务,但假如在一个事务中要对多个数据库操作,或多过
Servlet参与操作,那就必须使用分布式事务.
关于JDBC的事务我会放在下面来介绍,一个值得庆贺的功能出来了,就是事务保存点已经
JDBC3.0中实现,以前,如果我们把事务原子A,B,C放在一个事务中,如果A,B执行了,C失败,我们只能
把A,B都回滚了,但现在我们可以先把A,B保存为一个点,然后以这个点为回滚或提交,这就象在用WORD
编写文章时我们可以在不同的时候保存一个副本,而不会要么一字没有了,要么就是当前编辑的状态.
现在我们来优化我们在基础知识中实现的Bean,今天在家,没法上论坛,上次写的连结部分
叫什么名字忘记了,现在我们就叫它PooledDB吧.
当时我们已经把那个Bean分为三个部分,把生成连结部分独立出来了,而业务方法部份和扩
展部分根本不要动它,这就是继承的好处:)
package com.inmsg.beans;
import javax.naming.*;
import javax.sql.*;
public class PooledDB {
Connection con = null;
private String source = "";
public PooledDB() throws Exception {//默认构造方法,如果构造时不加参数,连结jdbc/office
source = "java:comp/env/jdbc/office";
Context ct = new InitialContext();
DataSource ds = (DataSource) ct.lookup(source);
con = ds.getConnection();
}
//然后增加重载方法,用来连结其它的数据源
public PooledDB(String source) throws Exception {
this.source = source;
Context ct = new InitialContext();
DataSource ds = (DataSource) ct.lookup(source);
con = ds.getConnection();
}
//注意一定要先把source赋给成员变量this.source,因为下面还有一个makeConnection()
//辅助方法,如果不把source赋给this.source,则makeConnection()调用默认的source字符串
private void makeConnection() throws Exception {
Context ct = new InitialContext();
DataSource ds = (DataSource) ct.lookup(source);
con = ds.getConnection();
}
//现在我们把close()方法拿到父类来实现,这是经过综合考虑的,它是一个业务方法,无论是什么
//方式取得连结,它本身不会修改,但为什么还封装到父类中呢?因为这样可以用一个独立的父类来
//做连结测试,如果我只想试一下数据库能不能连结,我就不必再引用子类,直接用这个父类就行了
public void close() throws Exception{
if(con != null && !con.isClosed()) con.close();
}
}
一般来说,构造方法尽量捕获异常处理而不要抛出异常,但作为Bean的实现,捕获异常调用者不容易
看到异常信息,所以抛给调用者处理,另外这个类又要在应用程序中调用,又要考虑作为Bean调用,所以一定
要有一个无参数的构造方法,否则不能作为javaBean调用.把异常抛出给调用者的另一目的,我在设计时是这
样考虑的,就是强迫你一定在使用try{}catch(){}块,这样你就会想到再加一个finally{},再次提醒,一定要
以下面的形式来调用你数据库连结的Bean或封装类:
PooledDB pd = null;
try{
pd = new PooledDB();
....................
}
catch(Exception e){}
finally{try{pd.close();}catch(Exception ex){}}
如果要测试你的数据库连结是否有泄漏,请你把DataSource中最大连结数设为1,只用一个连结的情
况下,如果你的程序中哪一处没有关闭连结,则下面的程序就不能再访问,然后从头到尾测试你的程序吧,一旦
发现不能访问数据库了,就查看刚才访问的代码,这样所有程序测试后,就可以放心了,一般来说我是不用这么
测试的,因为我在写数据库连结时是没有生成对象就写好close:
PooledDB pd = null;
try{}
catch(Exception e){}
finally{try{pd.close();}catch(Exception ex){}}
然后原在try{}的花括号中回车加上pd = PooledDB();和业务代码的 :)当然,你们都比我聪明用不
着这样死套也不会忘记close()的.
不要太高兴,到目前为止你仍然还没得到一个最好的解决方案,以下我们还会对这个数据库连结的
Bean(类)不断优化的..................................
好了,javax.sql的连结先说到这儿吧,今天是周六,出去玩一会了(广州街头上没有什么美女!)
本来想继续谈JDBC的高级连结方式,事务模式.但发现关于大对象存储有很多人在问,所以
先来插入一节关于大对象存储的内容,然后再接着原来的思路写下去.
JDBC的大对象存储听起来复杂,其实如果你明白了原理以后,就非常简单,网上有关这方面的
教材很少,而SUN的文档中,我从1.2开始看到一在仍然是错误的,不知道写文档的人长脑子没
有,就那几行代码你试试不就知道了,这么多次重抄下来还是错误的.
大对象分类:一般来说,大对象分为:大的文本对象,比如一个很长的文本(请你要注意什么是
文本文件,什么是二进制文件)文件,或者是你定义的一个长字符串,比如你定义了:
String s = "我们要去吃饭了......................然后睡觉!";
从吃饭到睡觉中间省略了实际的10000000000000字,虽然你不会真的定义这么称的String,但
有时会从什么地方得到这样的String,要写到数据库中.
另一种就是大的二进制对象,象执行文件,图象文件等,注意,word,excel,ppt这些"带格式"的文
档都应该以二进制对象存储.
一般来说,数据库如果支持大对象存储,会有这几种类型的SQL数据类型:
BLOB,CLOCB,NLOB,也有的数据数只有一种BLOB,基本上是这样的:BLOB用来存放二进制文件,而
CLOB用来存放文本文件,NLOB是对多字节文本文件支持.假如你的文本文件是纯英文的,放在
BLOB中当然可以,也就是说它是以byte格式存储的,而多字节是以CHAR格式存储的.
同样对于这几种类型的文档,有几种相对应的存取方式:
setter:
利用PreparedStatement的setXXX方法,
setAsciiStream()方法用于写入一般的文本流.setBinaryStream()方法用于写入二进制流
而setUnicodeStream()用于写好UNICODE编码的文本,与此相对应的ResultSet中三个getter方法
用于取回:getAsciiStream(),getBinaryStream(),getBinaryStream().
对于文件本身,要把它作为一个流,只要new InputStream(new FileInputStream("文件路径"))
就可以了,但对于大的String对象,你不会写入文件再转换成输入流吧?
new StringBufferInputStream(String s),记住了.
JDBC2以后提供了java.sql.BLOB对象,我不建议大家使用它,一是很麻类,二是容易出错,要先插
入一个空的BLOB对象,然后再填充它,实在没有必要,直接setXXX就行了,我试过,至少mysql,
oracle,sql server是可以直接set的.
好了,我们先看一个例子如何写入文件到数据库:
数据结构:
create table test(
name varchar(200),
content BLOB
);
File f = new File("a.exe");//先生成File对象是为了取得流的长度.FileInputStram可以直接
//传入文件路径
InputStream in = new InputStream(new FileInputStream(f));
PreparedStatement ps = conn.prepareStatement("insert into test (?,?)");
ps.setString(1,"a.exe");
ps.setBinaryStream(2,in,(int)f.length());
ps.executeUpdate();
f的长度一定要做从long到int的转换,SUN的文档中好几版都没有改过来.就这么简单,当然,不同的
数据库存本身要设置它允许的最大长度,MYSQL默认只能传1M的文件,要修改参数原能存更大的文件.
如果要从数库中取得文件:
PreparedStatement ps = conn.prepareStatement("select * from test where name=?");
ps.setString(1,"a.exe");
ResultSet rs = ps.executeQuery();
if(rs.next()){
InputStream in = rs.getBinaryStream("content");
}
得到in对象后,你可以进行任何处理,写向文件和写向页面只是out对象不同而已:
写向文件:
DateOutputStream out = new DateOutputStream(new FileOutputStream("b.exe"));
写向页面:
response.reset();
response.setContType("类型");
ServletOutputSreamt out = response.getOutputSream();
得到out对象后,就可以输出了:
byte[] buf = new byte[1024];
int len = 0;
while((len = in.read(buf)) >0)
out.write(buf,0,len);
in.close();
out.close();
对于向页面输入,要设置什么样的ContType,要看你想如何输出,如果你想让对方下载,就设为
"application/octet-stream",这样即使是文本,图象都会下载而不会在浏览器中打开.如果你要想
在浏览器中打开,就要设置相应的类型,还要在容器的配置文件中设置支持这种文档类型的输出,但
对于很多格式的文件,到底要输出什么类型,其实就是HTTP的MIME集,比如图片:image/gif,当然你如
果你的文件扩展名(ext)不确定,你也不要用if(ext.equals("gif"))......这样来判断,我教你一个
技巧,我之所以说是技巧,是我没有在别的地方发现有人用这种方法,对我来说我是绝对不会把别人的
方法拿来说是我的技巧的:
构造一个file类型的URL,我们知道URL目前JAVA可以支持HTTP,FTP,MAILTO,FILE,LDAP等,从FILE类型
的URL就可以得到它的MIME:
URL u = new URL("file://a.exe");
String mime = u.openConnection().getContentType();
这样你就可以直接response.setContType(mime);而不用一个一个类型判断了.
好了,大对象存储就说到这儿,不同的数据仍然和些特殊的规定,不在此一一列举了.
JDBC分布式事务:
在没有开始JDBC分布式事务前,先来回顾一下J2EE平台的数据源的整体构加:
在上面的介绍中,可能大家会过于限入理解如何实现ConnectionPoolDataSource和PooledConnection,而忘记了它的整体
结构.为了帮助大家更快地理解,我从以下几点进行总结.
声明:本文完全是作者根据SUN的文档从实践中总结,没有参考(事实上目前我还没有找到这方面的参考)任何文章:
1. DataSource和ConnectionPoolDataSource,PooledConnection,Connection的关系:
Sun的文档中只对ConnectionPoolDataSource接口作了一般性规定,说明它是PooledConnection的工
厂,即ConnectionPoolDataSource是传统的连结池角色,它负责产生物理连结PooledConnection.而PooledConnection又
是Connectio的工厂,一个PooledConnection对象负责产生多个Connection对象供应用程序调用.而DataSource是对上面两
个过程的包装,在DataSource中不仅要实现传统连结池ConnectionPoolDataSource来产生物理连结PooledConnection,还要
实现通过每个PooledConnection工厂来产生Connection,最后DataSource通过公开方法返回给调用者的是经过两次工厂
出来的Connection.如果我们先不考虑分布式事务,只看下图左边,就是说工厂ConnectionPoolDataSource生
成PooledConntion,二级工厂PooledConntion生产Connection,这两个过程由DataSource在内部包装,只提供最后的产
品Connection.
2. DataSource是服务端数据源,而传统的连结池是客户端数据源:
传统的连结池要调用者生成这个连池的实例,完成初如化,这样一个数据库为了防止连结池的实例生成无限多个物
理连结,就要对保存物理连结的数据结构进行静态定义,否则,你在你的程序中生成一个连结池对象,它生成30个物理
连结,我又在我的程序中生成连结池实例,又生成30个物理连结,那就无法控制了,所以保存这30个物理连结的数据结
构必须是静态的.
而DataSource同一个对象初始化后,对象被绑定到jdni服务器上,通过jdni得到的是它的代码存根,其中只包
含Connection,而物理连结是不可能序列化的,所以不会被重新生成,调用者通过Connection对象作为参数传给服务端,
由它来操作实际的物理连结.
思考一下:如果不考虑性能问题,我是否可以把PooledConnction不再二次工厂化,只把PooledConntion作为Connection作
为DataSource产品返回给调用者?
答案是不可以,因为物理连结不能序列化,也就是无法进行分布式引用.二次工厂化不仅解决了性能的问题,也同时解
决了分布式调用的问题.
3. 为什么说二次工厂化增加性能?
对于产生物理连结,没有什么区别,但物理连结本身并没有满负载工作,也就是一个物理Connection(TMD,我现在也不
好说Connection还是PooledConnectio,以前的Connection就是DataSource中的PooledConnection)其实可以同时绑定更多
的Statement,而如果它直接给调用者调用了,句柄就被调用者拿去了,在调用者没有返回时别的Statement没法和它进
行”联系”.而二次工厂的目的就是把多人的Statement通过”新的引用Connection”和物理连结绑定,使它更好地工作.
举个例子,汽车这种东西,在目前的中国还是很昂贵的,作为客户(调用者)我有几件货物要运,但一辆汽车(物理连结)
如果我一用,别人就不能用了.(传统连结池和连结),尽管它还可以装更多的货物,现在汽车公司只能你一个车号(新的
Connection),不给你实际的汽车,你只要把你的几件东西只交给这个车号,而其他人也可能同时把几件东西交给这个
车号,最尽有更多的货物因为使用同一车号而使那个物理汽车装载了更多的货物,当然如果它满了的话会产生另一
辆车,如果生产的辆达到规定的数目你只好等等了,但这样把多个客户的货物和同一车号关联使汽车能更多地处理
事务,明显地增加了性能.
理解了以上的结构,我们就不难理解javax.sql对分布式事务的支持,当然,如果你对事务本身还不理解,那我就没办
法让你理解以下的知识,因为我不可能再停下来讲什么是事务.它是和JDBC相同级别的内容,也许在别的地方我会再
讲.
从上面的结构中右边看到,在DataSource中,其实封装了两种工厂,这两种工厂都是两层次的,其实XADataSource的
作用和ConnectioPooledDataSource一样,都是产生物理连结的,只不过它产生支持分布式事务的物理连结XAConnectio而
已,(以后记住,凡以XA命名的类都是支持分布式事务的标记.)我们看到,XAConnection中getConnection()出来的连结
和PooledConnection中getConnection()出来的连结没有区别,而Connection是DataSource的最终产品,这意味作什么?
这意味着支持分布式事务的过程由DataSource来做,你要操作的Connection和平时没有两样,你只要声明事务的开
始和事务提交就行了!
要使你的连结支持分布式事务,你要在DataSource的配置中指明type是XADataSource就行了.然后申请一个一务(为
了说明方便省略了try{}catch(){})
UserTransaction ut = ...........;
ut.begin();
Connection con1 = .........;
Connection con2 = .........;
Connection con3 = .........;
if(条件) ut.setRollbackOnly();
con1.close();
con2.close();
con3.close();
考察一下,为什么XADataSource类型的物理工厂会产生的连结可以直接被事务管理呢?其实这就是封装的好处了,
在XADataSource产生XAConnection时,这个XAConnection实际是PooledConnection的子类,它扩展了一个getXAResource() 方
法,事务通过这个方法把它加入到事务容器中进行管理.对于调用者来说,根本看不到事务是如果管理的,你只要声明
开始事务,告诉容器我下面的操作要求事务参与了,最后告诉事务说到这儿可以提交或回滚了,别的都是黑箱操作,不
要你来做.
当然如果没有分布式事务的需求,虽然XADataSource可以用于本地事务,但它要做很多资源测试,是一种浪费.
最后要说明的是,既然你把操作交给事务来做,你就要对他放心,事务边界由容器管理,你只在最后确定是提交还是
回滚还是强行回滚setRollbackOnly()(强行回滚后不可以再提交).你不要在事务中调用某一连结的rollback,commit,也不
能把Connection设为自动提交,一般来说当你声明为支持分布式事务的DataSource时,创建的连结默认都是关闭自动
提交的,只是你自己不要打开它.
因为SUN的文档只对DataSource接口作了一般规定,并没有规定具体算法,所以我们在清楚上面的结构后,可以实现
不依赖容器的DataSource(其实只是它的思想.因为你写出来的不依赖容器的DataSource)已经不是这个意义上
的DataSource了.它不能绑定到服务器上让远程引用,所以生成物理连结的工厂应该是静态的,而物理连结这种产品
也应该是静态的.然后再生成多个引用连结.但这好象没有多大意义,因为纯客户端软件一般来说不可能同时有上万
个客户在线访问的,根本用不着这么费事地实现连结池.