[原创]Spring JdbcTemplate 使用总结与经验分享

引言

近期开发的几个项目,均是基于Spring boot框架的web后端项目,使用JdbcTemplate执行数据库操作,实际开发过程中,掌握了一些有效的开发经验,踩过一些坑,在此做个记录及总结,与各位读者分享。

欢迎留言与我交流。

正确使用JdbcTemplate执行数据库操作

1、Bean声明

新增类型DatabaseConfiguration,添加注解@Configuration

该类型用于DataSource及JdbcTempate Bean的声明

基础代码如下

@Configuration
class DatabaseConfiguration {
    @Bean
    public DataSource dataSource() {
        DataSource dataSource;
        ...
        return dataSource;
    }

    @Bean
    public JdbcTemplate jdbcTemplate() {
        return new JdbcTemplate(dataSource());
    }
}

注意这里将DataSource定义为Bean,Spring boot默认创建的TransactionManager对象依赖DataSource,若未将DataSource声明为Bean,则无法使用数据库事务

2、封装Dao类型

对于每一个数据库表,构建独立的Dao类型,提供供业务层调用的接口,注入JdbcTemplate对象,以实际操作db

可以定义基类如下

/**
 * Created by Ant on 2015/1/1.
 */
public abstract class AntSoftDaoBase {
    @Resource(name = "jdbcTemplate")
    private JdbcTemplate jdbcTemplate;

    private String tableName;

    protected AntSoftDaoBase(String tableName) {
        this.tableName = tableName;
    }

    protected JdbcTemplate getJdbcTemplate() {
        return jdbcTemplate;
    }

    public void clearAll() {
        getJdbcTemplate().update("DELETE FROM " + tableName);
    }

    public int count() {
        return getJdbcTemplate().queryForObject( "SELECT count(*) FROM " + tableName, Integer.class);
    }
}

通过@Resource注入jdbcTemplate对象,由于我仅定义了一个jdbcTemplate bean,可以这里可以省略掉name参数,及@Resource即可,或者使用@Autowired

如对于数据库中的table app

建立对应的Dao派生类

/**
 * Created by Ant on 2015/1/1.
 */
@Repository
public class AppDao extends AntSoftDaoBase{
    private Logger logger = LoggerFactory.getLogger(getClass());

    private static final String TABLE_NAME = "app";

    private static final String COLUMN_NAMES = "name, user_id, title, description, ctime, status";

    public AppDao() {
        super(TABLE_NAME);
    }

    public int create(final AppInfo appInfo) {
       ...
    }

    public List<AppInfo> list(int pageNo, int pageSize) {
        ...
    }

    public AppInfo get(int appId) {
       ...
    }

    public void update(AppInfo appInfo) {
        ...
    }
}

该Dao类型提供了对AppInfo数据的增删查改接口,对这些接口的具体实现,后面再进行详细介绍

3、使用Tomcat-jdbc数据库连接池

引入数据库连接池,将大幅度提升数据库操作性能

本例描述Tomcat-jdbc数据库连接池使用方式

Pom文件中引入tomcat-jdbc依赖项

        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
            <version>7.0.42</version>
        </dependency>

创建连接池DataSource的逻辑封装在如下方法中,DatabaseConfiguration.dataSource方法内部可以直接调用此方法获取具备连接池功能的DataSource

    private DataSource getTomcatPoolingDataSource(String databaseUrl, String userName, String password) {
        org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource();
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl(databaseUrl);
        dataSource.setUsername(userName);
        dataSource.setPassword(password);

        dataSource.setInitialSize(5); // 连接池启动时创建的初始化连接数量(默认值为0)
        dataSource.setMaxActive(20); // 连接池中可同时连接的最大的连接数
        dataSource.setMaxIdle(12); // 连接池中最大的空闲的连接数,超过的空闲连接将被释放,如果设置为负数表示不限
        dataSource.setMinIdle(0); // 连接池中最小的空闲的连接数,低于这个数量会被创建新的连接
        dataSource.setMaxWait(60000); // 最大等待时间,当没有可用连接时,连接池等待连接释放的最大时间,超过该时间限制会抛出异常,如果设置-1表示无限等待
        dataSource.setRemoveAbandonedTimeout(180); // 超过时间限制,回收没有用(废弃)的连接
        dataSource.setRemoveAbandoned(true); // 超过removeAbandonedTimeout时间后,是否进 行没用连接(废弃)的回收
        dataSource.setTestOnBorrow(true);
        dataSource.setTestOnReturn(true);
        dataSource.setTestWhileIdle(true);
        dataSource.setValidationQuery("SELECT 1");
        dataSource.setTimeBetweenEvictionRunsMillis(1000 * 60 * 30); // 检查无效连接的时间间隔 设为30分钟
        return dataSource;
    }

