[原創]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替代方案來源如下
 

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;
        }
    }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM