當(dāng)autoGeneratedKeys參數(shù)設(shè)置為Statement.RETURN_GENERATED_KEYS值時(shí)即可綁定數(shù)據(jù)庫(kù)產(chǎn)生的主鍵值,設(shè)置為Statement.NO_GENERATED_KEYS時(shí),不綁定主鍵值。下面的代碼演示了Statement綁定并獲取數(shù)據(jù)庫(kù)產(chǎn)生的主鍵值的過(guò)程:
Statement stmt = conn.createStatement(); String sql = "INSERT INTO t_topic(topic_title,user_id) VALUES(‘測(cè)試主題','123') "; stmt.executeUpdate(sql,Statement.RETURN_GENERATED_KEYS); ①指定綁定表自增主鍵值 ResultSet rs = stmt.getGeneratedKeys(); if ( rs.next() ) { int key = rs.getInt();②獲取對(duì)應(yīng)的表自增主鍵值 }
Spring利用這一技術(shù),提供了一個(gè)可以返回新增記錄對(duì)應(yīng)主鍵值的方法:
int update(PreparedStatementCreator psc, KeyHolder generatedKeyHolder)
org.springframework.jdbc.support.KeyHolder是一個(gè)回調(diào)接口,Spring使用它保存新增記錄對(duì)應(yīng)的主鍵,該接口的接口方法描述如下:
Number getKey() throws InvalidDataAccessApiUsageException
當(dāng) 僅插入一行數(shù)據(jù),主鍵不是復(fù)合鍵且是數(shù)字類型時(shí),通過(guò)該方法可以直接返回新的主鍵值。如果是復(fù)合主鍵,或者有多個(gè)主鍵返回時(shí),該方法拋出 InvalidDataAccessApiUsageException。該方法是最常用的方法,因?yàn)橐话闱闆r下,我們一次僅插入一條數(shù)據(jù)并且主鍵字段類型為數(shù)字類型;
Map getKeys() throws InvalidDataAccessApiUsageException
如果是復(fù)合主鍵,則列名和列值構(gòu)成Map中的一個(gè)Entry。如果返回的是多個(gè)主鍵,則該方法拋出InvalidDataAccessApiUsageException異常;
List getKeyList():
如果返回多個(gè)主鍵,即PreparedStatement新增了多條記錄,則每一個(gè)主鍵對(duì)應(yīng)一個(gè)Map,多個(gè)Map構(gòu)成一個(gè)List。
Spring為KeyHolder接口指代了一個(gè)通用的實(shí)現(xiàn)類GeneratedKeyHolder,該類返回新增記錄時(shí)的自增長(zhǎng)主鍵值。假設(shè)我們希望在新增論壇板塊對(duì)象后,希望將主鍵值加載到對(duì)象中,則可以按以下代碼進(jìn)行調(diào)整:
public void addForum(final Forum forum) { final String sql = "INSERT INTO t_forum(forum_name,forum_desc) VALUES(?,?)"; KeyHolder keyHolder = new GeneratedKeyHolder();①創(chuàng)建一個(gè)主鍵執(zhí)有者 getJdbcTemplate().update(new PreparedStatementCreator(){ public PreparedStatement createPreparedStatement(Connection conn) throws SQLException { PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, forum.getForumName()); ps.setString(2, forum.getForumDesc()); return ps; } },keyHolder); forum.setForumId(keyHolder.getKey().intValue());②從主鍵執(zhí)有者中獲取主鍵 }
這樣,在調(diào)用addForum(final Forum forum)新增forum領(lǐng)域?qū)ο蠛?,forum將擁有對(duì)應(yīng)的主鍵值,方便后繼的使用。
在JDBC 3.0之前的版本中,PreparedStatement不能綁定主鍵,如果采用表自增鍵(如MySql的auto increment或SqlServer的identity)將給獲取正確的主鍵值帶來(lái)挑戰(zhàn)——因?yàn)槟惚仨氃诓迦霐?shù)據(jù)后,馬上執(zhí)行另一條獲取新增主鍵的查詢語(yǔ)句。表 1給出了不同數(shù)據(jù)庫(kù)獲取最新自增主鍵值的查詢語(yǔ)句:
表 1 不同數(shù)據(jù)庫(kù)獲取新增加的主鍵值
數(shù)據(jù)庫(kù) |
獲取新增主鍵的查詢語(yǔ)句 |
DB2 |
IDENTITY_VAL_LOCAL() |
Informix |
SELECT dbinfo('sqlca.sqlerrd1') FROM TABLE> |
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('TABLE>_SEQ') |
如果數(shù)據(jù)庫(kù)的并發(fā)率很高,比如在插入記錄后執(zhí)行查詢主鍵之前,數(shù)據(jù)庫(kù)又執(zhí)行了若干條插入記錄的SQL語(yǔ)句,這時(shí),通過(guò)表 1 返回的主鍵值就是最后一條插入語(yǔ)句的主鍵值,而非我們希望的主鍵值了。所以使用查詢語(yǔ)句獲取表自增鍵值是不安全的,這也是為什么有些數(shù)據(jù)庫(kù)(如 Oracle、Firebird)故意不提供自增鍵,而只提供序列的原因,序列強(qiáng)制要求你在新增記錄前,先獲取主鍵值。Oracle通過(guò)SELECT SEQUENCE_NAME>.nextval FROM DUAL獲取序列的下一個(gè)值,而FireBird通過(guò)SELECT GEN_ID(SEQUENCE_NAME> 1) FROM RDB$DATABASE獲取序列的下一個(gè)值。在10.4.1小節(jié)中,我們還將講解應(yīng)用層自增鍵的相關(guān)知識(shí)。
應(yīng)用層產(chǎn)生主鍵
Spring JDBC提供了自增鍵以及行集的支持,自增鍵對(duì)象讓我們可以不依賴數(shù)據(jù)庫(kù)的自增鍵,在應(yīng)用層為新記錄提供主鍵值。在JDK 1.4中引入了RowSet,它允許在連接斷開(kāi)的情況下操作數(shù)據(jù),在這節(jié)里,我們將介紹如何在Spring JDBC中使用RowSet。
自增鍵的使用
一般數(shù)據(jù)庫(kù)都提供了自增鍵的功能,如MySql的auto_increment、SqlServerr的identity字段等。Spring允許你在應(yīng)用層產(chǎn)生主鍵值,為此定義了 org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer 接口,提供兩種產(chǎn)生主鍵的方案:第一,通過(guò)序列產(chǎn)生主鍵;第二,通過(guò)表產(chǎn)生主鍵。根據(jù)主鍵產(chǎn)生方式和數(shù)據(jù)庫(kù)的不同,Spring提供了若干實(shí)現(xiàn)類,如圖 1所示:
圖 1 DateFieldValueIncrementer繼承類圖
根據(jù)不同的主鍵產(chǎn)生方式,可能需要配置表名、主鍵字段名或序列名等信息。下面,我們以O(shè)racle和MySql為例分別講解使用序列及表字段產(chǎn)生主鍵值的方式。
DataFieldMaxValueIncrementer接口定義了3個(gè)獲取下一個(gè)主鍵值的方法:
int nextIntValue():獲取下一個(gè)主鍵值,主鍵數(shù)據(jù)類型為int;
long nextLongValue():獲取下一個(gè)主鍵值,主鍵數(shù)據(jù)類型為long;
String nextStringValue():獲取下一個(gè)主鍵值,主鍵數(shù)據(jù)類型為String;
在其抽象實(shí)現(xiàn)類AbstractDataFieldMaxValueIncrementer中,提供了幾個(gè)重要的屬性,其中 incrementerName定義序列或主鍵表的名稱;如果返回的主鍵是String類型,則paddingLength屬性可能會(huì)派上用場(chǎng),它允許你指定返回主鍵的長(zhǎng)度,不足的部分前面補(bǔ)0。
HsqlMaxValueIncrementer和MySQLMaxValueIncrementer兩個(gè)主鍵值產(chǎn)生器基于表進(jìn)行工作。通過(guò) columnName屬性定義主鍵列的名字,通過(guò)cacheSize屬性定義緩存的主鍵個(gè)數(shù),當(dāng)內(nèi)存中的主鍵值用完后,產(chǎn)生器將一次性獲取 cacheSize個(gè)主鍵,這樣可以減少數(shù)據(jù)訪問(wèn)的次數(shù),提高應(yīng)用的性能。
我們通過(guò)DateFieldValueIncrementer從數(shù)據(jù)庫(kù)中獲取主鍵值來(lái)彌補(bǔ)這個(gè)缺陷。首先,調(diào)整PostJdbcDao的代碼,添加DateFieldValueIncrementer屬性,并通過(guò)它從序列中得到下一個(gè)主鍵值:
代碼清單 13 使用DateFieldValueIncrementer產(chǎn)生主鍵
public class PostJdbcDao extends JdbcDaoSupport implements PostDao { private DataFieldMaxValueIncrementer incre; ①主鍵值產(chǎn)生器 public void addPost(final Post post) { … getJdbcTemplate().execute( sql,new AbstractLobCreatingPreparedStatementCallback( this.lobHandler) { protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException { ps.setInt(1, incre.nextIntValue());②獲取下一個(gè)主鍵值 … } }); } …//省略get/setter方法 }
在②處,我們通過(guò)incre.nextIntValue()獲取下一個(gè)主鍵值。
以序列方式產(chǎn)生主鍵值
在Oracle數(shù)據(jù)庫(kù)中創(chuàng)建一個(gè)seq_post_id序列,使用這個(gè)序列為t_post提供主鍵值,以下是創(chuàng)建seq_post_id的腳本:
create sequence seq_post_id increment by 1start with 1;
接著,調(diào)整Spring的配置,使用OracleSequenceMaxValueIncrementer作為主鍵產(chǎn)生器:
bean id="incre" class="org.springframework.jdbc.support.incrementer.OracleSequenceMaxValueIncrementer">property name="incrementerName" value="seq_post_id"/> ①指定序列名 property name="dataSource" ref="dataSource"/> ②設(shè)置數(shù)據(jù)源 /bean>bean id="postDao" parent="dao" class="com.baobaotao.dao.jdbc.PostJdbcDao">property name="lobHandler" ref="oracleLobHandler"/>property name="incre" ref="incre"/> ③添加主鍵主鍵產(chǎn)生器 /bean>
以表方式產(chǎn)生主鍵值
在Mysql中創(chuàng)建一張用于維護(hù)t_post主鍵的t_post_id表,以下是創(chuàng)建該表及插入初始化的SQL腳本:
create table t_post_id(sequence_id int) type = MYISAM;
insert into t_post_id values(0);
由于主鍵維護(hù)表的并發(fā)訪問(wèn)量很大,所以最好將其聲明為MYISAM類型,此外需要為該表提供初始值,以便后續(xù)主鍵值在此之上進(jìn)行遞增。
調(diào)整為MySql數(shù)據(jù)庫(kù)后,我們僅需要對(duì)Spring配置進(jìn)行小小的調(diào)整就可以了:
bean id="incre"class="org.springframework.jdbc.support.incrementer.MySQLMaxValueIncrementer">property name="incrementerName" value="t_post_id"/> ①設(shè)置維護(hù)主鍵的表名 property name="columnName" value="sequence_id"/>②用于生成主鍵值的列名 property name="cacheSize" value="10"/> ③緩存大小 property name="dataSource" ref="dataSource"/>/bean>bean id="postDao" parent="dao" class="com.baobaotao.dao.jdbc.PostJdbcDao">property name="lobHandler" ref="defaultLobHandler"/>property name="incre" ref="incre"/>/bean>
incrementerName和columnName都很容易理解,cacheSize決定一次返回的主鍵個(gè)數(shù),這里我們?cè)O(shè)置為10。當(dāng)?shù)谝淮瓮ㄟ^(guò) MySQLMaxValueIncrementer# nextIntValue()獲取主鍵值時(shí),MySQLMaxValueIncrementer將使t_post_id. sequence_id遞增10,而后續(xù)9次調(diào)用nextIntValue()方法時(shí),都從緩存中獲取主鍵值。直到第10次再次調(diào)用 nextIntValue()方法時(shí),才會(huì)再次將t_post_id. sequence_id字段值遞增10,如此循環(huán)反復(fù)。
小結(jié)
主鍵的生產(chǎn)方式從產(chǎn)生地點(diǎn)上可以分為應(yīng)用層產(chǎn)生和數(shù)據(jù)庫(kù)產(chǎn)生兩種方式。應(yīng)用層借助數(shù)據(jù)庫(kù)的序列或表產(chǎn)生主鍵,這種方式可以保證程序的可移植性和安全性,同時(shí)可以通過(guò)緩存機(jī)制提高運(yùn)行效率。有些數(shù)據(jù)庫(kù)支持?jǐn)?shù)據(jù)表自增鍵的主鍵產(chǎn)生機(jī)制,在JDBC 3.0以前的版本中,無(wú)法通過(guò)Statement自動(dòng)獲取新增記錄的對(duì)應(yīng)主鍵。這時(shí)需要在插入數(shù)據(jù)后,馬上執(zhí)行一條數(shù)據(jù)庫(kù)相關(guān)的主鍵獲取SQL語(yǔ)句以得到對(duì)應(yīng)的主鍵值,在數(shù)據(jù)庫(kù)高并發(fā)的情況下,有可能獲取到不正確的主鍵值。在這種情況下,在插入數(shù)據(jù)前事先在應(yīng)用層準(zhǔn)備好主鍵值是很好的備選方案。
另外補(bǔ)充一點(diǎn)在SqlUpdate執(zhí)行update之前需設(shè)置setReturnGeneratedKeys(true);
您可能感興趣的文章:- Spring MVC 框架搭建配置方法及詳解
- struts2+spring+hibernate分頁(yè)代碼[比較多]
- Spring中的事務(wù)管理實(shí)例詳解
- Spring事務(wù)管理只對(duì)出現(xiàn)運(yùn)行期異常進(jìn)行回滾
- SpringMVC文件上傳 多文件上傳實(shí)例
- 讀取spring配置文件的方法(spring讀取資源文件)
- Spring實(shí)現(xiàn)文件上傳(示例代碼)
- java Spring整合Freemarker的詳細(xì)步驟
- spring快速入門實(shí)例教程
- Spring測(cè)試 其實(shí)很簡(jiǎn)單