关于各数值的配置请根据实际情况调整

配置重连逻辑,以在连接失效是进行自动重连。默认情况下mysql数据库将关闭掉超过8小时的连接,开发的第一个java后端项目,加入数据库连接池后的几天早晨,web平台前几次数据库操作总是失败,配置重连逻辑即可解决

使用HSQL进行数据库操作单元测试

忠告:数据库操作需要有单元测试覆盖

本人给出如下理由:

1、对于使用JdbcTemplate,需要直接在代码中键入sql语句,如今编辑器似乎还做不到对于java代码中嵌入的sql语句做拼写提示,经验老道的高手,拼错sql也不罕见

2、更新db表结构后,希望快速知道哪些代码需要更改,跑一便单测比人肉搜索来的要快。重构速度*10

3、有单测保证后,几乎可以认为Dao层完全可靠。程序出错,仅需在业务层排查原因。Debug速度*10

4、没有单测,则需要在集成测试时构建更多更全面的测试数据,实际向mysql中插入数据。数据构建及维护麻烦、测试周期长

1、内嵌数据库HSQLDB

HSQLDB是一个开放源代码的JAVA数据库,其具有标准的SQL语法和JAVA接口

a)配置HSQL DataSource

引入HSQLDB依赖项

        <dependency>
            <groupId>org.hsqldb</groupId>
            <artifactId>hsqldb</artifactId>
            <version>2.3.0</version>
        </dependency>

生成DataSource的方法可以如下方式实现

    @Bean
    public DataSource antsoftDataSource() {
        DataSource dataSource;
        if (antsoftDatabaseIsEmbedded) {
            dataSource = getEmbeddedHsqlDataSource();
        } else {
            dataSource =
                    getTomcatPoolingDataSource(antsoftDatabaseUrl, antsoftDatabaseUsername, antsoftDatabasePassword);
        }
        return dataSource;
    }

其中antsoftDatabaseIsEmbedded等对象字段值的定义如下

    @Value("${antsoft.database.isEmbedded}")
    private boolean antsoftDatabaseIsEmbedded;

    @Value("${antsoft.database.url}")
    private String antsoftDatabaseUrl;

    @Value("${antsoft.database.username}")
    private String antsoftDatabaseUsername;

    @Value("${antsoft.database.password}")
    private String antsoftDatabasePassword;

通过@Value指定配置项key名称,运行时通过key查找配置值替换相应字段

配置文件为resources/application.properties

antsoft.database.isEmbedded=false
antsoft.database.url=jdbc:mysql://127.0.0.1:3306/antsoft_app
antsoft.database.username=root
antsoft.database.password=ant

单元测试配置文件为resources/application-test.properties

antsoft.database.isEmbedded=true

表示单测使用内嵌数据库

b)HSQL数据库初始化脚本

创建Hsql DataSource时,同时执行数据库初始化操作,构建所需的表结构,插入初始数据

getEmbeddedHsqlDataSource方法实现如下

    private DataSource getEmbeddedHsqlDataSource() {
        log.debug("create embeddedDatabase HSQL");
        return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.HSQL).addScript("classpath:db/hsql_init.sql").build();
    }

通过addScript指定初始化数据库SQL脚本resources/db/hsql_init.sql,内容如下

SET DATABASE SQL SYNTAX MYS TRUE;

