javascript
Spring JDBC-自增键和行集RowSet
- 概述
- 自增鍵的使用
- 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_CCpctfree 10initrans 1maxtrans 255storage(initial 16next 8minextents 1maxextents unlimited);-- Create sequence create sequence artisan_id_seq minvalue 1 maxvalue 999 start with 1 increment by 1 cache 20;接著我們調整下,Spring配置文件,使用OracleSequenceMaxValueIncrementer作為主鍵的產生器
<?xml version="1.0" encoding="UTF-8" ?> <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.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"><!-- 掃描類包,將標注Spring注解的類自動轉化Bean,同時完成Bean的注入 --><context:component-scan base-package="com.xgj.dao.dataFieldMaxValueIncrementer"/><!-- 使用context命名空間,加載數據庫的properties文件 --><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}" /><!-- 配置Jdbc模板 --><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*/@Autowiredpublic void setOracleIncre(OracleSequenceMaxValueIncrementer oracleIncre) {this.oracleIncre = oracleIncre;}/*** * * @Title: setJdbcTemplate* * @Description: 自動注入JdbcTemplate* * @param jdbcTemplate* * @return: void*/@Autowiredpublic void setJdbcTemplate(JdbcTemplate jdbcTemplate) {this.jdbcTemplate = jdbcTemplate;}/*** 使用 oracleIncre.nextIntValue() 作為主鍵自增長*/@Overridepublic 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;@Beforepublic 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");}@Testpublic void queryTeacherById() {for (int i = 0; i < 5; i++) {Artisan artisan = new Artisan();artisan.setArtisanName("Xiao" + i);aritsanOracleDaoImpl.addArtisan(artisan);}}@Afterpublic 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*/@Autowiredpublic void setJdbcTemplate(JdbcTemplate jdbcTemplate) {this.jdbcTemplate = jdbcTemplate;}@Overridepublic 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;@Beforepublic 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");}@Testpublic void queryTeacherById() {SqlRowSet sqlRowSet = aritsanOracleDaoImpl.selectArtisanById(1);// 這時,數據連接已經斷開while (sqlRowSet.next()) {System.out.println("artisan_name:"+ sqlRowSet.getString("artisan_name"));}}@Afterpublic 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
總結
以上是生活随笔為你收集整理的Spring JDBC-自增键和行集RowSet的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring JDBC-使用Spring
- 下一篇: Spring JDBC-NamedPar