在一般情況下,在新增領域對象后,都需要獲取對應的主鍵值。使用應用層來維護主鍵,在一定程度上有利于程序性能的優化和應用移植性的提高。在采用數據庫自增主鍵的方案里,如果JDBC驅動不能綁定新增記錄對應的主鍵,就需要手工執行查詢語句以獲取對應的主鍵值,對于高并發的系統,這很容易返回錯誤的主鍵。通過帶緩存的DataFieldMaxValueIncrementer,可以一次獲取批量的主鍵值,供多次插入領域對象時使用,它的執行性能是很高的。
使用數據庫的自增主鍵
我們經常使用數據的自增字段作為表主鍵,也即主鍵值不在應用層產生,而是在新增記錄時,由數據庫產生。這樣,應用層在保存對象前并不知道對象主鍵值,而必須在保存數據后才能從數據庫中返回主鍵值。在很多情況下,我們需要獲取新對象持久化后的主鍵值。在Hibernate等ORM框架,新對象持久化后,Hibernate會自動將主鍵值綁定到對象上,給程序的開發帶來了很多方便。
在JDBC 3.0規范中,當新增記錄時,允許將數據庫自動產生的主鍵值綁定到Statement或PreparedStatement中。使用Statement時,可以通過以下方法綁定主鍵值:
int executeUpdate(String sql,int autoGeneratedKeys)
也可以通過Connection創建綁定自增值的PreparedStatement:
PreparedStatement prepareStatement(String sql, int autoGeneratedKeys)
當autoGeneratedKeys參數設置為Statement.RETURN_GENERATED_KEYS值時即可綁定數據庫產生的主鍵值,設置為Statement.NO_GENERATED_KEYS時,不綁定主鍵值。下面的代碼演示了Statement綁定并獲取數據庫產生的主鍵值的過程:
Statement stmt = conn.createStatement(); String sql = "INSERT INTO t_topic(topic_title,user_id) VALUES(‘測試主題’,’123’) "; stmt.executeUpdate(sql,Statement.RETURN_GENERATED_KEYS); ①指定綁定表自增主鍵值ResultSet rs = stmt.getGeneratedKeys(); if( rs.next() ) { intkey = rs.getInt();②獲取對應的表自增主鍵值}
Spring利用這一技術,提供了一個可以返回新增記錄對應主鍵值的方法:
int update(PreparedStatementCreator psc, KeyHolder generatedKeyHolder)
org.springframework.jdbc.support.KeyHolder是一個回調接口,Spring使用它保存新增記錄對應的主鍵,該接口的接口方法描述如下:
?Number getKey() throws InvalidDataAccessApiUsageException
當僅插入一行數據,主鍵不是復合鍵且是數字類型時,通過該方法可以直接返回新的主鍵值。如果是復合主鍵,或者有多個主鍵返回時,該方法拋出InvalidDataAccessApiUsageException。該方法是最常用的方法,因為一般情況下,我們一次僅插入一條數據并且主鍵字段類型為數字類型;
?Map getKeys() throws InvalidDataAccessApiUsageException
如果是復合主鍵,則列名和列值構成Map中的一個Entry。如果返回的是多個主鍵,則該方法拋出InvalidDataAccessApiUsageException異常;
?List getKeyList():
如果返回多個主鍵,即PreparedStatement新增了多條記錄,則每一個主鍵對應一個Map,多個Map構成一個List。
Spring為KeyHolder接口指代了一個通用的實現類GeneratedKeyHolder,該類返回新增記錄時的自增長主鍵值。假設我們希望在新增論壇板塊對象后,希望將主鍵值加載到對象中,則可以按以下代碼進行調整:
public voidaddForum(final Forum forum) { final String sql = “INSERT INTO t_forum(forum_name,forum_desc) VALUES(?,?)”; KeyHolder keyHolder = newGeneratedKeyHolder();①創建一個主鍵執有者getJdbcTemplate().update(newPreparedStatementCreator(){ public PreparedStatement createPreparedStatement(Connection conn) throws SQLException { PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, forum.getForumName()); ps.setString(2, forum.getForumDesc()); returnps; } },keyHolder); forum.setForumId(keyHolder.getKey().intValue());②從主鍵執有者中獲取主鍵}
這樣,在調用addForum(final Forum forum)新增forum領域對象后,forum將擁有對應的主鍵值,方便后繼的使用。
在JDBC 3.0之前的版本中,PreparedStatement不能綁定主鍵,如果采用表自增鍵(如MySql的auto increment或SqlServer的identity)將給獲取正確的主鍵值帶來挑戰——因為你必須在插入數據后,馬上執行另一條獲取新增主鍵的查詢語句。表1給出了不同數據庫獲取最新自增主鍵值的查詢語句:
表1 不同數據庫獲取新增加的主鍵值
數據庫
獲取新增主鍵的查詢語句
DB2
IDENTITY_VAL_LOCAL()
Informix
SELECT dbinfo(‘sqlca.sqlerrd1’) FROM
Sybase
SELECT @@IDENTITY
SqlServer
SELECT SCOPE_IDENTITY()或SELECT @@IDENTITY
MySql
SELECT LAST_INSERT_ID()
HsqlDB
CALL IDENTITY()
Cloudscape
IDENTITY_VAL_LOCAL()
Derby
IDENTITY_VAL_LOCAL()
PostgreSQL
SELECT nextval(’
如果數據庫的并發率很高,比如在插入記錄后執行查詢主鍵之前,數據庫又執行了若干條插入記錄的SQL語句,這時,通過表1 返回的主鍵值就是最后一條插入語句的主鍵值,而非我們希望的主鍵值了。所以使用查詢語句獲取表自增鍵值是不安全的,這也是為什么有些數據庫(如Oracle、Firebird)故意不提供自增鍵,而只提供序列的原因,序列強制要求你在新增記錄前,先獲取主鍵值。Oracle通過SELECT .nextval FROM DUAL獲取序列的下一個值,而FireBird通過SELECT GEN_ID( 1) FROM RDB$DATABASE獲取序列的下一個值。在10.4.1小節中,我們還將講解應用層自增鍵的相關知識。