Spring-jdbcTempalate研究

   很多时候,需要使用jdbcTemplate,既有出于性能考虑的因素,也有出于个人偏好。

   关于jdbcTemplate的几个关键性的问题:

一、简介

    JdbcTemplate位于org.springframework包,组件标识为spring-jdbc。

    处于spring家族的核心区域。spring专注于应用开发,应用开发据大部分和数据库有关,数据库的操作主要由jdbc负责。

    用spring.io自己的话说,spring-jdbc就是默默地干了大家不愿意干,但又不得不干的事情。

    具体哪些是我们不愿意干的,看spring自己提供的图:

    x表示需要做的。

   本文不讨论jdbcTemplate是如何做了大家不想做的事情,而是讨论能用jdbcTemplate做什么。

   要研究透JdbcTemplate,其实光JdbcTemplate自身是不够,还需要了解jdbc的其它一些内容,如果要彻底研究,请阅读spring.io有关的内容。

   限于篇幅,本文只讨论jdbcTempalte等几个template。

   关键字列表:

  • DataSource
  • DataSourceUtils
  • Connection
  • RowMapper
  • SqlParameterSource
  • ListMap
  • InitializingBean

 

二、传递SQL参数

   从jdbc底层来说,只有一种传递参数的方式,下面来看参考代码:Lesson: JDBC Basics (The Java™ Tutorials > JDBC Database Access) (oracle.com)

Processing SQL Statements with JDBC (The Java™ Tutorials > JDBC Database Access > JDBC Basics) (oracle.com)
 public void updateCoffeeSales(HashMap<String, Integer> salesForWeek) throws SQLException {
    String updateString =
      "update COFFEES set SALES = ? where COF_NAME = ?";
    String updateStatement =
      "update COFFEES set TOTAL = TOTAL + ? where COF_NAME = ?";

    try (PreparedStatement updateSales = con.prepareStatement(updateString);
         PreparedStatement updateTotal = con.prepareStatement(updateStatement))
    
    {
      con.setAutoCommit(false);
      for (Map.Entry<String, Integer> e : salesForWeek.entrySet()) {
        updateSales.setInt(1, e.getValue().intValue());
        updateSales.setString(2, e.getKey());
        updateSales.executeUpdate();

        updateTotal.setInt(1, e.getValue().intValue());
        updateTotal.setString(2, e.getKey());
        updateTotal.executeUpdate();
        con.commit();
      }
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
      if (con != null) {
        try {
          System.err.print("Transaction is being rolled back");
          con.rollback();
        } catch (SQLException excep) {
          JDBCTutorialUtilities.printSQLException(excep);
        }
      }
    }
  }

 

    在原生jdbc中,使用?表示一个参数,?起到占位的作用。

    Spring jdbcTemplate为了传递参数方便,支持多种表示参数和设置参数的方式。

    表示参数的方式:

    a.占位,使用?表示

    b.命名,使用":参数名“表示

   

    传递参数的几种方式:

    a.不定大小的数组,集合。通常对应占位传参

    b.Map,Bean。通常对应命名参数

    来看看Spring JdbcTemplate的一些源码:

JdbcTemplate
org.springframework.jdbc.core.JdbcTemplate.batchUpdate(String, Collection<T>, int, ParameterizedPreparedStatementSetter<T>)
org.springframework.jdbc.core.JdbcTemplate.batchUpdate(String, List<Object[]>)
org.springframework.jdbc.core.JdbcTemplate.query(String, Object[], int[], ResultSetExtractor<T>)
org.springframework.jdbc.core.JdbcTemplate.update(String, Object...)

NamedParamterJdbcTempalte
org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate.update(String, Map<String, ?>)
org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate.queryForObject(String, SqlParameterSource, Class<T>)

 

    大部分传参都容易理解。命名参数传递总体比较优雅,比较好维护,除了写sql的时候会有那么一点点麻烦。

    但我们感兴趣的是SqlParameterSource

    我们来看下SqlParameterSource

 * @author Thomas Risberg
 * @author Juergen Hoeller
 * @since 2.0
 * @see NamedParameterJdbcOperations
 * @see NamedParameterJdbcTemplate
 * @see MapSqlParameterSource
 * @see BeanPropertySqlParameterSource
 */
public interface SqlParameterSource 


SqlParameterSource 接口有三个真正的实现:
org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource
org.springframework.jdbc.core.namedparam.MapSqlParameterSource
org.springframework.jdbc.core.namedparam.EmptySqlParameterSource

其中BeanPropertySqlParameterSource特别受一些人喜欢(有些人喜欢把任何东西包装成bean)

BeanPropertySqlParameterSource的源码注释:
SqlParameterSource implementation that obtains parameter valuesfrom bean properties of a given JavaBean object. The names of the beanproperties have to match the parameter names. 

Uses a Spring BeanWrapper for bean property access underneath.

 

    下面来看看一个例子:

