Spring Boot JdbcTemplate 入門
在實際項目中,在對數據庫訪問層對數據庫進行操作時,大部分時候我們都用的mybatis,但是偶爾會有用到使用jdbc的時候,一般使用jdbc的話要么就自己封裝一個jdbc連接池進行使用,要么就是使用Apache Common 提供的 DbUtils 工具類,還有個就是Spring JDBC ,提供的JdbcTemplate 工具類
因為我的工作中基本上都是使用的spring boot,所以就學習了一下JdbcTemplate 工具類的使用
JdbcTemplate是Spring對JDBC的封裝,目的是使JDBC更加易於使用。JdbcTemplate是Spring的一部分。JdbcTemplate處理了資源的建立和釋放。他幫助我們避免一些常見的錯誤,比如忘了總要關閉連接。他運行核心的JDBC工作流,如Statement的建立和執行,而我們只需要提供SQL語句和提取結果。
在JdbcTemplate中執行SQL語句的方法大致分為3類:
execute
:可以執行所有SQL語句,一般用於執行DDL語句。update
:用於執行INSERT
、UPDATE
、DELETE
等DML語句。queryXxx
:用於DQL數據查詢語句。call
:用於執行存儲過程、函數相關語句。
應用場景以及技術選型
首先先說一下JdbcTemplate 的應用場景,其他的應用場景我說不准,現在java項目中大多數的數據訪問層用的都是mybatis,但是在項目中總會遇到那種數據的批量插入,或者批量刪除等操作,在這樣的場景下,總要考慮到一個性能的問題,所以在做之前總要先行做好技術選型,所以這里就拿Mybatis和傳統的Jdbc做了一下比較。
這里我先用JdbcTemplate 和mybatis分別做一下批量數據插入的測試:
首先環境准備,測試是在相同變量的情況下進行的,所以我就使用同一個mysql5.7的數據庫進行測試。
先建表,建表可看DDL操作那里,這里先根據表結構,創建一個實體類:
TestRecord.java
public class TestRecord {
private String aa;
private String bb;
private String cc;
private String dd;
private String ee;
private String ff;
private String gg;
private String hh;
private Integer ii;
private Integer jj;
//getter and setter
}
JdbcTemplate
代碼寫法后面講解,
案例代碼:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = TestApplication.class)
public class JdbcDMLTest {
@Autowired
private JdbcTemplate jdbcTemplate;
String sql="insert into test_record(a,b, c, d, e, f, g, h, i,j) " +
" values(?,?,?,?,?,?,?,?,?,?) ";
//預編譯的批量插入
@Test
public void batchUpdate(){
//獲取參數
List<TestRecord> recordList=new ArrayList<>();
for(int i=0;i<1000;i++){
TestRecord testRecord=new TestRecord();
testRecord.setAa("aa"+i);
testRecord.setBb("bb"+i);
testRecord.setCc("cc"+i);
testRecord.setDd("dd"+i);
testRecord.setEe("ee"+i);
testRecord.setFf("ff"+i);
testRecord.setGg("gg"+i);
testRecord.setHh("hh"+i);
testRecord.setIi(i);
testRecord.setJj(i);
recordList.add(testRecord);
}
Long start=System.currentTimeMillis();
int[] count = jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i)
throws SQLException {
//注入參數值
ps.setString(1, recordList.get(i).getAa());
ps.setString(2 ,recordList.get(i).getBb());
ps.setString(3, recordList.get(i).getCc());
ps.setString(4, recordList.get(i).getDd());
ps.setString(5, recordList.get(i).getEe());
ps.setString(6, recordList.get(i).getFf());
ps.setString(7, recordList.get(i).getGg());
ps.setString(8, recordList.get(i).getHh());
ps.setInt(9,recordList.get(i).getIi() );
ps.setInt(10, recordList.get(i).getJj());
}
@Override
public int getBatchSize() {
//批量執行的數量
return recordList.size();
}
});
System.out.println("插入數量:"+count.length+",用時:"+(System.currentTimeMillis()-start));
}
}
在執行代碼之前需要注意一個點,JDBC做批量插入的時候需要在mysql的驅動配置信息里加一個屬性配置,這個是開啟批量執行的關鍵
rewriteBatchedStatements=true //加在配置文件數據庫url參數中
執行程序:
可以看到1000條數據是549毫秒,也就是0.5秒完成插入。下來看一下mybatis的處理效率
Mybatis
案例代碼:
MybatisBatchDML.java
package com.zkk.test.javaTest;
import com.zkk.test.TestApplication;
import com.zkk.test.domain.TestRecord;
import com.zkk.test.mapper.BatchInertTestMapper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* description
*
* @author Zaker 2020/06/04 17:21
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = TestApplication.class)
public class MybatisBatchDML {
@Autowired
private BatchInertTestMapper batchInertTestMapper;
@Test
public void mybatisBatchInsert(){
List<TestRecord> recordList=new ArrayList<>();
for(int i=0;i<1000;i++){
TestRecord testRecord=new TestRecord();
testRecord.setAa("aa"+i);
testRecord.setBb("bb"+i);
testRecord.setCc("cc"+i);
testRecord.setDd("dd"+i);
testRecord.setEe("ee"+i);
testRecord.setFf("ff"+i);
testRecord.setGg("gg"+i);
testRecord.setHh("hh"+i);
testRecord.setIi(i);
testRecord.setJj(i);
recordList.add(testRecord);
}
Map<String,Object> param =new HashMap();
param.put("recordList", recordList);
Long start=System.currentTimeMillis();
int count=batchInertTestMapper.batchInsert(param);
System.out.println("插入數量:"+count+",用時:"+(System.currentTimeMillis()-start));
}
}
BatchInertTestMapper.java
package com.zkk.test.mapper;
import com.zkk.test.domain.TestRecord;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Map;
/**
* description
*
* @author Zaker 2020/06/04 17:32
*/
@Repository
@Mapper
public interface BatchInertTestMapper {
Integer batchInsert(Map<String,Object> param);
}
BatchInertTestMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.zkk.test.mapper.BatchInertTestMapper">
<insert id="batchInsert" parameterType="java.util.Map">
insert into test_record
(a,b,c,d,e,f,g,h,i,j)
values
<foreach collection="recordList" item="record" open="" close=""
separator=",">
(
#{record.aa},
#{record.bb},
#{record.cc},
#{record.dd},
#{record.ee},
#{record.ff},
#{record.gg},
#{record.hh},
#{record.ii},
#{record.jj}
)
</foreach>
;
</insert>
</mapper>
以上就是一套很常見的mybatis使用foreach處理的批量數據插入的寫法
注:這里面mybatis使用的都是拼接字符串的形式然后再執行,所必須注意mybatis中拼接的字符串的長度的限制,如果數據量過大,需要分批進行執行。
執行結果:
可以看到使用mybatis插入1000條數據使用了1764毫秒,就是1.7秒,和JDBC的差距已經展現出來了。
mybatis也使用的批量執行,那如果就單純的for循環逐條執行插入呢?這里我就不嘗試了,隨便想想就能想到只會更慢,更別說更大批量的數據了。
對比結果
下來兩種方式我做了一個對比測試,這種測試不一定每次結果都是一樣的,所以就做5次測試,選擇中間的一個值做對比(懶得算平均值)
記錄數 | JDBC耗時/ms | Mybatis耗時/ms |
---|---|---|
100 | 432 | 727 |
1000 | 538 | 1340 |
10000 | 1036 | 3816 |
100000 | 5368 | 14937 |
可以看到這里的執行效果差,明顯JDBC在處理這種大批量的數據的時候表現是更加優秀的,所以在有這種處理大批量數據的場景的時候,可以考慮選擇JDBC來進行實現。
詳細介紹
JdbcTemplate類對可變部分采用回調接口方式實現,如ConnectionCallback通過回調接口返回給用戶一個連接,從而可以使用該連接做任何事情、StatementCallback通過回調接口返回給用戶一個Statement,從而可以使用該Statement做任何事情等等,還有其他一些回調接口
DDL操作
主要使用execute
:用於執行DDL語句,這里嘗試一下使用JdbcTemplate 做DDL操作,新建一張表
案例代碼:
package com.zkk.test.javaTest;
import com.zkk.test.TestApplication;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.junit4.SpringRunner;
/**
* description
*
* @author Zaker 2020/06/04 14:26
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = TestApplication.class)
public class JdbcDDL {
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
public void caret() {
// 創建表的SQL語句
String sql = "CREATE TABLE test_record("
+ "a VARCHAR(20),"
+ "b VARCHAR(20),"
+ "c VARCHAR(20),"
+ "d VARCHAR(20),"
+ "e VARCHAR(20),"
+ "f VARCHAR(20),"
+ "g VARCHAR(20),"
+ "h VARCHAR(20),"
+ "i int,"
+ "j int"
+ ");";
jdbcTemplate.execute(sql);
}
}
執行測試用例,執行完成沒有報錯,查看數據庫可以看到創建成功
這樣一個簡單的建表操作就完成了。
具體的其他的一些刪除以及更改等操作就不做練習了,大概會用就行。
DML操作
單條數據操作
一般都是使用update方法,主要看一下update有哪些方法
-
int update(String sql)
用來執行固定的sql,沒有參數注入,直接執行,因為沒有預編譯,所以應該是效率會慢。
-
int update(String sql, Object... args)
執行參數注入的sql,Object... args可變參數組里面放入的是要注入的參數值
-
int update(PreparedStatementCreator psc)
PreparedStatementCreator ,通過回調獲取JdbcTemplate提供的Connection,由用戶使用該Conncetion創建相關的preparedStatement預編譯對象,使用原始jdbc方式給預編譯sql注入參數
-
int update(String sql, PreparedStatementSetter pss)
可以進行參數注入,PreparedStatementSetter 方法就是用來獲取預編譯語句對象,並設置相應參數值。
-
int update(String sql, Object... args,int[] argTypes)
執行參數注入的sql,Object... args可變參數組里面放入的是要注入的參數值,argTypes里面是需要注入的sql參數的JDBC類型(java.sql.Types中來獲取類型的常量)
-
int update(PreparedStatementCreator psc,KeyHolder generatedKeyHolder)
如果需要返回插入的主鍵,只能用此方法,增加KeyHolder參數
案例1:int update(String sql)
@Test
public void insertOne1(){
String sql="insert into test_record(a,b, c, d, e, f, g, h, i,j) " +
"values(a,b,c,d,e,f,g,h,1,2)";
//返回的是更新的行數
int count = jdbcTemplate.update(sql);
System.out.println("影響的行數:"+count);
}
案例2:int update(String sql, Object... args)
@Test
public void insertOne3(){
String sql="insert into test_record(a,b, c, d, e, f, g, h, i,j) " +
" values(?,?,?,?,?,?,?,?,?,?) ";
//返回的是更新的行數
int count = jdbcTemplate.update(sql,new Object[]{"a","b","c","d","e","f","g","h",1,1});
System.out.println("影響的行數:"+count);
}
案例3
增加、更新、刪除(一條sql語句)(sql需要參數注入)
int update(String sql, PreparedStatementSetter pss)
String sql="insert into test_record(a,b, c, d, e, f, g, h, i,j) " +
" values(?,?,?,?,?,?,?,?,?,?) ";
@Test
public void insertOne(){
//返回的是更新的行數
int count = jdbcTemplate.update(sql, new PreparedStatementSetter(){
@Override
public void setValues(PreparedStatement pstmt)
throws SQLException {
pstmt.setObject(1, "aa");
pstmt.setObject(2 ,"bb");
pstmt.setObject(3, "cc");
pstmt.setObject(4, "dd");
pstmt.setObject(5, "ee");
pstmt.setObject(6, "ff");
pstmt.setObject(7, "gg");
pstmt.setObject(8, "hh");
pstmt.setObject(9, 1);
pstmt.setObject(10, 1);
}
});
System.out.println("影響的行數:"+count);
}
執行結果:成功
查看數據庫:成功
批量數據操作
案例1:批量插入(需要預編譯的)
批量操作的話使用的是batchUpdate這個方法,這個方法有很多不同的實現,可以看一下
第一個方法int[] batchUpdate(String sql, BatchPreparedStatementSetter pss)就是前面應用場景案例那里的方法,可以看一下大概的用法。
值得注意的是BatchPreparedStatementSetter 這個方法類似於PreparedStatementSetter,但用於批處理,需要指定批處理大小。
下面再練習一下
案例2: 批量插入(需要預編譯的)
int[] batchUpdate(String sql,List<Object[]> batchArgs)
//預編譯的批量插入2
@Test
public void batchUpdate2(){
List<Object[]> batchArgs=new ArrayList<Object[]>();
//獲取參數
List<TestRecord> recordList=new ArrayList<>();
for(int i=0;i<10000;i++){
TestRecord testRecord=new TestRecord();
testRecord.setAa("aa"+i);
testRecord.setBb("bb"+i);
testRecord.setCc("cc"+i);
testRecord.setDd("dd"+i);
testRecord.setEe("ee"+i);
testRecord.setFf("ff"+i);
testRecord.setGg("gg"+i);
testRecord.setHh("hh"+i);
testRecord.setIi(i);
testRecord.setJj(i);
recordList.add(testRecord);
}
for(TestRecord record:recordList){
batchArgs.add(new Object[]{record.getAa(),
record.getBb(),
record.getCc(),
record.getDd(),
record.getEe(),
record.getFf(),
record.getGg(),
record.getHh(),
record.getIi(),
record.getJj()});
}
Long start=System.currentTimeMillis();
int[] count = jdbcTemplate.batchUpdate(sql,batchArgs);
System.out.println("插入數量:"+count.length+",用時:"+(System.currentTimeMillis()-start));
}
這里就是把預編譯用的參數以數組的形式放入,比上一種方式操作起來更好理解。
查看執行結果:
一萬條據也只用了1429毫秒,也就是1.4秒。
這里只做兩個常用方法的練習。
DQL操作
查詢語句的話可以做一個了解,就不做demo練習了
public int queryForInt(String sql) 執行查詢語句,返回一個int類型的值。
public long queryForLong(String sql) 執行查詢語句,返回一個Long類型的數據。
public
public Map<String, Object> queryForMap(String sql) 執行查詢語句,將一條記錄放到一個Map中。
public List<Map<String, Object>> queryForList(String sql) 執行查詢語句,返回一個List集合,List中存放的是Map類型的數據。
public
public