CREATE TABLE app (
  id int GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1) NOT NULL,
  name varchar(64) NOT NULL,
  user_id varchar(64) NOT NULL,
  title varchar(64) NOT NULL,
  description varchar(1024) NOT NULL,
  ctime datetime NOT NULL,
  status int NOT NULL,
  PRIMARY KEY (id),
  UNIQUE (name)
);

CREATE TABLE app_unique_name (
  id int GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1) NOT NULL,
  unique_name varchar(64) NOT NULL UNIQUE,
  PRIMARY KEY (id)
);

...

HSQL语法与MySql语法存在差异,使用是需注意,我在开发过程中注意到的不同点列举如下

  - 不支持tinyint等数据类型,int后不允许附带表示数据长度的括号,如不支持int(11)

  - 不支持index索引,但支持unique index

  - 不支持AUTO_INCREMENT语法

c)验证你的HSQL脚本

可采用如下方式验证hsql语句正确性

在本地maven仓库中找到hsqldb(正确引入过hsqldb),博主本机目录 C:\Users\ant\.m2\repository\org\hsqldb\hsqldb\2.3.2

执行hsqldb-2.3.2.jar  (java -jar hsqldb-2.3.2.jar)

默认窗体一个提示框,点击ok。在右侧输入SQL语句,执行工具栏中中Execuete SQL

如下截图,显示SQL执行成功

上图SQL语句如下

CREATE TABLE app_message (
  id bigint GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1) NOT NULL,
  app_id int NOT NULL,
  message varchar(1024) NOT NULL,
  ctime datetime NOT NULL,
  status int NOT NULL,
  PRIMARY KEY (id)
);

该SQL语句中使用  GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1)  替代 AUTO_INCREMENT,似乎是HSQL不支持该语法,读者亲自尝试一下

AUTO_INCREMENT替代方案来源如下

http://stackoverflow.com/questions/13206473/create-table-syntax-not-working-in-hsql

2、编写单元测试覆盖Dao数据库操作

使用JUnit及Spring-test。单测可以直接注入所需的Bean

统一定义单元测试注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
@IntegrationTest("server.port=0")
@ActiveProfiles("test")
public @interface AntSoftIntegrationTest {
}

定义测试类型,添加如下注解

@AntSoftIntegrationTest
@RunWith(SpringJUnit4ClassRunner.class)

我对自己代码的期望是,尽可能100%的Dao方法都被单元测试覆盖。

以下代码演示对AppService(其接口实现转发调用AppDao相应接口)进行的基本单元测试,其中测试了create、update及get三种操作

@AntSoftIntegrationTest
@RunWith(SpringJUnit4ClassRunner.class)
public class AppServiceTests {
    @Autowired
    private AppService appService;

    @Autowired
    private TestService testService;

    @Before
    public void clearApp() {
        testService.clearApp();
    }

    @Test
    public void testApp() {
        final String name = "xxx";
        final String userId = "Ant";
        final String title = "Hello World";
        final String description = "Description for Hello World";

        final String updatedName = "xxx";
        final String updatedUserId = "Ant";
        final String updatedTitle = "Hello World";
        final String updatedDescription = "Description for Hello World";

        int appId;
        {
            // 创建应用
            AppInfo appInfo = new AppInfo();
            appInfo.setName(name);
            appInfo.setUserId(userId);
            appInfo.setTitle(title);
            appInfo.setDescription(description);
            appId = appService.createApp(appInfo);
        }

        CheckAppInfo(appId, name, userId, title, description, AppStatus.NORMAL);

        {
            // 更新应用
            AppInfo appInfo = new AppInfo();
            appInfo.setId(appId);
            appInfo.setName(updatedName);
            appInfo.setUserId(updatedUserId);
            appInfo.setTitle(updatedTitle);
            appInfo.setDescription(updatedDescription);
            appService.updateApp(appInfo);
        }

        CheckAppInfo(appId, updatedName, updatedUserId, updatedTitle, updatedDescription, AppStatus.NORMAL);
    }