@Override
    @Transactional(propagation=Propagation.REQUIRED,isolation=Isolation.DEFAULT,rollbackFor=Exception.class)
    public int addFamilyWithNJT2(String name) {
        String sql="insert into family(name) values(:name)";
        //使用bean/pojo传递参数
        Family family=new Family(name);
        KeyHolder keyHolder=new GeneratedKeyHolder();        
        SqlParameterSource paramSource=new BeanPropertySqlParameterSource(family);
        int qty=njdbcTp.update(sql, paramSource, keyHolder);
        JSONObject.toJSONString(paramSource, true);
        return keyHolder.getKey().intValue();
    }

 

三、批处理执行

    批量执行,多用于数据导入,采集的业务场景。

    当然,如果是对付高速大量的数据导入,不建议使用目前这种方式,建议直接使用原生的jdbc或者是数据库产生的api来操作。

    只不过,只要咱的数据量不是太大,一般也够用。

   下面来个例子:

   

@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class)
    @Override
    public String batchExecute() {
        /**
         * 几种基本的batch操作
         */
        String sql = "insert into family(name,batch_no) values(?,?)";
        //1.0 ParameterizedPreparedStatementSetter
        List<Object[]> argList = new ArrayList<>();
        String batchNo = UUID.randomUUID().toString();
        for (int i = 0; i < 2; i++) {
            Object[] a = new Object[2];
            a[0] = UUID.randomUUID().toString();
            a[1] = batchNo;
            argList.add(a);
        }
        jdbcTp.batchUpdate(sql, argList, 4, (PreparedStatement ps, Object[] argument) -> {
            ps.setObject(1, argument[0]);
            ps.setObject(2, argument[1]);
        });

        //2.0 BatchPreparedStatementSetter
        BatchPreparedStatementSetter btss = new BatchPreparedStatementSetter() {
            @Override
            public void setValues(PreparedStatement ps, int i) throws SQLException {
                ps.setObject(1, argList.get(i)[0]);
                ps.setObject(2, argList.get(i)[1]);
            }

            @Override
            public int getBatchSize() {
                //这个大小不能超过参数集合大小,否则会报错。
                return argList.size();
            }
        };
        int[] qtys = jdbcTp.batchUpdate(sql, btss);
        int ttlQty = 0;
        for (int i = 0, len = qtys.length; i < len; i++) {
            ttlQty += qtys[i];
        }
        System.out.println(ttlQty);
        return batchNo;
    }

 

 

四、插入并返回自增主键值

 @Override
    public int addFamilyWithJT(String name) {
        KeyHolder keyHolder = new GeneratedKeyHolder();
        jdbcTp.update((Connection con) -> {
            String sql = "insert into family(name) values(?)";
            PreparedStatement ps =con.prepareStatement(sql, new String[]{"custom_id"});
            ps.setInt(1, Integer.valueOf(name));
            return ps;
        }, keyHolder);
        return keyHolder.getKey().intValue();
    }

 

 

五、查询并返回bean/pojo

@Override
    public HcsThirdsrv getByName(String serviceName) {
        String sql="SELECT\r\n"
                + "  service_name,\r\n"
                + "  service_name_cn,\r\n"
                + "  instance_service_name,\r\n"
                + "  service_desc,\r\n"
                + "  status_flag,\r\n"
                + "  add_time,\r\n"
                + "  last_optime\r\n"
                + "FROM\r\n"
                + "  hcs_thirdsrv \n"
                + "where  service_name=? or service_name_cn=? \n"
                + "limit 1";
        RowMapper<HcsThirdsrv> rowMapper =new BeanPropertyRowMapper<HcsThirdsrv>(HcsThirdsrv.class);
        HcsThirdsrv srv=this.jdbcTp.queryForObject(sql, rowMapper,serviceName);
        return srv;
    }

    spring对于返回bean的支持并不友好,希望以后的版本,能够直接出一个不用rowMapper的(现在我们自己都是对jdbcTemplate再封装一遍)。

六、性能

 

1.比较-mybatis、原生jdbc

   主要是和mybatis比较,网上有专门的测试,例如:

   https://blog.csdn.net/liulk20170518/article/details/119358143

   但不是很多,也比较老旧。此外考虑到mybatis的不断进化。

   但毫无疑问,mybatis总会比jdbcTemplate慢一些,因为它花了额外的一些时间做七七八八的处理。

   执行速度上,原生jdbc>jdbcTemplate>mybatis,这是没有异议的。

   我们很多项目还大量使用mybatis,主要是出于工程考虑:用cpu的速度来弥补工程师的思维能力欠缺和手动速度,以提高工程效率。

   灵活优雅,有时候就是慢的代名词。

   简单粗暴,有时候能够更快解决问题。

