- 概述
- 自增鍵的使用
- Oracle以序列方式産生主鍵值
- MySQL以表方式産生主鍵值
- 如何規劃主鍵方案
- 自增鍵小結
- 以行集傳回資料
- 示例
- 示例源碼
概述
Spring JDBC提供了對自增鍵及行集的支援,自增鍵對象讓使用者可以不依賴資料庫的自增鍵,在應用層為新紀錄提供主鍵。
在Java1.4中引入RowSet,它允許在連接配接斷開的情況下操作資料。 這裡我們讨論如何在Spring JDBC中使用RowSet。
自增鍵的使用
一般資料庫都提供了自增鍵的功能,比如MySql的auto_increment , SQL Server的identifty字段等.
Spring允許使用者在應用層産生主鍵值,為此定義了
org.springframework.jdbc.supprot.incrementer.DataFieldMaxValueIncrementer
接口 , 提供了兩種産生主鍵的方案,第一是通過序列産生主鍵,第二是通過表産生主鍵。 根據主鍵産生方式及資料庫類型的不同,Spring提供了不同的實作類。
DataFieldMaxValueIncrementer繼承類圖
根據不同的主鍵産生方式,可能需要配置表名、主鍵字段或者序列等資訊。
DataFieldMaxValueIncrementer接口中定義了3個擷取主鍵值的方法
- int nextIntValue():擷取下一個主鍵值,主鍵值類型為int
- long nextLongValue();擷取下一個主鍵值,主鍵值類型為long
- String nextStringValue();擷取下一個主鍵值,主鍵值類型為String
在其抽象類AbstractDataFieldMaxValueIncrementer中,提供了幾個重要屬性:
incrementerName:定義序列名後模拟序清單的名稱,如果傳回的主鍵值類型是String類型,則paddingLength屬性就會派上用場,它允許使用者指定傳回主鍵的長度,不足的部分前面補0.
AbstractSequenceMaxAbstractSequence使用标準的資料庫序列産生主鍵值,
而AbstractColumnMaxValueIncrementer使用一張模拟序列的表産生主鍵值,AbstractColumnMaxValueIncrementer可以通過cacheSize屬性指定緩存的主鍵個數,當記憶體中主鍵值用完後,遞增器将一次性擷取cacheSize個主鍵,這樣可以減少資料庫通路的次數,提高應用的性能。
下面分别以Oracle和MySQL為例子,分别闡述下使用序列以及字段産生主鍵值的方式。
Oracle以序列方式産生主鍵值
在Oracle資料庫中建立artisan表以及artisan_id的序列
-- Create table
create table ARTISAN
(
artisan_id NUMBER,
artisan_name VARCHAR2(50)
)
tablespace TAB_CC
pctfree 10
initrans 1
maxtrans 255
storage
(
initial 16
next 8
minextents 1
maxextents unlimited
);
-- Create sequence
create sequence artisan_id_seq
minvalue 1
maxvalue 999
start with 1
increment by 1
cache 20;
複制
接着我們調整下,Spring配置檔案,使用OracleSequenceMaxValueIncrementer作為主鍵的産生器
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.xgj.dao.dataFieldMaxValueIncrementer"/>
<context:property-placeholder location="classpath:spring/jdbc.properties" />
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close"
p:driverClassName="${jdbc.driverClassName}"
p:url="${jdbc.url}"
p:username="${jdbc.username}"
p:password="${jdbc.password}" />
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"
p:dataSource-ref="dataSource" />
<bean id="oracleIncre" class="org.springframework.jdbc.support.incrementer.OracleSequenceMaxValueIncrementer"
p:dataSource-ref="dataSource"
p:incrementerName="artisan_id_seq"/>
beans>
複制
業務類
package com.xgj.dao.dataFieldMaxValueIncrementer.oracle.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.incrementer.OracleSequenceMaxValueIncrementer;
import org.springframework.stereotype.Repository;
import com.xgj.dao.dataFieldMaxValueIncrementer.oracle.domain.Artisan;
/**
*
*
* @ClassName: AritsanOracleDaoImpl
*
* @Description: @Repository标注DAO層,并被Spring管理
*
* @author: Mr.Yang
*
* @date: 2017年9月29日 下午8:39:32
*/
@Repository
public class AritsanOracleDaoImpl implements AritsanOracleDao {
private JdbcTemplate jdbcTemplate;
private OracleSequenceMaxValueIncrementer oracleIncre;
private static final String addArtisanSql = "insert into artisan(artisan_id ,artisan_name) values(?,?)";
/**
*
*
* @Title: setOracleIncre
*
* @Description: 自動注入OracleSequenceMaxValueIncrementer
*
* @param oracleIncre
*
* @return: void
*/
@Autowired
public void setOracleIncre(OracleSequenceMaxValueIncrementer oracleIncre) {
this.oracleIncre = oracleIncre;
}
/**
*
*
* @Title: setJdbcTemplate
*
* @Description: 自動注入JdbcTemplate
*
* @param jdbcTemplate
*
* @return: void
*/
@Autowired
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
/**
* 使用 oracleIncre.nextIntValue() 作為主鍵自增長
*/
@Override
public void addArtisan(Artisan artisan) {
jdbcTemplate.update(addArtisanSql, oracleIncre.nextIntValue(),
artisan.getArtisanName());
System.out.println("add Artisan successfully");
}
}
複制
單元測試
package com.xgj.dao.dataFieldMaxValueIncrementer.oracle.dao;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.xgj.dao.dataFieldMaxValueIncrementer.oracle.domain.Artisan;
public class OracleSeqIncreaseTest {
ClassPathXmlApplicationContext ctx = null;
AritsanOracleDaoImpl aritsanOracleDaoImpl = null;
@Before
public void initContext() {
// 啟動Spring 容器
ctx = new ClassPathXmlApplicationContext(
"classpath:com/xgj/dao/dataFieldMaxValueIncrementer/conf_oracleincreaseId.xml");
aritsanOracleDaoImpl = ctx.getBean("aritsanOracleDaoImpl",
AritsanOracleDaoImpl.class);
System.out.println("initContext successfully");
}
@Test
public void queryTeacherById() {
for (int i = 0; i < 5; i++) {
Artisan artisan = new Artisan();
artisan.setArtisanName("Xiao" + i);
aritsanOracleDaoImpl.addArtisan(artisan);
}
}
@After
public void closeContext() {
if (ctx != null) {
ctx.close();
}
System.out.println("close context successfully");
}
}
複制
日志
2017-09-29 20:41:28,720 INFO [main] (AbstractApplicationContext.java:583) - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@4a05bc0c: startup date [Fri Sep 29 20:41:28 BOT 2017]; root of context hierarchy
2017-09-29 20:41:28,823 INFO [main] (XmlBeanDefinitionReader.java:317) - Loading XML bean definitions from class path resource [com/xgj/dao/dataFieldMaxValueIncrementer/conf_oracleincreaseId.xml]
initContext successfully
add Artisan successfully
add Artisan successfully
add Artisan successfully
add Artisan successfully
add Artisan successfully
2017-09-29 20:41:30,221 INFO [main] (AbstractApplicationContext.java:984) - Closing org.springframework.context.support.ClassPathXmlApplicationContext@4a05bc0c: startup date [Fri Sep 29 20:41:28 BOT 2017]; root of context hierarchy
close context successfully
複制
測試結果
觀察ID,是按照定義的序列生成的ID
MySQL以表方式産生主鍵值
在MySQL資料庫中建立一張用于維護artisan主鍵的artisan_id表
create table artisan_id(sequence_id int) type = MYISAM;
insert into artisan_id values(0);
複制
由于主鍵維護表的并發通路量很大,最好将其聲明為MYISAM類型。 此外,需要為該表提供初始值,以便後續主鍵值在此基礎上增長。
Spring配置檔案微調
<bean id="mysqlIncrease" class="org.springframework.jdbc.support.incrementer.MySQLMaxValueIncrementer"
p:dataSource-ref="dataSource"
p:incrementerName="artisan_id"
p:columnName="sequence_id"
p:cacheSize="10"/>
複制
p:incrementerName –維護主鍵的表名
p:columnName 用于生成主鍵值的列名
p:cacheSize 緩存大小
cacheSize 決定一次傳回的主鍵個數,這裡設定為10 ,當第一次通過MySQLMaxValueIncrementer#nextIntValue()方法擷取主鍵時,MySQLMaxValueIncrementer将使artisan_id.sequence_id 遞增到10 ,而後9次調用nextIntValue方法時,都從緩存中擷取主鍵值,直到第10次調用nextIntValue()方法時,才會再此将artisan_id.sequence_id遞增10 ,如此循環反複.
如何規劃主鍵方案
從主鍵建立者的角度看,我們可以将主鍵建立方案分為兩類:
- 其一為“應用層主鍵方案”,新資料的主鍵配置設定由應用層負責,如采用UUID或者使用DataFieldMaxValueIncrementer生成主鍵都屬于這一類型;
-
其二為“資料庫層主鍵方案”,新資料的主鍵配置設定由資料庫負責,即在表結構定義時,将主鍵設定為auto
increment或通過表的觸發器配置設定主鍵。
1、資料庫層主鍵方案不足:
其一,它給應用開發帶來不便,因為你必須通過一個查詢擷取新增資料的主鍵值;
其二,不友善主鍵值的全局管理和控制,使系統散失靈活性;
其三,不友善資料的整合和遷移。
2、采用應用層主鍵方案,使用UUID産生主鍵值,這樣可以保證ID的全局唯一性,為後期資料整合帶來了便利。
當然,采用UUID也有不好地方,就是UUID是一個36位的字元串,會占用大量的存儲空間。
是以另一個候選的方案就是采用分段長整型編碼方案,将主鍵編碼分為N段:這樣就可以建立一個全局的唯一的整數型的主鍵值。
這裡不能使用DataFieldMaxValueIncrementer,因為DataFieldMaxValueIncrementer隻能為一個表建立主鍵,但道理是相同,我們可以建立一個包含N個字段的主鍵表,編寫一個類似DataFieldMaxValueIncrementer的接口以擷取主鍵值。
自增鍵小結
在高并發的系統中,如果采用基于序清單的方式建立主鍵值,則應該考慮兩個層面的并發問題:
第一:應用層擷取主鍵的并發問題,Spring的DataFielMaxValueIncrementer實作類已經對擷取主鍵值的代碼進行了同步,確定同一JVM内應用不會産生應發問題
第二:全局的并發問題,如果應用是叢集部署的,所有叢集節點通過同一個序清單擷取主鍵,那麼就必須對這張序清單進行樂觀鎖定(序清單必須添加一個版本或者時間戳字段),以防止叢集節點的并發問題。 很可惜的是Spring的DataFielMaxValueIncrementer并滅有對序清單進行樂觀鎖定。我們隻有自己實作DataFielMaxValueIncrementer接口,以解決全局并發的問題。
另外 DataFielMaxValueIncrementer接口隻能為一張表提供元件,即每張表都必須配置一個單獨的DataFielMaxValueIncrementer,是以比較死闆,建議參照DataFielMaxValueIncrementer接口,自行編寫一個可為多張表提供主鍵的接口。
以行集傳回資料
行集對象可以綁定一個資料連接配接并在整個生命周期中維持該連接配接,在此情況下,該行集對象被稱為“連接配接的行集”。
行集對象還可以先綁定一個資料源,擷取資料後就關閉它,這種行集被稱為“非連接配接行集”。 非連接配接行集可以在斷開連接配接時更改資料,然後重新綁定資料連接配接,并将對資料的更改同步到資料庫中。
JdbcTemplate 為擷取基于行集的結果集,提供如下查詢方法
- SqlRowSet queryForRowSet(String sql)
- SqlRowSet queryForRowSet(String sql,Object … args)
- SqlRowSet queryForRowSet(String sql,Object[] args ,int[] argTypes)
示例
package com.xgj.dao.rowset.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.rowset.SqlRowSet;
import org.springframework.stereotype.Repository;
/**
*
*
* @ClassName: AritsanOracleDaoImpl
*
* @Description: @Repository标注DAO層,并被Spring管理
*
* @author: Mr.Yang
*
* @date: 2017年9月29日 下午10:41:10
*/
@Repository
public class AritsanOracleDaoImpl implements AritsanOracleDao {
private JdbcTemplate jdbcTemplate;
private static final String selectArtisanByIdSql = "select artisan_name from artisan where artisan_id = ?";
/**
*
*
* @Title: setJdbcTemplate
*
* @Description: 自動注入JdbcTemplate
*
* @param jdbcTemplate
*
* @return: void
*/
@Autowired
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public SqlRowSet selectArtisanById(int artisanId) {
return jdbcTemplate.queryForRowSet(selectArtisanByIdSql, artisanId);
}
}
複制
單元測試
package com.xgj.dao.rowset.dao;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jdbc.support.rowset.SqlRowSet;
public class RowSetTest {
ClassPathXmlApplicationContext ctx = null;
AritsanOracleDaoImpl aritsanOracleDaoImpl = null;
@Before
public void initContext() {
// 啟動Spring 容器
ctx = new ClassPathXmlApplicationContext(
"classpath:com/xgj/dao/rowset/conf_rowset.xml");
aritsanOracleDaoImpl = ctx.getBean("aritsanOracleDaoImpl",
AritsanOracleDaoImpl.class);
System.out.println("initContext successfully");
}
@Test
public void queryTeacherById() {
SqlRowSet sqlRowSet = aritsanOracleDaoImpl.selectArtisanById(1);
// 這時,資料連接配接已經斷開
while (sqlRowSet.next()) {
System.out.println("artisan_name:"
+ sqlRowSet.getString("artisan_name"));
}
}
@After
public void closeContext() {
if (ctx != null) {
ctx.close();
}
System.out.println("close context successfully");
}
}
複制
測試結果:
2017-09-29 22:27:14,381 INFO [main] (AbstractApplicationContext.java:583) - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@70221d9a: startup date [Fri Sep 29 22:27:14 BOT 2017]; root of context hierarchy
2017-09-29 22:27:14,500 INFO [main] (XmlBeanDefinitionReader.java:317) - Loading XML bean definitions from class path resource [com/xgj/dao/rowset/conf_rowset.xml]
initContext successfully
artisan_name:Xiao0
2017-09-29 22:27:16,161 INFO [main] (AbstractApplicationContext.java:984) - Closing org.springframework.context.support.ClassPathXmlApplicationContext@70221d9a: startup date [Fri Sep 29 22:27:14 BOT 2017]; root of context hierarchy
close context successfully
複制
在selectArtisanById查詢并傳回SqlRowSet的結果集後,資料連接配接已經斷開,但是結果集的資料已經儲存在SqlRowSet中。 是以,我們仍然可以通路到SqlRowSet的結果集資料。
值的注意的是,RowSet會一次性裝載所有的比對資料,而不像ResultSet一樣,分批次傳回一批資料(一批的行數為fetchSize).
是以對于大結果集的資料,使用SQLRowSet會造成很大的記憶體消耗,不過JdbcTemplate的maxSize屬性依然會現在SqlRowSet的傳回記錄數。
示例源碼
代碼已托管到Github—> https://github.com/yangshangwei/SpringMaster