返回顶部

JdbcTemplate的用法

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类:

  1. execute:可以执行所有SQL语句,一般用于执行DDL语句。
  2. update:用于执行INSERTUPDATEDELETE等DML语句。
  3. queryXxx:用于DQL数据查询语句。
  4. 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 T queryForObject(String sql, Class requiredType) 执行查询语句,返回一个指定类型的数据。

public Map<String, Object> queryForMap(String sql) 执行查询语句,将一条记录放到一个Map中。

public List<Map<String, Object>> queryForList(String sql) 执行查询语句,返回一个List集合,List中存放的是Map类型的数据。

public List query(String sql, RowMapper rowMapper) 执行查询语句,返回一个List集合,List中存放的是RowMapper指定类型的数据。

public List query(String sql, RowMapper rowMapper) 执行查询语句,返回一个List集合,List中存放的是RowMapper指定类型的数据。

posted @ 2020-06-23 14:57  张十三S  阅读(2354)  评论(0编辑  收藏  举报