2.如何优化

   java的主要性能消耗在于数据转换、反射和解析,后者是先天不可调整,所以只能尽量减少反射操作和数据转换。

   所以,如果可能的话,执行sql的时候,尽量使用ListMap或者Map来返回结果。如果您看不习惯没有关系,只要快就可以了。

七、异常

   spring提供了几个常见的异常:

  • BadSqlGrammarException --语法错误
  • CannotGetJdbcConnectionException -- 获取连接异常
  • IncorrectResultSetColumnCountException -- 错误结果集合列数异常,例如本来只要一列的,现在有2列
  • InvalidResultSetAccessException  --  不可用的结果集合读取异常,通常发生在列位置或者名称设置错误的情况
  • JdbcUpdateAffectedIncorrectNumberOfRowsException -- 实际影响行数超出预计的异常。例如本来应该只影响1行,但现在2行
  • LobRetrievalFailureException -- 读取大字段数据失败
  • SQLWarningException  -- sql警告异常。没有特别说明
  • UncategorizedSQLException -- 无分类sql异常

    实际执行的时候,更可能抛出的是org.springframework.dao下异常,这个包路径属于spring事务模块。

    在这个包里面,有更多更在明确的异常说明,例如下图:

 

 

八、和spring其它组件关系

    影响比较多,其中主要是事务。其余略。

    如何保证事务?

    从原生jdbc可以看出,要完成一个事务,它的代码大概是这样的:

https://www.cnblogs.com/azhqiang/p/4044127.html
------------------------------------------------------------
private Connection conn = null;  
private PreparedStatement ps = null;  
try {  
    conn.setAutoCommit(false);  //将自动提交设置为false  
    ps.executeUpdate("修改SQL"); //执行修改操作  
    ps.executeQuery("查询SQL");  //执行查询操作                 
    conn.commit();      //当两个操作成功后手动提交  
} catch (Exception e) {  
    conn.rollback();    //一旦其中一个操作出错都将回滚,使两个操作都不成功  
    e.printStackTrace();  
} 

    保持事务的关键在于:使用同一个连接。

    以oracle为例子,不同的连接就是不同的会话,它们之间的事务是无关的。这个原则在绝大部分rdbms上是一样的成立的,这也即使rdbms存在的主要理由之一。

    如果spirng要完成事务的关键就是保证在事务传递的情况下,能够使用同样的一个连接(大部分情况下)。

    这里就涉及到spring的事务组件spring-tx和spring-jdbc中的DataSourceUtils。

    spring-tx如何如何保证连接的一致性,是一个有点小小复杂的事情,本文略,总之道路就是这个道理。

 

九、有关工具类

  •     DataSourceUtils
  •     JdbcUtils

       在只有一个数据源的环境中获取当前数据源(仅限于特定环境):

     

package study.spring;

import javax.sql.DataSource;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class SpringbootAwareHelper implements BeanFactoryAware, ApplicationContextAware {

    private static BeanFactory beanFactory;
    private static ApplicationContext appContext;

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        SpringbootAwareHelper.beanFactory = beanFactory;
    }

    public static <T> T getBean(String id, Class<T> type) {
        return beanFactory.getBean(id, type);
    }

    public static <T> T getBean(Class<T> type) {
        return beanFactory.getBean(type);

    }

    @SuppressWarnings("unchecked")
    public static <T> T getBean(String beanName) {
        return (T) beanFactory.getBean(beanName);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        appContext = applicationContext;
        Object ds = applicationContext.getBean("dataSource");
        if (ds != null) {
            System.out.println("当前的连接池是:" + ds.getClass().getName());
        } else {
            System.out.println("没有连接池!");
        }
        // 打印加载的bean信息

        /*String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            System.out.println(beanDefinitionName);
        }*/
    }

    public static DataSource getCurrentDatasource() {
        return appContext.getBean(DataSource.class);
    }

}

十、适用场景

  1. 对性能要求较高的。
  2. 个性化要求高的。有些sql在mapper中写有点麻烦,尤其一些复杂的sql。
  3. 不想多一个依赖的,例如一些核心的东西。能够少依赖就尽量少依赖。这种情况下,可能直接上原生jdbc了,连jdbcTemplate都不用了

    因为这个缘故,所以spring提供了spring-jdbc组件。

    例如公用组件,核心工具,应该尽量少依赖外部的框架。当然实际做的时候,更可能取决于公司的规模和实例,项目和产品的性质。

    如果我们想的再长远一些,那么java是否真有存在的必要性?除了强大的生态,java并没有什么傲人的优势,而计算机最大优势就是人对于速度的最求。所以java要存活下去,则必须

修改jvm和编译器,虽然现在已经不断在优化,但是还不够。最好的办法是提供一个编译器直接在运行主机上进行编译打包,不要耗费时间每次去询问。

     一处编译处处执行,并没有那么大的必要性,尤其是做项目。

   

 

posted @ 2022-05-02 12:13  正在战斗中  阅读(162)  评论(0编辑  收藏  举报