转自:http://blog.163.com/sir_876/blog/static/1170522320106259562270/

清单 1. 使用 JDBC 的简单数据库插入

view plaincopy to clipboardprint?
@Stateless
public class TradingServiceImpl implements TradingService {  
   @Resource SessionContext ctx;  
   @Resource(mappedName="java:jdbc/tradingDS") DataSource ds;  

   public long insertTrade(TradeData trade) throws Exception {  
      Connection dbConnection = ds.getConnection();  
      try {  
         Statement sql = dbConnection.createStatement();  
         String stmt =  
            "INSERT INTO TRADE (ACCT_ID, SIDE, SYMBOL, SHARES, PRICE, STATE)"
          + "VALUES ("
          + trade.getAcct() + "','"
          + trade.getAction() + "','"
          + trade.getSymbol() + "',"
          + trade.getShares() + ","
          + trade.getPrice() + ",'"
          + trade.getState() + "')";  
         sql.executeUpdate(stmt, Statement.RETURN_GENERATED_KEYS);  
         ResultSet rs = sql.getGeneratedKeys();  
         if (rs.next()) {  
            return rs.getBigDecimal(1).longValue();  
         } else {  
            throw new Exception("Trade Order Insert Failed");  
         }  
      } finally {  
         if (dbConnection != null) dbConnection.close();  
      }  
   }  
}
@Stateless
public class TradingServiceImpl implements TradingService {
   @Resource SessionContext ctx;
   @Resource(mappedName="java:jdbc/tradingDS") DataSource ds;
public long insertTrade(TradeData trade) throws Exception {
      Connection dbConnection = ds.getConnection();
      try {
         Statement sql = dbConnection.createStatement();
         String stmt =
            "INSERT INTO TRADE (ACCT_ID, SIDE, SYMBOL, SHARES, PRICE, STATE)"
          + "VALUES ("
          + trade.getAcct() + "','"
          + trade.getAction() + "','"
          + trade.getSymbol() + "',"
          + trade.getShares() + ","
          + trade.getPrice() + ",'"
          + trade.getState() + "')";
         sql.executeUpdate(stmt, Statement.RETURN_GENERATED_KEYS);
         ResultSet rs = sql.getGeneratedKeys();
         if (rs.next()) {
            return rs.getBigDecimal(1).longValue();
         } else {
            throw new Exception("Trade Order Insert Failed");
         }
      } finally {
         if (dbConnection != null) dbConnection.close();
      }
   }
}

清单 1 中的 JDBC 代码没有包含任何事务逻辑,它只是在数据库中保存 TRADE 表中的交易订单。在本例中,数据库处理事务逻辑。

在 LUW 中,这是一个不错的单个数据库维护操作。但是如果需要在向数据库插入交易订单的同时更新帐户余款呢?如清单 2 所示:


清单 2. 在同一方法中执行多次表更新

view plaincopy to clipboardprint?
public TradeData placeTrade(TradeData trade) throws Exception {  
   try {  
      insertTrade(trade);  
      updateAcct(trade);  
      return trade;  
   } catch (Exception up) {  
      //log the error  
      throw up;  
   }  
}
public TradeData placeTrade(TradeData trade) throws Exception {
   try {
      insertTrade(trade);
      updateAcct(trade);
      return trade;
   } catch (Exception up) {
      //log the error
      throw up;
   }
}

在本例中,insertTrade() 和 updateAcct() 方法使用不带事务的标准 JDBC 代码。insertTrade() 方法结束后,数据库保存(并提交了)交易订单。如果 updateAcct() 方法由于任意原因失败,交易订单仍然会在 placeTrade() 方法结束时保存在 TRADE 表内,这会导致数据库出现不一致的数据。如果 placeTrade() 方法使用了事务,这两个活动都会包含在一个 LUW 中,如果帐户更新失败,交易订单就会回滚。

清单 4. 使用 @Transactional 注释

view plaincopy to clipboardprint?
public class TradingServiceImpl {  
   @PersistenceContext(unitName="trading") EntityManager em;  

   @Transactional
   public long insertTrade(TradeData trade) throws Exception {  
      em.persist(trade);  
      return trade.getTradeId();  
   }  
}
public class TradingServiceImpl {
   @PersistenceContext(unitName="trading") EntityManager em;

   @Transactional
   public long insertTrade(TradeData trade) throws Exception {
      em.persist(trade);
      return trade.getTradeId();
   }
}

现在重新测试代码,您发现上述方法仍然不能工作。问题在于您必须告诉 Spring Framework,您正在对事务管理应用注释。除非您进行充分的单元测试,否则有时候很难发现这个陷阱。这通常只会导致开发人员在 Spring 配置文件中简单地添加事务逻辑,而不会使用注释。

要在 Spring 中使用 @Transactional 注释,必须在 Spring 配置文件中添加以下代码行:

view plaincopy to clipboardprint?
<tx:annotation-driven transaction-manager="transactionManager"/>
<tx:annotation-driven transaction-manager="transactionManager"/>

transaction-manager 属性保存一个对在 Spring 配置文件中定义的事务管理器 bean 的引用。这段代码告诉 Spring 在应用事务拦截器时使用 @Transaction 注释。如果没有它,就会忽略 @Transactional 注释,导致代码不会使用任何事务。

让基本的 @Transactional 注释在 清单 4 的代码中工作仅仅是开始。注意,清单 4 使用 @Transactional 注释时没有指定任何额外的注释参数。我发现许多开发人员在使用 @Transactional 注释时并没有花时间理解它的作用。例如,像我一样在清单 4 中单独使用 @Transactional 注释时,事务传播模式被设置成什么呢?只读标志被设置成什么呢?事务隔离级别的设置是怎样的?更重要的是,事务应何时回滚工作?理解如何使用这个注释对于 确保在应用程序中获得合适的事务支持级别非常重要。回答我刚才提出的问题:在单独使用不带任何参数的 @Transactional 注释时,传播模式要设置为 REQUIRED,只读标志设置为 false,事务隔离级别设置为 READ_COMMITTED,而且事务不会针对受控异常(checked exception)回滚。

@Transactional 只读标志陷阱

我在工作中经常碰到的一个常见陷阱是 Spring @Transactional 注释中的只读标志没有得到恰当使用。这里有一个快速测试方法:在使用标准 JDBC 代码获得 Java 持久性时,如果只读标志设置为 true,传播模式设置为 SUPPORTS,清单 5 中的 @Transactional 注释的作用是什么呢?


清单 5. 将只读标志与 SUPPORTS 传播模式结合使用 — JDBC

view plaincopy to clipboardprint?
@Transactional(readOnly = true, propagation=Propagation.SUPPORTS)  
public long insertTrade(TradeData trade) throws Exception {  
   //JDBC Code...  
}
@Transactional(readOnly = true, propagation=Propagation.SUPPORTS)
public long insertTrade(TradeData trade) throws Exception {
   //JDBC Code...
}

当执行清单 5 中的 insertTrade() 方法时,猜一猜会得到下面哪一种结果:
抛出一个只读连接异常
正确插入交易订单并提交数据
什么也不做,因为传播级别被设置为 SUPPORTS
是哪一个呢?正确答案是 B。交易订单会被正确地插入到数据库中,即使只读标志被设置为 true,且事务传播模式被设置为 SUPPORTS。但这是如何做到的呢?由于传播模式被设置为 SUPPORTS,所以不会启动任何事物,因此该方法有效地利用了一个本地(数据库)事务。只读标志只在事务启动时应用。在本例中,因为没有启动任何事 务,所以只读标志被忽略。