    // 获取应用,并验证数据
    private void CheckAppInfo(int appId, String name, String userId, String title, String description,
                              AppStatus appStatus) {
        AppInfo appInfo = appService.getApp(appId);
        assertEquals(appId, appInfo.getId());
        assertEquals(name, appInfo.getName());
        assertEquals(userId, appInfo.getUserId());
        assertEquals(title, appInfo.getTitle());
        assertEquals(description, appInfo.getDescription());
        assertEquals(appStatus, appInfo.getStatus());
    }
}

开发经验分享

本节记录笔者在实际项目开发过程中遇到的问题及解决方法、以及一些良好的开发实践

1、失效的事务

在使用Spring提供的事务处理机制时,事务的start与commit rollback操作由TransactionManager对象维护,开发中,我们只需在需要进行事务处理的方法上添加@Transactional注解,即可轻松开启事务

见Spring boot源码

spring-boot/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceTransactionManagerAutoConfiguration.java

/**
 * {@link EnableAutoConfiguration Auto-configuration} for
 * {@link DataSourceTransactionManager}.
 *
 * @author Dave Syer
 */
@Configuration
@ConditionalOnClass({ JdbcTemplate.class, PlatformTransactionManager.class })
public class DataSourceTransactionManagerAutoConfiguration implements Ordered {

    @Override
    public int getOrder() {
        return Integer.MAX_VALUE;
    }

    @Autowired(required = false)
    private DataSource dataSource;

    @Bean
    @ConditionalOnMissingBean(name = "transactionManager")
    @ConditionalOnBean(DataSource.class)
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(this.dataSource);
    }

    @ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class)
    @Configuration
    @EnableTransactionManagement
    protected static class TransactionManagementConfiguration {

    }

}

由此可见,若未将DataSource什么为Bean,将导致transactionManager得不到创建,@Transactional注解将毫无作用

当然,也有另一种方法,及不使用默认的transactionManager,而是自行定义,如下,在DatabaseConfiguration类中增加如下方法

    @Bean
    public PlatformTransactionManager transactionManager() {return new DataSourceTransactionManager(myDataSource());
    }

默认会使用方法名作为bean的命名,因此此处覆盖了默认的transactionManager Bean对象

如何多数据源启用事务(此处描述的非分布式事务)?

如果项目中涉及操作多个数据库,则存在多个数据源DataSource。解决方案同上例,即自行声明transactionManager Bean,与每个DataSource一一对应。需要注意的是,在使用@Transactional注解是,需要添加transactionManager Bean的名称,如@Transactional("myTransactionManager")

2、获取新增数据的自增id

如下Dao类型,方法create演示了如何创建一条MessageInfo记录,同时,获取该新增数据的主键,即自增id

@Repository
public class MessageDao extends AntSoftDaoBase {
    private static final String TABLE_NAME = "app_message";

    private static final String COLUMN_NAMES = "app_id, message, ctime, status";

    protected MessageDao() {
        super(TABLE_NAME);
    }

    private static final String SQL_INSERT_DATA =
            "INSERT INTO " + TABLE_NAME + " (" + COLUMN_NAMES + ") "
                    + "VALUES (?, ?, ?, ?)";

    public int create(final MessageInfo messageInfo) {
        KeyHolder keyHolder = new GeneratedKeyHolder();
        getJdbcTemplate().update(new PreparedStatementCreator() {
                                     public PreparedStatement createPreparedStatement(Connection connection) throws
                                             SQLException {
                                         PreparedStatement ps =
                                                 connection.prepareStatement(SQL_INSERT_DATA, Statement.RETURN_GENERATED_KEYS);
                                         int i = 0;
                                         ps.setInt(++i, messageInfo.getAppId());
                                         ps.setString(++i, messageInfo.getMessage());
                                         ps.setTimestamp(++i, new Timestamp(new Date().getTime()));
                                         ps.setInt(++i, 0); // 状态默认为0
                                         return ps;
                                     }
                                 }, keyHolder
        );
        return keyHolder.getKey().intValue();
    }
    ...
}

3、SQL IN 语句

