javascript
Spring JDBC-使用Spring JDBC访问数据库
- 概述
- 使用Spring JDBC
- 基本的數據操作
- 更改數據
- 返回數據庫表的自增主鍵值
- 批量更改數據
- 查詢數據
- 使用RowCallbackHandler處理結果集
- 使用RowMapperT處理結果集
- RowCallbackHandler和RowMapperT的比較
- 查詢單值數據
- 調用存儲過程3種方式
- 示例源碼
概述
Spring JDBC是Spring所提供的持久層技術,它的主要目的降低JDBC API的使用難度,以一種更直接、更簡潔的方式使用JDBC API。
Spring JDBC中,僅僅需要做那些和業務相關的DML操作的事兒而將獲取資源、Statement創建、釋放資源以及異常處理等繁雜乏味的工作交給Spring JDBC.
使用Spring JDBC
Spring JDBC通過模板和回調機制大大降低了使用JDBC的復雜度。
一般情況下,都是在DAO類中使用JdbcTemplate,JdbcTemplate在XML配置文件中后,在DAO中直接注入引用JdbcTemplate即可.
我們看一個配置文件的例子
<?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.demo"/><!-- 不使用context命名空間,則需要定義Bean<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="locations" value="classpath:spring/jdbc.properties" /> </bean> --><!-- 使用context命名空間,同上面的Bean等效.在xml文件中配置數據庫的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" /></beans>在Spring配配置那文件中配置DAO一般分為4個步驟
定義DataSource
定義JdbcTemplate
聲明一個抽象的Bean,以便所有的DAO復用配置JdbcTemplate屬性的配置(使用注解的方式更加方便)
配置具體的DAO(使用注解的方式更加方便)
其中JdbCTemplate有幾個屬性可以控制底層JDBC API的屬性。
queryTimeout 查詢數據的最大超時時間,默認為0 ,表示使用底層JDBC驅動程序的默認設置
fetchSize:設置底層的ResultSet每次從數據庫返回的行數,該屬性對程序的性能影響較大,如果設置過大,因為一次性載入的數據都會放到內存中,所以內存消耗會很大,反之設置的過小,從數據庫讀取的次數將增大,也會影響性能。 默認為0 ,表示使用底層JDCB驅動程序的默認設置。 Oracle驅動程序的fetchsize的默認值為10
maxRows:設置底層的ResutlSet從數據庫返回的最大行數,默認為0 ,表示使用底層JDBC驅動程序默認的設置
ignoreWarnings :是否忽略SQL的告警信息。默認為true,即所有的告警信息都記錄到日志中,如果設置為false,則JdbcTemplate將拋出SQLWarningException
基本的數據操作
數據庫的增刪改查(CRUD)及存儲過程調用是最常見的數據庫操作,JdbcTemplate提供了眾多的方法,通過JdbcTemplate可以用簡單的方法完成這些數據操作。
下面我們以示例來實際演示下這些操作
更改數據
JdbcTemplate提供了若干個update方法,允許對數據表記錄記錄進行更改和刪除操作。
首先我們定義一個抽象的DAO基類, BaseDao。 其中暫時我們只封裝了注入JDBC的操作(擴展的話可以將分頁等通用的功能抽取到BaseDao中等)
package com.xgj.dao.basicOperation;import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate;public abstract class BaseDao {public JdbcTemplate jdbcTemplate;// 注入JdbcTemplate實例@Autowiredpublic void setJdbcTemplate(JdbcTemplate jdbcTemplate) {this.jdbcTemplate = jdbcTemplate;} }然后我們編寫DAO層的代碼,簡單旗艦,直接將DAO定義成了類。一般來講將DAO層編寫成接口更合適。
package com.xgj.dao.basicOperation.insertUpdateAndDelete;import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List;import org.springframework.jdbc.core.BatchPreparedStatementSetter; import org.springframework.stereotype.Repository;import com.xgj.dao.demo.BaseDao;/*** * * @ClassName: ArtisanDao* * @Description: @Repository標注的DAO* * @author: Mr.Yang* * @date: 2017年9月18日 下午4:19:06*/@Repository public class ArtisanDao extends BaseDao {private static final String sql = "insert into artisan_user(user_name,password) values(?,?)";/*** * * @Title: addSingleArtisan* * @Description: 增加一個Artisan* * @param artisan* * @return: void*/public void addSingleArtisan(Artisan artisan) {jdbcTemplate.update(sql, artisan.getUserName(), artisan.getPassword());System.out.println("insert successfully");}}由于JdbcTemplate在內部通過PreparedStatement執行SQL語句,所以可以綁定參數的SQL語句,每個“?”占位符可以接受一個參數。
盡量使用可綁定參數的SQL語句,以便數據庫可以復用SQL的執行計劃,提高數據庫的執行效率。 此外,應該在DAO使用類級別的靜態常量(final static)定義SQL字符串,不應該在方法內部聲明SQL字符串變量,以提高JVM的內存使用效率。
在通過public int update(String sql, Object... args) throws DataAccessException 方法為SQL語句的占位符綁定參數時,并沒有顯示的指定對應字段的數據類型,此時,Spring直接讓PreparedStatement根據參數的類型進行“猜測”。 有一種更好的做法是使用public int update(String sql, Object[] args, int[] argTypes) throws DataAccessException顯示指定每個占位符所對英的字段數據類型,這樣就可以保證類型安全,當參數值為null時,這種形式提供了更好的支持。
以下代碼僅為演示
// 使用該類中的常量屬性定義參數類型 import java.sql.Type ....jdbcTemplate,update(sql, new Object[]{..} ,new int[]{Types.VARCHAR2...});....配置文件如下(以下的幾個操作都加載這個配置文件)
<?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.basicOperation" /><!-- 不使用context命名空間,則需要定義Bean <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="locations" value="classpath:spring/jdbc.properties" /> </bean> --><!-- 使用context命名空間,同上面的Bean等效.在xml文件中配置數據庫的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" /></beans>測試類
package com.xgj.dao.basicOperation.insertUpdateAndDelete;import java.util.ArrayList; import java.util.List;import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext;public class ArtisanDaoTest {public static void main(String[] args) {// 啟動Spring 容器ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:com/xgj/dao/basicOperation/basicOperation.xml");Artisan artisan = ctx.getBean("artisan", Artisan.class);artisan.setUserName("Artisan");artisan.setPassword("987654");ArtisanDao artisanDao = ctx.getBean("artisanDao", ArtisanDao.class);// 調用目標方法artisanDao.addSingleArtisan(artisan); }除了上述兩個update方法外,JdbcTemplate還提供了以下幾個功能相似的重載方法
public int update(final String sql) 為不帶占位符的SQL語句提供的便利方法
public int update(String sql, PreparedStatementSetter pss) PreparedStatementSetter 是一個回調接口,它定義了一個setValues(PreparedStatement ps)接口方法 ,如下所示
PreparedStatement綁定參數時,參數索引從1開始,而非0開始。 第一個參數索引為1,第二個參數索引為2,依次類推。
當然了,還有其他方法 ,需要指出的是,在實際用用中,應該優先考慮不帶回調接口的JdbcTemplate方法。沒有必要使用那些帶有回調接口的方法,因為Spring會在內部自動創建這些回調實例。
返回數據庫表的自增主鍵值
舉個例子 ORACLE數據庫
com.xgj.dao.transaction.annotationTrans.dao.impl.StudentDaoImpl.java
@Overridepublic void addStudent(final Student student) {// 這里采用和addTeacher不同的方式,輸出插入數據庫的主鍵IDKeyHolder keyHolder = new GeneratedKeyHolder();PreparedStatementCreator preparedStatementCreator = new PreparedStatementCreator() {@Overridepublic PreparedStatement createPreparedStatement(Connection con)throws SQLException {PreparedStatement ps = con.prepareStatement(addTeacherSQL,new String[] { "id" });ps.setString(1, student.getName());ps.setInt(2, student.getAge());ps.setString(3, student.getSex());return ps;}};jdbcTemplate.update(preparedStatementCreator, keyHolder);System.out.println("獲取到的插入數據庫的ID:" + keyHolder.getKey().longValue());}在實際開發中,我們并不太建議使用表自增鍵,因為這種方式會讓開發變得更加復雜且降低程序的移植性,在應用層中創建主鍵才是主流的方式,可以使用UUID或者通過一個編碼引擎獲取主鍵值。
批量更改數據
如果需要一次性插入或者更新多條記錄,當然可以簡單的通過多次調用update()方法完成任務,但是這不是最好的實現方案。 更好的選擇是使用JDBCTemplate批量數據更改的方法。一般情況下,后者擁有更好的性能,因為更新的數據將被批量發送到數據庫中,它減少了對數據庫訪問的次數。
我們解讀下下面兩個方法:
public int[] batchUpdate(String[] sql)
多條SQL語句組成一個數組,注意此處的sql語句不能帶參數,該方法以批量方式執行這些SQL語句。Spring在內部使用JDBC提供的批量更新API完成操作,如果底層的JDBC Driver不支持批量更新操作,Spring將采用逐條更新的方式模擬批量更新。int[] batchUpdate(String sql,BatchPreparedStatementSetter pss)使用本方法對于同一結構的帶參SQL語句多次進行數據更新操作。通過BatchPreparedStatementSetter回調接口進行批量參數的綁定工作。
BatchPreparedStatementSetter定義了兩個方法:
- int getBatchSize():指定本批次的大小
- void setValues(PreparedStatement ps,int i):為給定的PreparedStatement設置參數
需要注意的是BatchPreparedStatementSetter是一次性地批量提交數據,而不會分批提交,getBatchSize()是整批的大小。所以,如果希望將一個List中的數據通過BatchPreparedStatementSetter批量更新到數據庫中,getBatchSize()就應該設置為List的大小。
如果List非常大,希望分多次批量提交,則可分段讀取這個大List并暫存到一個小的List中,再將這個小的List通過BatchPreparedStatemetSetter批量保存到數據庫中。
查詢數據
在Spring JDBC中,僅需要指定SQL查詢語句并定義好如何從結果集中返回數據就可以了。
使用RowCallbackHandler處理結果集
Spring提供了org.springframework.jdbc.core.RowCallbackHandler回調接口,通過該接口可以定義如何從結果集中獲取數據. RowCaIlbackHandler接口很簡單,僅有一 個方法void processRow(ResultSet rs) throws SQLException。
Spring會遍歷結果集, 對結果集中的每一行調用RowCallbackHandler回調接口處理數據。所以用戶無 須 調用ResultSet的next()方法,而只需要定義好如何獲取結果行數據的邏輯就可以了。
我們來看個示例
package com.xgj.dao.basicOperation.retrieve_select;import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List;import org.springframework.jdbc.core.RowCallbackHandler; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Repository;import com.xgj.dao.demo.BaseDao;/*** * * @ClassName: ArtisanRDao* * @Description: @Repository 標注的DAO* * @author: Mr.Yang* * @date: 2017年9月18日 下午6:14:24*/@Repository public class ArtisanRDao extends BaseDao {private static final String selectArtisanUserSql = "select user_name ,password from artisan_user where user_id = ? ";private static final String selectArtisanUsersSql = "select user_name ,password from artisan_user where user_id between ? and ? ";/*** * * @Title: selectArtisanById* * @Description: 取一條數據* * @param artisanId* @return* * @return: Artisan*/public Artisan selectArtisanById(int artisanId) {final Artisan artisan = new Artisan();// (1)將結果集中的數據抽取到artisan對象中jdbcTemplate.query(selectArtisanUserSql, new Object[] { artisanId },new RowCallbackHandler() {@Overridepublic void processRow(ResultSet rs) throws SQLException {artisan.setUserName(rs.getString("user_name"));artisan.setPassword(rs.getString("password"));}});return artisan;} }如果需要獲取多條記錄,依舊可以使用RowCallbackHandler完成任務,只需要稍微調整一下結果集的處理邏輯就可以了。 代碼如下
/*** * * @Title: selectArtisansByIds* * @Description: 使用RowCallbackHandler獲取多條記錄* * @param beginId* @param toId* @return* * @return: List<Artisan>*/public List<Artisan> selectArtisansByIds(int beginId, int toId) {final List<Artisan> artisanList = new ArrayList<Artisan>();jdbcTemplate.query(selectArtisanUsersSql,new Object[] { beginId, toId }, new RowCallbackHandler() {@Overridepublic void processRow(ResultSet rs) throws SQLException {Artisan artisan = new Artisan();artisan.setUserName(rs.getString("user_name"));artisan.setPassword(rs.getString("password"));artisanList.add(artisan);}});return artisanList;}當結果集中沒有數據時,并不會拋出異常。只是此時RowCallbackHandle:回調接口 中定義的處理邏輯沒有得到調用罷了。
使用RowMapper<T>處理結果集
Spring還提供了一個和RowCallbackHandler功能類似的RowMapper<T>接口,它也可以使用RowMapper定義結果集映射邏輯,在結果集為多行記錄時,該接口更 容易使用。RowMapper<T>也只有一個接口方法:
T mapRow(ResultSet rs, int rowNum)看下示例
/*** * * @Title: selectArtisansByIdsWithRowMapper* * @Description: 使用RowMapper獲取多行結果集* * @param beginId* @param toId* @return* * @return: List<Artisan>*/public List<Artisan> selectArtisansByIdsWithRowMapper(int beginId, int toId) {return jdbcTemplate.query(selectArtisanUsersSql, new Object[] {beginId, toId }, new RowMapper<Artisan>() {@Overridepublic Artisan mapRow(ResultSet rs, int rowNum) throws SQLException {Artisan artisan = new Artisan();artisan.setUserName(rs.getString("user_name"));artisan.setPassword(rs.getString("password"));return artisan;}});}RowCallbackHandler和RowMapper<T>的比較
從功能上講,RowCallbackHandler和RowMapper沒有太大的區別,它們都是用于定義結果集行的讀取邏輯,將ResultSet中的數據映射到對象或者List中 。
RowCallbackHandler接口實現類可以是有狀態的,而RowMapper的實現類應該是無狀態的。如果RowCallbackHandler實現類是有狀態的,用戶就不能在多個地方復用,只有無狀態的實例都能在不同的地方復用。
比如,Spring有一個RowCallbackHandler的實現類是RowCountCallbackHandler,可以計算結果集行數:
RowCountCallbackHandler countCallback = new RowCountCallbackHandler(); jdbcTemplate.query("select * from user", countCallback); int rowCount = countCallback.getRowCount();可見RowCountCallbackHandler包含了一個記錄結果集行數的狀態,在多線程的環境中,如果沒有進行特殊的處理,就不能在多個地方復用countCallback實例。
Spring也提供了幾個RowMapper實現類,如ColumnMapRowMapper和SingleColumnRowMapper。
- ColumnMapRowMapper將結果集中的每一行映射為一個
- MapSingleColumnRowMapper將結果集中的某一列映射為一個Object。它們都只是定義了映射邏輯,而沒有保持狀態。
我們知道,通過JDBC查詢返回一個ResultSet結果集時,JDBC并不會一次性將所有匹配的數據都加載到JVM中,而是只返回同一批次的數據(由JDBC驅動程序決定,如Oracle的JDBC驅動程序默認返回10行數據),當通過ResultSet#next()游標流動結果集超過數據范圍時,JDBC再獲取一批數據。這樣以一種“批量化+串行化”的處理方式避免大結果集處理時JVM內存的過大開銷。
當處理大結果集數據時,如果使用RowMapper,則雖然獲取數據的過程是串行化的,但是結果集中的所有數據最終都會映射并匯總成一個List對象,占用大量的JVM內存,甚至可以直接引發OutOfMemoryException異常。這里應該使用RowCallbackHandler接口,在processRow接口方法內部處理結果集數據。
當使用RowCallbackHandler接口時,如果結果集中沒有數據,并不會拋出異常,只是此時RowCallbackHandler回調接口中定義的處理邏輯沒有得到調用罷了。
查詢單值數據
Both queryForInt() and queryForLong() are deprecated since version 3.2.2 . To fix it, replace the code with queryForObject(String, Class).
package com.xgj.dao.basicOperation.getSingleValue;import org.springframework.stereotype.Repository;import com.xgj.dao.basicOperation.BaseDao;/*** * * @ClassName: GetCountOfArtisanDao* * @Description: @Repository 標注的DAO* * @author: Mr.Yang* * @date: 2017年9月19日 下午12:05:08*/@Repository public class GetCountOfArtisanDao extends BaseDao {private final static String COUNTSQL = "select count(1) from artisan_user where user_name = ? ";/*** * * @Title: getCount* * @Description: Both queryForInt() and queryForLong() are deprecated since* version 3.2.2 (correct me if mistake). To fix it, replace* the code with queryForObject(String, Class).* * https://www.mkyong.com/spring/jdbctemplate-queryforint-is-* deprecated/* * @return* * @return: int*/public boolean getCount(String userName) {boolean isExist = false;int count = jdbcTemplate.queryForObject(COUNTSQL,new Object[] { userName }, Integer.class);if (count > 0) {isExist = true;} else {isExist = false;}return isExist;} }調用存儲過程(3種方式)
CallProcDemo
package com.xgj.dao.basicOperation.callProc;import java.sql.CallableStatement; import java.sql.SQLException; import java.sql.Types;import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.CallableStatementCallback; import org.springframework.stereotype.Repository;import com.xgj.dao.demo.BaseDao;@Repository public class CallProcDemo extends BaseDao {// (1) 調用存過的語句private static final String PROCSQL_STRING = "call PROC_artisan_oper(?,?)";public int getUserCount(final String userName) {int num = jdbcTemplate.execute(PROCSQL_STRING,new CallableStatementCallback<Integer>() {@Overridepublic Integer doInCallableStatement(CallableStatement cs)throws SQLException, DataAccessException {// (2)綁定入參cs.setString(1, userName);// (3)注冊輸出參數cs.registerOutParameter(2, Types.INTEGER);// 執行cs.execute();return cs.getInt(2);}});System.out.println("num:" + num);return num;} }CallProcDemoWithSimpleJdbcCall
package com.xgj.dao.basicOperation.callProc;import java.util.Map;import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.SqlParameterSource; import org.springframework.jdbc.core.simple.SimpleJdbcCall; import org.springframework.stereotype.Repository;@Repository public class CallProcDemoWithSimpleJdbcCall {private JdbcTemplate jdbcTemplate;@Autowiredpublic void setJdbcTemplate(JdbcTemplate jdbcTemplate) {this.jdbcTemplate = jdbcTemplate;}public void getUserCount(int userId) {SimpleJdbcCall jdbcCall = new SimpleJdbcCall(jdbcTemplate).withProcedureName("PROC_artisan_selectInfo_withId");// 如果調用function 則為withFunctionName(functionName)// 注冊入參 必須和存過的入參保持一致 不區分大小寫SqlParameterSource in = new MapSqlParameterSource().addValue("p_user_id", userId);// 獲取返回結果Map<String, Object> outMap = jdbcCall.execute(in);for (Map.Entry<String, Object> entry : outMap.entrySet()) {System.out.println("key=" + entry.getKey() + ",value="+ entry.getValue());}String userName = (String) outMap.get("O_USERNAME");String password = (String) outMap.get("O_PASSWORD");System.out.println("userName:" + userName + " ,password=" + password);} }CallProcDemoWithCallableStatementCreator
package com.xgj.dao.basicOperation.callProc;import java.sql.CallableStatement; import java.sql.SQLException; import java.sql.Types; import java.util.HashMap; import java.util.Map;import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.CallableStatementCallback; import org.springframework.jdbc.core.CallableStatementCreator; import org.springframework.jdbc.core.CallableStatementCreatorFactory; import org.springframework.jdbc.core.SqlOutParameter; import org.springframework.jdbc.core.SqlParameter; import org.springframework.stereotype.Repository;import com.xgj.dao.demo.BaseDao;@Repository public class CallProcDemoWithCallableStatementCreator extends BaseDao {private static final String PROCSQL_STRING = "call PROC_artisan_selectInfo_withId(?,?,?)";@SuppressWarnings({ "rawtypes", "unchecked" })public void printUserInfo(int userId) {// 使用CallableStatementCreatorFactory 創建 CallableStatementCreatorCallableStatementCreatorFactory factory = new CallableStatementCreatorFactory(PROCSQL_STRING);// 設置入參factory.addParameter(new SqlParameter("p_user_id", Types.INTEGER));// 設置出參factory.addParameter(new SqlOutParameter("o_username", Types.VARCHAR));factory.addParameter(new SqlOutParameter("o_password", Types.VARCHAR));Map<String, Integer> paramMap = new HashMap<String, Integer>();paramMap.put("p_user_id", userId);CallableStatementCreator csc = factory.newCallableStatementCreator(paramMap);String userInfo = jdbcTemplate.execute(csc,new CallableStatementCallback() {@Overridepublic Object doInCallableStatement(CallableStatement cs)throws SQLException, DataAccessException {// 執行cs.execute();// 獲取返回結果String userName = cs.getString(2);String password = cs.getString(3);String returnInfo = userName + "|" + password;return returnInfo;}});System.out.println("UserInfo:" + userInfo);} }示例源碼
代碼已托管到Github—> https://github.com/yangshangwei/SpringMaster
總結
以上是生活随笔為你收集整理的Spring JDBC-使用Spring JDBC访问数据库的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring JDBC-数据连接泄露解读
- 下一篇: Spring JDBC-使用Spring