使用连接
有时您要从两张或多张表中检索数据。例如,假设Coffee Break的老板想查询从Acme公司购买的咖啡,这就涉及COFFEES表和尚未创建的SUPPLIERS表。这就是需要使用连接的一种情形。连接是一种数据库操作,它用两张或多张表所共享的信息将表彼此联系起来。在示例数据库中,COFFEES表和SUPPLIERS表都有SUP_ID列,该列可用于连接这两张表。
在继续学习之前,我们需要创建SUPPLIERS表,并在其中输入一些数据。
String createSUPPLIERS = "create table SUPPLIERS " + "(SUP_ID INTEGER, SUP_NAME VARCHAR(40), " + "STREET VARCHAR(40), CITY VARCHAR(20), " + "STATE CHAR(2), ZIP CHAR(5))"; stmt.executeUpdate(createSUPPLIERS);stmt.executeUpdate("insert into SUPPLIERS values (101, " + "'Acme, Inc.', '99 Market Street', 'Groundsville', " + "'CA', '95199'"); stmt.executeUpdate("Insert into SUPPLIERS values (49," + "'Superior Coffee', '1 Party Place', 'Mendocino', 'CA', " + "'95460'"); stmt.executeUpdate("Insert into SUPPLIERS values (150, " + "'The High Ground', '100 Coffee Lane', 'Meadows', 'CA', " + "'93966'");下面的代码检出了整张表,让您看到SUPPLIERS表中的内容:
ResultSet rs = stmt.executeQuery("select * from SUPPLIERS");
SUP_ID SUP_NAME
------ 101 49 150 现在有了COFFEES表和SUPPLIERS表,可以继续前面的查询,即查询老板从特定供应商那里购买的咖啡。供应商名称在SUPPLIERS表中,咖啡名称在COFFEES表中。因为两张表中都有SUP_ID列,所以这一列可用于连接两张表。但您要有区分所引用的SUP_ID列的方法。这是通过列名加上表名作为前缀来实现的(如“COFFEES.SUP_ID”表示所引用的是COFFEES表中的SUP_ID列 )。下面的代码(其中stmt是一个Statement对象)将检出从Acme公司购买的咖啡:
String query = " SELECT COFFEES.COF_NAME " + "FROM COFFEES, SUPPLIERS " + "WHERE SUPPLIERS.SUP_NAME LIKE 'Acme, Inc.' " + "and SUPPLIERS.SUP_ID = COFFEES.SUP_ID"; ResultSet rs = stmt.executeQuery(query); System.out.println("Coffees bought from Acme, Inc.: "); while (rs.next()) { String coffeeName = rs.getString("COF_NAME"); System.out.println(" " + coffeeName); }Coffees bought from Acme, Inc.: Colombian Colombian_Decaf
使用事务
有时您想让一条语句只有在另一条语句也成功执行时才产生效果。例如,Coffee Break老板在更新每周销售量时也想更新迄今销售量。但他不期望一列更新而另外一列没有更新;否则数据将不一致。确保两列全部更新或全部不更新的方法就是使用事务。一个事务是作为一个单元执行的一组语句(一条或多条语句),因此它们要么全部执行,要么全部不执行。
当连接创建时,它处在自动提交模式。这表明每条SQL语句作为一个事务,语句执行后马上提交(更确切地说,默认情况下SQL语句是在执行完成时提交,而不是在执行时提交。当一条语句的所有结果集和更新数目已检出时,这条语句就执行完毕了。但在多数场合,一条语句执行完就结束了,因此也就提交了)。
要让两条或更多条语句组成一个事务就要禁用自动提交模式。下面的代码例示了这一操作(其中con为活动连接):
con.setAutoCommit(false);一旦禁用了自动提交模式,就没有SQL语句会提交了,除非您显式调用commit方法。在前一次调用commit方法后执行的所有语句将包含在当前事务中,这些语句将作为一个单元进行提交。下面的代码(其中con是一个活动连接)例示了一个事务:
con.setAutoCommit(false); PreparedStatement updateSales = con.prepareStatement( "UPDATE COFFEES SET SALES = ? WHERE COF_NAME LIKE ?"); updateSales.setInt(1, 50); updateSales.setString(2, "Colombian"); updateSales.executeUpdate(); PreparedStatement updateTotal = con.prepareStatement( "UPDATE COFFEES SET TOTAL = TOTAL + ? WHERE COF_NAME LIKE ?"); updateTotal.setInt(1, 50); updateTotal.setString(2, "Colombian"); updateTotal.executeUpdate(); con.commit(); con.setAutoCommit(true);在本例中,连接con的自动提交模式被禁用了,这表明当调用commit方法时,updateSales和updateTotal这两个预备语句将一起提交。一旦调用了commit方法(启用自动提交模式时是自动调用该方法,禁用自动提交模式时要显式调用该方法),事务中的语句产生的变更将变为永久的。在本例中,这表明Colombian咖啡的SALES和TOTAL列已变为50(如果先前的TOTAL是0),它们将维持这个值直到另一条更新语句改变它们。TransactionPairs.java例示了类似的事务,但使用for循环为updateSales和updateTotal的setXXX方法提供数值。
前面例子的最后一行启用了自动提交模式,这表明每条语句执行完毕将再次自动提交。您将返回到默认状态,在这种状态下不必亲自调用commit方法。禁用自动提交模式是可取的,除非您想处在事务模式。在这种模式下可避免持有多条语句的数据库锁,但增加了与其他用户发生冲突的可能性。
除了将语句组成一个单元一起执行外,事务还有助于保持表中数据的完整性。例如,假设一个员工计划在COFFEES表中输入新的咖啡价格,但由于其他事情耽搁了几天。期间价格上涨了,今天老板正在输入更高的价格。就在老板要更新那张表时,那位员工回来输入已过时的价格。在插入过时价格后,那位员工意识到这些价格不再有效了,就调用Connection方法rollback来取消操作(rollback方法取消一个事务,将数据还原到试图变更之前)。此时老板正在执行SELECT语句并输出新的价格。在这种情形下,老板就可能输出了不正确的价格,这些价格就被还原到以前的值了。
这种情形可通过使用事务来避免。如果DBMS支持事务(几乎所有DBMS都支持),它将提供一些级别的冲突保护,两个用户同时访问数据会引起冲突。
为避免事务期间的冲突,DBMS将使用锁——阻止其他人访问事务正在访问的数据的一种机制(在自动提交模式下,每条语句就是一个事务,锁只由一条语句持有)。一旦加了锁,它将维持有效,直到事务提交或回滚。例如,DBMS可能锁住表中一行,直到提交对它的更新。这种锁可防止用户脏读,即在数据变为永久前读取(访问没有提交的更新数据就是脏读,因为数据可能回滚到原来的值。如果读取了随后回滚的数据,那就是读取了无效的数据)。
加锁机制由事务隔离级别(Isolatoin Level)决定,事务隔离级别可从完全不支持事务一直上升到支持强加了非常严格的访问规则的事务。
JDBC事务隔离级别的一个例子是TRANSACTION_READ_COMMITTED,它不允许访问没有提交的数据。换言之,如果事务隔离级别设为TRANSACTION_READ_COMMITTED,DBMS将不允许脏读出现。Connection接口包括了5个值,代表JDBC中可用的事务隔离级别。
一般不必设置事务隔离级别,可使用DBMS默认的事务隔离级别。JDBC允许查出DMBS中设置的事务隔离级别(使用Connection方法getTransactionIsolation),也允许设置另外的事务隔离级别(使用Connection方法setTransactionIsolation)。但是一定要记住,虽然JDBC允许设置事务隔离级别,但是除非得到所使用的驱动程序和DBMS的支持,否则设置也没有用。
如前所述,调用rollback方法可取消一个事务,将修改的任何数据返还到以前的值。如果在执行一个事务中的一条或多条语句时得到了SQLException,就应该调用rollback方法取消事务,从头开始整个事务。这是确信什么已提交和什么未提交的惟一方法。捕获了SQLException表明出错了,但它并不能说明什么提交了或什么未提交。因为您不能相信什么都没提交的事实,所以调用rollback方法是确保什么都没提交的惟一方法。
Transaction.java例示了一个事务,并包含一个调用rollback方法的catch块。在这里确实没有必要调用rollback,但程序调用了rollback,这主要为了说明rollback的使用。如果程序继续执行,并使用了事务的结果,就有必要在catch块中调用rollback,以避免可能使用不正确的数据。