IN语句中的数据项由逗号分隔,数量不固定,"?"仅支持单参数的替换,因此无法使用。此时只能拼接SQL字符串,如更新一批数据的status值,简单有效的实现方式如下

    private static final String SQL_UPDATE_STATUS =
            "UPDATE " + TABLE_NAME + " SET "
                    + "status = ? "
                    + "WHERE id IN (%s)";

    public void updateStatus(List<Integer> ids, Status status) {
        if (ids == null || ids.size() == 0) {
            throw new IllegalArgumentException("ids is empty");
        }
        String idsText = StringUtils.join(ids, ", ");
        String sql = String.format(SQL_UPDATE_STATUS , idsText);
        getJdbcTemplate().update(sql, status.toValue());
    } 

4、查询数据一般方法,及注意事项

AppDao类型中提供get方法,以根据一个appId获取该APP数据,代码如下

    private static final String SQL_SELECT_DATA =
            "SELECT id, " + COLUMN_NAMES + " FROM " + TABLE_NAME + " WHERE id = ?";

    public AppInfo get(int appId) {
        List<AppInfo> appInfoList = getJdbcTemplate().query(SQL_SELECT_DATA, new Object[] {appId}, new AppRowMapper());
        return appInfoList.size() > 0 ? appInfoList.get(0) : null;
    }

注意点:由于主键id会唯一标识一个数据项,有些人会使用queryForObject获取数据项,若未找到目标数据时,该方法并非返回null,而是抛异常EmptyResultDataAccessException。应使用query方法,并检测返回值数据量

AppRowMapper用于解析每行数据并转成Model类型,其代码如下

    private static class AppRowMapper implements RowMapper<AppInfo> {
        @Override
        public AppInfo mapRow(ResultSet rs, int i) throws SQLException {
            AppInfo appInfo = new AppInfo();
            appInfo.setId(rs.getInt("id"));
            appInfo.setName(rs.getString("name"));
            appInfo.setUserId(rs.getString("user_id"));
            appInfo.setTitle(rs.getString("title"));
            appInfo.setDescription(rs.getString("description"));
            appInfo.setCtime(rs.getTimestamp("ctime"));
            appInfo.setStatus(AppStatus.fromValue(rs.getInt("status")));
            return appInfo;
        }
    }
时间: 2024-08-07 04:08:54

[原创]Spring JdbcTemplate 使用总结与经验分享的相关文章

【原创经验分享】JQuery(Ajax)调用WCF服务

最近在学习这个WCF,由于刚开始学 不久,发现网上的一些WCF教程都比较简单,感觉功能跟WebService没什么特别大的区别,但是看网上的介绍,就说WCF比WebService牛逼多少多少,反正我刚开始入门,就没觉得多大区别啦,这次写的东西跟WebService一样,我们写了一个WCF,那当然就是要用的,要用的话,当然不能只能在.NET平台下用了,必须跨平台呀,所以,Ajax能调用,这个基本的要求就必须要实现的了,所以,本次经验分享就是写JQuery的Ajax调用WCF的服务了.   一.新建

短视频自媒体实操经验分享,原来赚钱就这么简单!

从去年开始,由一条.二更等短视频自媒体开始崛起,短视频的红利期到来了.大家想通过做短视频搬运工想赚钱,要懂得讨好平台和读者.了解平台的游戏规则和网民内心的真实需求,只有这样才能获得平台更多的推荐和网民的喜爱. 以今日头条为例,视频的播放量主要通过系统推荐来获得,它的智能推荐系统(俗称人工智能)就是把合适的新闻推荐给对他感兴趣的人群.头条每天的内容几百万,不可能靠小编来干预推荐,都是智能机器人根据网友的互动数据来决定推荐的大小. 智能机器人怎么能知道网民的兴趣爱好,靠的就是大量网民的行为数据. 比

我在3天内众筹到1.8万的经验分享

