数据库编程
4.1 JDBC的设计
JDBC遵循了微软公司非常成功的ODBC模式,ODBC为C语言访问数据库提供了一套编程接口。JDBC和ODBC都基于同一个思想:根据API编写的程序都可以与驱动管理器进行通信,而驱动管理器则通过驱动程序与实际的数据库进行通信。
JDBC驱动程序类型
- 第1类驱动程序将JDBC翻译成ODBC,然后使用一个ODBC驱动程序与数据库进行通信。
- 第2类驱动程序是由部分Java程序和部分本地代码组成的,用于与数据库的客户端API进行通信。在使用这种驱动程序之前,客户端不但需要安装Java类库,还需要安装一些与平台相关的代码。
- 第3类驱动程序是纯Java客户端类库,它使用一种与具体数据库无关的协议将数据库请求发送给服务器构件,然后该构件再将数据库请求翻译成数据库相关的协议。这简化了部署,因为平台相关的代码只位于服务器端。
- 第4类驱动程序是纯Java类库,它将JDBC请求直接翻译成数据库相关的协议。
JDBC的典型用法
- 在传统的客户端/服务器模式中,通常是在服务器端部署数据库,而在客户端安装内容丰富的GUI界面。在此模型中,JDBC驱动程序应该部署在客户端。
- 但是,如今全世界都在从客户端/服务器模式转向“三层应用模式”,甚至更高级的“n层应用模式”。在三层应用模式中,客户端不直接调用数据库,而是调用服务器上的中间件层,最后由中间件层完成数据库查询操作。这种三层模式有以下优点:它将可视化表示(位于客户端)从业务逻辑(中间件)和原始数据(数据库)中分离开来。客户端和中间层之间的通信可以通过HTTP,或诸如远程方法调用RMI这样的机制来完成。JDBC负责在中间层和后台数据库之间通信。
4.2 结构化查询语言
- 按照惯例,SQL关键字全部使用大写字母。
- 在Books中查询价格小于等于29.95的书的ISBN、价格和书名:
SELECT ISBN,Price,Title FROM Books WHERE Price<=29.95
- WHERE子句也可以使用LIKE操作符来实现模式匹配。不过这里的通配符不是通常的*和?,而是用%表示0或0以上个字符,用下划线表示单个字符:
SELECT ISBN,Price,Title FROM Books WHERE Title NOT LIKE '%n_x%'
这句语句排除了所有书名中包含UNIX或linux的图书。注意,字符串都是用单引号括起来的,而非双引号。字符串中的单引号则需要用一对单引号代替:SELECT Title FROM Books WHERE Title LIKE '%''%'
这个语句返回所有包含单引号的书名。 - 查询与出版社相匹配的图书:
SELECT * FROM Books,Publishers WHERE Books.Publisher_Id=Publishers.Publisher_id
- 将所有书名中包含“C++”的图书降价5美元:
UPDATE Books SET Price=Price-5.00 WHERE Title LIKE '%C++%'
- 删除所有C++图书:
DELETE FROM Books WHERE Title LIKE '%C++%'
- 向表中插入值:
INSERT INTO Books VALUES('A Guide to the SQL Standard','0-201-96424-0','0201',47.95)
- 创建新表并指定列名和数据类型:
CREATE TABLE Books (Title CHAR(60),ISBN CHAR(13),Publisher_Id CHAR(6),Price DECIMAL(10,2))
4.3 JDBC配置
首先,需要有一个可获得JDBC驱动程序的数据库软件,如IBM DB2、Microsoft SQL Server、MySQL、Oracle。
数据库URL
- 在连接数据库时,必须使用各种与数据库类型相关的参数,例如主机号、端口号和数据库名。
- JDBC使用了一种与普通URL类似的语法描述数据源:
jdbc:subprotocol:other stuff
如jdbc:derby://localhost:1527/Books;create=true
其中,subprotocol用于选择连接到数据库的具体驱动程序。other stuff随使用的subprotocol不同而不同。
驱动程序jar文件
- 需要获得包含了你所使用的数据库的驱动程序的JAR文件。如果使用的是Derby,就需要derbeclient.jar。如果是其他的数据库,就需要寻找恰当的驱动程序。
- 在运行访问数据库的程序时,需要将驱动程序的JAR文件包含到类路径中。
注册驱动器类
- 某些JDBC的JAR文件将自动注册驱动器。包含META-INF/services/java.sqlDriver文件的jar文件可以自动注册驱动器类,解压缩驱动程序jar文件就可以检查是否包含该文件。
- 如果驱动程序jar文件不支持自动注册,那就需要找出数据库提供商使用的JDBC驱动器类的名字。典型的驱动器名如下:org.apache.derby.jdbc.ClientDriver org.postgresql.Driver。
- 通过使用DriverManager,可以用两种方式来注册驱动器。
- 一种是在Java程序中加载驱动器类:
Class.forName("org.postgresql.Driver");
这条语句将使得驱动器类被加载,由此将执行可以注册驱动器的静态初始化器。 - 另一种是设置jdbc.drivers属性。可以用命令行参数来指定这个属性:
java -Djdbc.drivers=org.postgresql.Driver ProgramName
或者在应用中用下面的调用设置系统属性:System.setProperty("jdbc.drivers","org.postgresql.Driver");
在这种方式中可以提供多个驱动器,用冒号分隔开。
连接到数据库
在Java程序中,使用以下代码打开一个数据库连接:
String url="jdbc:postgresql:COREJAVA"
String username="dbuser"
String password="secret"
Connection conn=DriverManager.getConnection(url,username,password);
需要数据库的连接信息,可以放在一个database.properties的文件中,连接到数据库的时候加载。下面是一个连接到PostgreSQL数据库的示例:
jdbc.drivers=org.postgresql.Driver
jdbc.url=jdbc:postgresql:COREJAVA
jdbc.username=dbuser
jdbc.password=secret
调试与JDBC相关的问题时,有种方法是启动JDBC的跟踪特性。调用DriverManager.setLogWriter方法可以将跟踪信息发送给PrintWriter,而PrintWriter将输出JDBC活动的详细列表。大多数JDBC驱动程序的实现都提供了用于跟踪的特性,例如,在使用Derby时,可以在JDBC的URL中添加traceFile选项:jdbc:derby://localhost:1527//COREJAVA;create=true;traceFile=trace.out
4.4 执行SQL语句
- 在执行SQL语句之前,首先需要创建一个Statement对象。要创建Statement对象,需要使用调用DriverManager.getConnection方法所获得的Connection对象。
Statement stat=conn.createStatement();
- 接着,将要执行的SQL语句放入字符串中,调用Statement接口中的executeUpdate方法:
stat.executeUpdate(command);
executeUpdate方法将返回受SQL命令影响的行数。 - executeUpdate方法既可以执行诸如INSERT、UPDATE和DELETE之类的操作,也可以执行CREATE TABLE 和DROP TABLE之类的数据定义语句。但是,执行SELECT查询时必须使用executeQuery方法。另外还有一个execute语句可以执行任意SQL语句,此方法通常用于用户提供的交互式查询。
- executeQuery方法返回一个ResultSet类型的对象,可以通过它来每次一行地迭代遍历所有查询结果。
ResultSet rs=stat.executeQuery("....");
分析结果集时通常可以用如下的循环:while(rs.next()){...}
- ResultSet接口的迭代协议和java.util.Iterator接口不同。对于ResultSet,迭代器初始化时被设定在第一行之前的位置,必须调用next()将它移动到第一行。另外,它没有hasNext()方法,我们需要不断地调用next,直至该方法返回false.
- 结果集中行的顺序是任意的。除非使用ORDER BY子句指定行的顺序,否则不能为行顺序强加任何意义。
- 查看每一行时,可以用访问器方法获取每一行的信息。
String isbn=rs.getString(1);
double price=rs.getDouble("Price");
不同的数据类型有不同的访问器。每个访问器都有两种形式,一种接受数字型参数,另一种接受字符型参数。当使用数字型参数时,指的是所在的列。如rs.getString(1)
返回当前行中第一列的值。(与数组的索引不同,数据库的列序号是从1开始的) 当使用字符型参数时,指的是以该字符串为名的列。
管理连接、语句和结果集
- 每个Connection对象都可以创建一个或多个Statement对象。同一个Statement对象可以用于多个不相关的命令和查询。但是,一个Statement对象最多只能有一个打开的结果集。如果需要执行多个查询操作,且需要同时分析查询结果,必须创建多个Statement对象。
- 有些数据库的JDBC驱动程序只允许同时存在一个活动的Statement对象。使用DatabaseMetaData接口中的getMaxStatements可以获取JDBC驱动程序支持的同时活动的语句对象总数。
- 实际上,通常不需要同时处理多个结果集。可以采用组合查询,这样就只需要分析一个结果。对数据库进行组合查询的比使用Java程序遍历多个结果集要高效得多。
- 使用完ResultSet、Statement或Connection对象后,应立即调用close方法。这些对象都使用了规模较大的数据库服务器上的有限资源。如果Statement对象上有一个打开的结果集,那么调用close方法将自动关闭该结果集。同样地,调用Connection类的close方法将关闭连接上的所有语句。
- 在Java SE7中,可以在Statement上调用closeOnCompletion方法,在其所有结果集都被关闭后,该语句会立即被自动关闭。
- 如果所用连接都是短时的,那么无需考虑关闭与语句和结果集。只需将close语句放在带资源的try语句中,以确保最终连接对象不可能继续保持打开状态。(应该使用带资源的try语句块关闭资源,并使用一个单独的try catch语句处理异常。分离try语句块可以提高代码的可读性和可维护性。)
分析SQL异常
- 每个SQLException都有一个由多个SQLException对象构成的链,这些对象可以通过getNextException方法获取。这个异常链是每个异常都具有的由Throwable对象构成的“成因”链之外的异常链。可以使用循环迭代这两个链:
for(Throwable t: SQLException){ do something with t}
- 可以在SQLException上调用getSQLState和getErrorCode方法来进一步分析。
- 另外,数据库驱动程序可以将非致命问题作为警告报告。SQLWarning是SQLException的子类,可以调用getSQLState和getErrorCode获取警告信息。与SQL异常类似,警告也是串成链的。要获取所有的警告,可以使用一下循环:
SQLWarning w=stat.getWarning();
while(w!=null)
{
do something with w
w=w.nextWarning();
}
- 当数据从数据库中读出并被意外截断时,SQLWarning的DataTruncation子类就派上用场了。如果数据截断发生在更新语句中,那么DataTruncation将被当做异常抛出。
4.5 执行查询操作
预备语句(prepared statement)
- 没有必要每次开始查询时都建立新的查询语句,而是准备一个带有宿主变量的查询语句,每次查询时只需填入不同的字符串就可以反复使用该语句。通过事先准备好查询并多次重用它,我们就可以确保查询所需的准备步骤只被执行一次。
- 在预备查询语句中,每个宿主变量都用”?”表示。如果存在一个以上的变量,那么在设置值的时候必须注意”?”的位置。
- 在执行预备语句之前,必须使用set方法将变量绑定到实际的值上。和ResultSet接口中的get方法类似,针对不同数据类型也有不同的set方法。如:
stat.setString(1,publisher);
第一个参数是指需要设置的宿主变量的位置,位置1表示第一个”?”。第二个参数是赋予宿主变量的值。 - 如果想要重用已经执行过的预备查询语句,除非使用set方法或调用clearParameters方法,否则所有宿主变量的绑定都不会改变。这就意味着,在从一个查询到另一个查询的过程中,只需使用setXxx重新绑定那些需要改变的变量即可。
- 一旦为所有变量绑定了具体的值,就可以执行查询操作了:
ResultSet rs=stat.executeQuery();
读写LOB
- 除了数字、字符串和日期之外,许多数据库还可以存储大对象,例如图片或其他数据。在SQL中,二进制大对象称为BLOB,字符型大对象称为CLOB。
- 要读取LOB,需要执行SELECT语句,然后在ResultSet上调用getBlob或getClob方法,这样就可以获得Blob或Clob类型的对象。要从Blob中获取二进制数据,可以调用getBytes或getInputStream。例如,获取图像:
if(rs.next()){
Blob pic=rs.getBlob(1);
Image picImage=ImageIO.read(pic.getBinaryStream());
}
- 类似的,如果获取了Blob对象,就可以通过调用getSubString或getCharacterStream方法获取其中的字符数据。
- 要将Blob置于数据库中,需要在Connection对象上调用createBlob或createClob,然后获取一个用于该对象的输出流或写出器,写出数据,并将该对象存储到数据库中。例如,下面展示了如何存储一张图像:
Blob coverBlob=connection.createBlob();
int offset=0;
OutputStream out=coverBlob.setBinaryStream(offset);
ImageIO.write(coverImage,"PNG",out);
PreparedStatement stat=conn.prepareStatement("INSERT INTO Cover VALUES(?,?)");
stat.set(1,isbn);
stat.set(2, coverBlob);
stat.executeUpdate();
//用以上方法读写Blob不成功。可以参看: http://blog.csdn.net/a600423444/article/details/6524406
SQL转义
- “转义”语法支持各种数据库普遍支持的特性,但是数据库使用的是与数据库相关的语法变体,因此,将转义语法转译为特定数据库的语法是JDBC驱动程序的任务之一。
- 转义主要用于以下特性:日期和时间字面常量;调用标量函数;调用存储过程;外连接;在LIKE子句中的转义字符
- 日期和时间字面常量随数据库的不同而变化很大。要嵌入日期或时间字面常量,需要按ISO 8601格式指定它的值,之后驱动程序会将其转译为本地格式。应该使用d、t、ts表示DATE、TIME和TIMESTAMP值:
{d '2008-01-07'}
{t '23:59:59'}
{ts '2008-01-07 23:59:59.999'}
- 标量函数(scalar function)是指仅返回一个值的函数。在数据库中包含大量的函数,但不同的数据库中这些函数名存在差异.JDBC规范提供了标准的名字,并将其转译为数据库相关的名字。要调用函数,需要像下面这样嵌入标准的函数名和参数:
fn left(?,20)
- 存储过程(stored procedure)是在数据库中执行的用数据库相关的语言编写的过程。要调用存储过程,需要使用call转义命令,其在存储过程没有任何参数时不用加上括号。另外,应该用=捕获存储过程的返回值:
{call PROC1(?,?)}
{call PROC2}
{call ?=PROC3(?)}
- 两个表的外连接(outer join)并不要求每个表的所有行都要根据连接条件进行匹配,如:
SELECT * FROM {oj Books LEFT OUTER JOIN Publishers ON Books.Publisher_id=Publishers.Publisher_id}
这个查询的执行结果将包含有Publisher_id在Publishers表中没有任何匹配的书。由于并非所有的数据库对于这些连接都是用标准的写法,因此需要转义语法。 _
和%
在LIKE子句中具有特殊的含义,用来匹配一个字符或一个字符序列。如果想要匹配所有包含_
字符的字符串,就必须使用下面的结构:... WHERE ? LIKE %!_%{escape '!'}
这里将!
定义为转义字符,而!_
表示字面常量下划线。
多结果集
执行存储过程中,或者在使用允许在单个查询中提交多个SELECT语句的数据库时,一个查询可能返回多个结果集。下面是获取所有结果集的步骤:
- 使用execute方法执行SQL语句
- 获取第一个结果集或更新计数
- 重复调用getMoreResults方法以移动到下一个结果集
- 当不存在更多的结果集或更新计数时,完成操作
如果由多结果集构成的链中的下一项是结果集,execute和getMoreResults方法将返回true,而如果在链中的下一项不是更新计数,getUpdateCount方法将返回-1.下面的循环可以遍历所有结果:
boolean isResult=stat.execute(command);
boolean done=false;
while(!done){
if(isResult){
ResultSet rs=stat.getResultSet();
do something with rs
}else{
int updateCount=stat.getUpdateCount();
if(updateCount>=0) do something with updatedCount
else done=true;
}
if(!done) isResult=stat.getMoreResults();
}
获取自动生成键
- 大多数数据库都支持某种在数据库中对行自动计数的机制,而这些自动计数的值经常用作主键。当我们向数据表中插入一个新行,且其键自动生成时,可以实现下面的代码获取这个键:
stat.executeUpdate(sql,Statement.RETURN_GENERATED_KEYS);
ResultSet rs=stat.getGeneratedKeys();
if(rs.next()){
int key=rs.getInt(1);
...
}
4.6 可滚动和可更新的结果集
- 如果是用于显示一张表或查询结果的可视化数据表示,通常会希望用户可以在结果集上前后滚动。在可滚动结果集中,可以在其中向前或向后移动,甚至可以跳到任意位置。
- 另外,一旦向用户显示了结果集的内容,他们就有可能编辑这些内容。在可更新的结果集中,可以以编程的方式更新其中的项,使得数据库可以自动更新数据。
可滚动的结果集
- 默认情况下,结果集是不可滚动和不可更新的。为了从查询中获得可滚动的结果集,必须使用下面的方法得到一个不同的Statement对象:
Statement stat=conn.getStatement(type,concurrency);
如果要获得预备语句,请调用下面的方法:PreparedStatement stat=conn.prepareStatement(command,type,concurrency);
type值 | 解释 | concurrency值 | 解释 |
---|---|---|---|
TYPE_FORWARD_ONLY | 结果集不能滚动(默认) | CONCUR_READ_ONLY | 结果集不能用于更新数据库(默认) |
TYPE_SCROLL_INSENSITIVE | 结果集可以滚动,但对数据库变化不敏感 | CONCUR_UPDATABLE | 结果集可以用于更新数据库 |
TYPE_SCROLL_SENSITIVE | 结果集可以滚动,且对数据库变化敏感 |
- 例如,如果只想滚动遍历结果集,而不想编辑它的数据,可以使用以下语句:Statement stat=conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE,ResultSet.CONCUR_READ_ONLY);
现在,通过调用以下方法获得的所有结果集都将是滚动的:ResultSet rs=stat.executeQuery(query);
可滚动的结果集有一个游标,用以指示当前位置。
- 注:并非所有的数据库驱动程序都支持可滚动和可更新的结果集。使用DatabaseMetaData接口中的supportsResultSetType和supportsResultSetConcurrency方法可以获知在使用具体的驱动程序时,某个数据库究竟支持哪些结果集类型以及哪些并发模式。
- 在结果集上的滚动可以调用:if(rs.previous()) ...
向后滚动。如果游标位于一个实际的行上,返回true;如果游标位于第一行之前,返回false。可以使用以下调用将游标向前或向后移动多行:rs.relative(n);
如果n为正数,游标将向前移动。如果为负,向后移动。如果n为0,不起任何作用。如果试图将游标移动到当前行集的范围外,返回false,且不移动游标。如果游标位于一个实际的行上,返回true。或者可以把游标设置在指定行号上:rs.absolute(n);
调用以下方法返回当前行行号:int row=rs.getRow();
结果集中第一行的行号为1.如果返回值为0,那么当前游标不在任何行上,要么位于第一行之前,要么位于最后一行之后。first、last、beforeFirst、afterLast这些简便方法用于将游标移到第一行、最后一行、第一行之前或最后一行之后。isFirst等方法用于测试游标是否处于特殊位置上。
可更新的结果集
- 获得可更新的结果集:
Statement stat=conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVEResultSet.CONCUR_UPDATABLE);
这样,调用executeQuery方法返回的结果集就是可更新的结果集。 - 注:并非所有的查询都会返回可更新的结果集。如果查询涉及多个表的连接操作,那么产生的结果集将是不可更新的。如果查询只涉及一个表,或者在查询时是使用主键连接多个表的,产生的是可更新的结果集。可以调用ResultSet接口中的getConcurrency方法确定结果集是否可更新。
- 更新结果集:
rs.updateDouble("Price",price+increase);
rs.updateRow();
所有对应于SQL类型的数据类型都有updateXxx方法(在使用第一个参数为列序号的updateXxx方法时,列序号指的是该列在结果集中的序号)。updateXxx方法更新的只是结果集中的行值,而非数据库中的值。当更新完行中的字段值时,必须调用updateRow方法将当前行中的信息发送给数据库。还可以调用cancelRowUpdates方法取消对当前行的更新。 - 在数据库中添加新行,首先要使用moveToInsertRow方法将游标移到特定位置,称之为插入行。然后调用updateXxx方法在插入行的位置创建一个新行。在上述操作完成之后,调用insertRow方法将新建的行发送给数据库。然后调用moveToCurrentRow方法将游标移回调用moveToInsertRow之前的位置。
rs.moveToInsertRow();
rs.updateString("Title",title);
rs.updateString("ISBN",isbn);
rs.updateDouble("Price",price);
rs.insertRow();
rs.moveToCurrentRow();
- 注意:无法控制在结果集或数据库中插入行的位置。对于在插入行中没有指定值的列,将被设为NULL。但是如果这个列有NOT NULL约束,将会抛出异常,这一行也无法插入。
- 删除游标所在行:
rs.deleteRow();
这个方法会立即将该行从结果集和数据库中删除。 - ResultSet中的updateRow、insertRow和deleteRow的执行效果等同于SQL语句中的update、insert和delete。就大多数程序的修改而言,使用SQL语句更合适一些。
4.7 行集
可滚动的结果集虽然功能强大,但有一个重要的缺陷:在与用户的整个交互过程中,必须始终与数据库保持连接。用户也许会离开电脑旁一段时间,而在此期间却始终占有着数据库连接。在这种情况下,可以使用行集。RowSet接口扩展自ResultSet接口,却无需始终保持和数据库的连接。行集还适用于将查询结果移动到复杂应用层的其他层,或是诸如手机之类的设备中。
构建行集
以下为javax.sql.rowset包的提供的接口,它们都扩展了RowSet接口:
- CachedRowSet允许在断开连接的状态下执行相关操作。
- WebRowSet代表了一个被缓存的行集,该行集可以保存为XML文件。该文件可以移动到Web应用的其他层中。
- FilteredRowSet和JoinRowSet支持对行集的轻量级操作,等同于SQL中的SELECT和JOIN操作。这两个接口的操作对象是存储在行集中的数据,所以运行时无需建立数据库连接。
- JdbcRowSet是ResultSet接口的一个瘦包装器。它从ResultSet接口中继承了有用的get和set方法从而将结果集转换成一个bean。
Java 7 获取行集的标准方式:
RowSetFactory factory=RowSetProvider.newFactory();
CachedRowSet crs=factory.createCachedRowSet();
- 在Java7 以前,创建行集的方式是与供应商相关的。另外,JDK在com.sun.rowset中还提供了参考实现,这些实现类的名字以Impl结尾如CachedRowSetImpl.如果无法使用RowSetProvider,可以使用下面的语句:
CachedRowSet crs=com.sun.rowset.CachedRowSetImpl();
被缓存的行集
- 一个被缓存的行集包含了一个结果集中的所有数据。可以像使用结果集一样使用被缓存的行集。断开数据库连接后仍然可以使用被缓存的行集。这种做法大大简化了交互式应用的实现。在执行每个用户命令时,只需打开数据库连接、执行查询操作、将查询结果放入被缓存的行集,然后关闭数据库连接即可。
- 可以修改缓存的行集中的数据,当然,这些修改不会立刻反馈到数据库中。相反,必须发起一个显式的请求,以便让数据库真正接受所有修改。此时CachedRowSet会重新连接到数据库,并通过执行SQL语句向数据库写入所有修改后的数据。
- 使用结果集填充CachedRowSet对象:
ResultSet rs=...;
CachedRowSet crs=com.sun.rowset.CachedRowSetImpl();
crs.populate(rs);
conn.close();
- 或者,也可以让CachedRowSet对象自动创建一个数据库连接。这个方法调用会建立数据库连接、执行查询操作、填充行集,最后断开连接。
crs.setURL("jdbc://derby://localhost:1527/COREJAVA");
crs.setUsername("dbuser");
crs.setPassword("secret");
crs.setCommand("SELECT * FROM books WHERE publisher=?");
crs.setString(1,publisherName);
crs.execute();
- 如果查询结果非常大,可以指定每一页的尺寸,如只能获取20行。
crs.setCommand("....");
crs.setPageSize(20);
crs....
crs.execute();
- 要获取下一批数据,可以调用:
crs.nextPage();
- 可以使用与结果集中相同的方法来查看和修改行集中的数据。如果修改了行集的内容,必须调用以下方法将修改写回数据库:
crs.acceptChanges(conn);
或crs.acceptChanges();
第二个调用只有当行集中设置了连接数据库所需的信息时才有效。 - 之前提到过并非所有的结果集都是可更新的。同样,如果一个行集包含复杂的查询结果,就无法把对行集数据的修改写回到数据库中。不过如果行集上的数据都来自同一张数据表,就可以安全地写回数据。(如果是使用结果集填充行集,那么行集无从得知需要更新的数据库表名,此时必须调用setTable方法设置表名)
- 另一个问题是,如果在填充了行集后,数据库的数据发生了改变,容易引发数据不一致。为了解决这个问题,参考实现会首先检查行集中的数据(修改前的值)与数据库的当前值是否一致。如果一致,修改后的值会覆盖数据库的当前值。如果不一致,将抛出SyncProviderException异常,且不向数据库写入任何值。
4.8 元数据
- 在SQL中,描述数据库或其组成部分的数据称为元数据(区别于那些存在数据库中的实际数据)。可以获得三类元数据:数据库的元数据、结果集的元数据以及预备语句的元数据。
- 如果要了解数据库的更多信息,可以从数据库连接中获得一个DatabaseMetaData对象:
DatabaseMetaData meta=conn.getMetaData();
现在就可以获取元数据了,如:ResultSet mrs=meta.getTables(null,null,null,new String[]{"TABLE"});
将返回一个包含所有数据库表信息的结果集。结果集中的每一行都包含了一张表的详细信息。其中,第三列是表名。下面的循环可以获取所有表名:while(mrs.next) tableName.addItem(mrs.getString(3));
- 数据库元数据还有第二个重要的应用。DatabaseMetaData接口中有上百个方法用于查询数据库的相关信息,包括一些使用奇特的名字进行调用的方法,如
meta.supportsCatalogsInPrivilegeDefinitions()
和meta.nullPlusNonNullIsNull()
这些方法是针对有特殊要求的高级用户的,尤其是那些需要编写多个数据库且具有高可移植性的代码的编程人员。 - ResultSetMetaData用于提供结果集的信息。每当通过查询得到一个结果集时,都可以获取该结果集的列数以及每一列的名称、类型和字段宽度。
ResultSet mrs=stat.executeQuery("SELECT * FROM "+tableName);
ResultSetMetaData meta=mrs.getMetaData();
for(int i=1;i<=meta.getColumnCount();i++){
String columnName=meta.getColumnLabel(i);
int columnWidth=meta.getColunmnDisplaySize(i);
}
4.9 事务
- 可以将一组语句构建成一个事务(transaction)。当所有语句都顺利执行后,事务可以被提交(submit)。否则,如果其中某个语句遇到错误,那么事务将被回滚,就好像没有语句执行过一样。
- 默认情况下,数据库连接处于自动提交模式(autocommit mode)。每个SQL语句一旦被执行就被提交给数据库。一旦命令被提交,就无法对它进行回滚操作。在使用事务时,要关闭这个默认值:
conn.setAutoCommit(false);
- 可以使用通常的方法创建一个语句对象,然后任意多次地执行executeUpdate方法。如果执行了所有命令之后没有出错,则调用commit方法:
conn.commit();
如果出现错误,则调用:conn.rollback();
此时,程序将自动撤销自上次提交以来的所有语句。
保存点
- 在使用某些驱动程序时,使用保存点(save point)可以更细粒度地控制回滚操作。创建一个保存点意味着稍后只需返回到这个点,而非事务开头。
stat.executeUpdate(command1);
Savepoint svpt=conn.setSavepoint();
stat.executeUpdate(command2);
if(...) conn.rollback(svpt);
....
conn.commit();
- 当不再需要保存点时,必须释放它:
conn.releaseSavepoint(svpt);
批量更新
- 如果程序需要执行多个INSERT操作,可以使用批量更新的方法提高程序性能。在使用批量更新(batch update)时,一个语句序列作为一批操作将同时被收集和提交。
- 可以使用DatabaseMetaData接口中的supportsBatchUpdates方法可以获知数据库是否支持这种特性。
- 处于同一批的语句可以是INSERT、UPDATE和DELETE操作,也可以是数据库定义的CREATE TABLE和DROP TABLE。但是在批量处理中添加SELECT语句会抛出异常。
- 执行批量处理:
Statement stat=conn.createStatement();
String command="...";
stat.addBatch(command);
while(...){
command="....";
stat.addBatch(command);
}
int[] counts=stat.executeBatch();
- 为了在批量模式下正确处理错误,应该将批量执行的操作视为单个事务。如果批量更新在执行过程中失败,必须把它回滚到批量操作开始之前的状态。首先,关闭自动提交模式,然后收集批量操作,执行并提交该草操作,最后恢复最初的自动提交模式。
boolean autoCommit=conn.getAutoCommit();
conn.setAutoCommit(false);
Statement stat=conn.getStatement();
....
stat.executeBatch();
conn.commit();
conn.setAutoCommit(autoCommit);
高级SQL类型
- SQL ARRAY指的是值的序列。例如,student表通常会有一个scores列,这个列就应该是ARRAY OF INTEGER。getArray方法返回一个接口类型为java.sql.Array的对象,该接口中有许多方法可以用于获取数组的值。
- 从数据库中获得一个LOB或数组并不等于获取了它的实际内容,只有在访问具体的值时它们才会从数据库中被读取出来。
- 某些数据库支持描述行位置的ROWID值,这样就可以非常快捷地获取某一行值。JDBC4引入了java.sql.RowId接口,并提供了用于在查询中提供行ID,以及从结果中获取值的方法。
4.10 Web与企业应用中的连接管理
- 使用database.properties文件可以对数据库连接进行简单的设置。这种方法适用于小型的测试程序,但是不适合大规模的应用。
- 在Web或企业环境中部署JDBC应用时,数据库连接管理与Java名字和目录接口(JNDI)是集成在一起的。遍布企业的数据源的属性可以存储在一个目录中,采用这种方式可以集中管理用户名、密码、数据库名和JDBC URL。
- 创建数据库连接:
Context jndiContext=new InitialContext();
DataSource source=(DataSource) jndiContext.lookup("java:comp/env/jdbc/corejava");
Connection conn=source.getConnection();
- 不再使用DriverManager,而是使用JNDI服务来定位数据源。数据源就是一个能够提供简单的JDBC连接和更多高级服务的接口,比如执行涉及多个数据库的分布式事务。
- 当然,必须在某个地方配置数据源。如果编写的数据库程序将在Servlet容器中运行,如Apache Tomcat,或在应用服务器中运行,那么必须将数据库配置信息(包括JNDI名字、JDBC URL、用户名和密码)放置在配置文件中,或在管理员GUI中进行设置。
- 一个重要问题是建立数据库连接所需的开销。一方面,数据库连接是有限的资源,用户如果要离开应用一段时间,那么他占用的连接就不应该保持开放状态,另一方面,每次查询都获取连接并在随后关闭它的代价也是相当高的。解决方法是建立数据库连接池。这意味着数据库连接在物理上并未被关闭,而是保留在一个队列中被反复使用。连接池是一种非常重要的服务。但JDK本身并未实现这个服务,数据库供应商提供的JDBC驱动通常也不包含这项服务。相反,Web容器和应用服务器的开发商通常会提供连接池服务的实现。