@Configurationclass 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
/** * Created by Ant on 2015/1/1. */@Repositorypublic 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表結構后,希望快速知道哪些代碼需要更改,跑一便單測比人肉搜索來的要快。重構速度*103、有單測保證后,幾乎可以認為Dao層完全可靠。程序出錯,僅需在業務層排查原因。Debug速度*104、沒有單測,則需要在集成測試時構建更多更全面的測試數據,實際向mysql中插入數據。數據構建及維護麻煩、測試周期長1、內嵌數據庫HSQLDBHSQLDB是一個開放源代碼的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=falseantsoft.database.url=jdbc:mysql://127.0.0.1:3306/antsoft_appantsoft.database.username=rootantsoft.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-hsql2、編寫單元測試覆蓋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@Repositorypublic 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; } }
新聞熱點
疑難解答