2014年6月13日,正当世界杯火热上演的时候,我小心翼翼地询问秦刚老师(历任IT世界网站CEO,39健康网联席总裁.微信号.QQ号都是 1111884.)我能否加入他和"中国SEO第一人"王通老师(微信号ufoerwang)开的"秦王会"?(垂直互联网精英俱乐部,里面各行各业的牛人) 秦刚老师对我一番质问,内容大概是自我介绍,然后做了什么,能够给"秦王会"带来什么.我在这之前就和秦刚老师有聊过,估计他对我早就有一点认识,所以我在自我介绍后先入为

暑期报修项目经验分享二(附原码)

楔子 第二期项目经验分享,其实整个项目流程,我们只是参与了其中一小部分,仅仅是添加了材料管理模块的内容,在第一期的实践里面,主要维护材料类别的增查改操作.二期维护做的是材料出入库处理,因为我处理的是入库管理这一块,所以分享下做这一块的经验. 需求分析: 在分析业务需求的时候,因为考虑到出入库在一张数据表里面,即是共用数据,那么type区分出入库,在写代码的时候一个反人类的操作就是把流水号设置成整形,虽然影响不大,另外关于材料类别代码这里,因为材料类别是三级联动设置,所以我们只取最后一级的代码,因

IdentityServer4系列之中文文档及实际项目经验分享

0.前言 原文:http://docs.identityserver.io/en/release/声明: 1.目录一至五章节根据IdentityServer英文文档翻译而来,有些内容会根据自己的理解来表述的(包括标题),文档的内容会不断的更新. 2.第六章节会进行阐述在实际项目中所用的内容以及问题 一.介绍 IdentityServer4特性简介 深入讲解 术语解释 支持规范 包和构建说明 二.快速入门 设置和概述 使用客户端证书访问API(客户端模式) 使用密码访问API(密码模式) 使用Op

MySQL 性能优化的最佳20多条经验分享

今天,数据库的操作越来越成为整个应用的性能瓶颈了,这点对于Web应用尤其明显.关于数据库的性能,这并不只是DBA才需要担心的                    事,而这更是我们程序员需要去关注的事情. 当我们去设计数据库表结构,对操作数据库时(尤其是查表时的SQL语句),我们都需要注意数据操作的性能.这里,我们不会讲过多的SQL语                句的优化,而只是针对MySQL这一Web应用最多的数据库.希望下面的这些优化技巧对你有用. 1. 为查询缓存优化你的查询 大多数的M

使用latex撰写博士,硕士学位论文(浙大博士经验分享)

使用latex撰写博士,硕士学位论文(浙大博士经验分享) 浙大博士:  个人感觉,还是要用latex来写.因为之前发过几篇word排版的中文论文,在参考文献的引用.文字格式调整上,实在是难受.如果坚持用word,请一定用endnote结合word来管理参考文献,否则100多个参考文献的调整一定会浪费你很多时间的. latex不难,就几个简单功能,稍微适应一下就好. 言归正传,我们开始: 1. 模板请采用http://zjuthesistex.googlecode.com/files/rookie

关于Altera LVDS 经验分享

http://www.alteraforum.com.cn/showtopic-355-1.aspx 骏龙科技_技术资料汇总52:关于Altera LVDS 经验分享 作者:上海骏龙 毛老师 相信大家在帮助客户调试altea lvds接口的时候,都遇到过不少问题.下面两个应该是最常见的: (一)字节对齐字节对齐是我遇到的最多的,收到的数据并不是错了,只是起始bit的位置变了.使用bitslip,比特滑动控制,可以解决这个问题,但有更为简单的方法:只要随路时钟与数率的比值(W),与串化因子(J)相

20多条MySQL 性能优化经验分享

当我们去设计数据库表结构,对操作数据库时(尤其是查表时的SQL语句),我们都需要注意数据操作的性能.这里,我们不会讲过多的SQL语句的优化,而只是针对MySQL这一Web应用最多的数据库.希望下面的这些优化技巧对你有用. 1. 为查询缓存优化你的查询 大多数的MySQL服务器都开启了查询缓存.这是提高性最有效的方法之一,而且这是被MySQL的数据库引擎处理的.当有很多相同的查询被执行了多次的时候,这些查询结果会被放到一个缓存中,这样,后续的相同的查询就不用操作表而直接访问缓存结果了. 这里最主要