Spring實戰 十 連線資料庫
扯dz
Spring提供了jdbcTemplate簡化資料庫操作。使用JDBC原生來開發資料庫難受的一批,只有一個SQLException讓我們不知道發生了什麼問題
以下是Spring提供的異常和JDBC的異常對照表
而且Spring的異常都是執行時異常,不強制我們必須對異常進行處理,其實大部分SQL異常我們都沒辦法處理。
還有就是jdbc的模板程式碼,都寫過,即使只需要編寫一行插入語句也要幾十行模板程式碼。Spring的jdbcTemplate使用了模板設計模式,把構造SQL語句和將ResultSet轉換成物件這兩個核心操作留給我們,其他模板程式碼由Spring執行。
這是使用jdbcTemplate後的查詢程式碼
資料來源
既然要連線資料庫,就要有資料來源。Spring支援多種資料來源
- 通過JNDI查詢的資料來源
- JDBC驅動中定義的資料來源
- 連線池的資料來源
連線池資料來源
這裡只介紹連線池的資料來源。選用c3p0連線池,mysql資料庫和一個用於開發環境的h2嵌入式資料庫,需要引入的依賴如下
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>1.3.172</version> </dependency> <dependency> <groupId>com.mchange</groupId> <artifactId>c3p0</artifactId> <version>${c3p0.version}</version> </dependency> <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.connector.version}</version> </dependency>
然後就是定義資料來源,在WebConfig.java
類上添加了@PropertySource
註解引入外部配置檔案,然後通過@Value
來將對應的配置繫結在引數上。
@Bean public DataSource dataSource( @Value("${jdbc.url}") String url, @Value("${jdbc.username}") String username, @Value("${jdbc.password}") String password, @Value("${jdbc.driverClass}") String driverClass, @Value("${jdbc.initialSize}") Integer initialSize, @Value("${jdbc.maxPoolSize}") Integer maxPoolSize, @Value("${jdbc.minPoolSize}") Integer minPoolSize, @Value("${jdbc.maxIdleTime}") Integer maxIdleTime ) throws PropertyVetoException { ComboPooledDataSource dataSource = new ComboPooledDataSource(); dataSource.setJdbcUrl(url); dataSource.setUser(username); dataSource.setPassword(password); dataSource.setDriverClass(driverClass); dataSource.setInitialPoolSize(initialSize); dataSource.setMaxPoolSize(maxPoolSize); dataSource.setMinPoolSize(minPoolSize); dataSource.setMaxIdleTime(maxIdleTime); return dataSource; }
基於profile配置資料來源
使用@Profile
註解定義在不同環境下的資料來源,在開發環境下使用h2嵌入式資料庫。
@Bean
@Profile("dev")
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:schema.sql")
.addScript("classpath:test-data.sql")
.build();
}
@Bean
@Profile("prod")
public DataSource dataSource(
@Value("${jdbc.url}") String url,
@Value("${jdbc.username}") String username,
@Value("${jdbc.password}") String password,
@Value("${jdbc.driverClass}") String driverClass,
@Value("${jdbc.initialSize}") Integer initialSize,
@Value("${jdbc.maxPoolSize}") Integer maxPoolSize,
@Value("${jdbc.minPoolSize}") Integer minPoolSize,
@Value("${jdbc.maxIdleTime}") Integer maxIdleTime
) throws PropertyVetoException {
System.out.println("CREATING DATASOURCE c3p0");
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setJdbcUrl(url);
dataSource.setUser(username);
dataSource.setPassword(password);
dataSource.setDriverClass(driverClass);
dataSource.setInitialPoolSize(initialSize);
dataSource.setMaxPoolSize(maxPoolSize);
dataSource.setMinPoolSize(minPoolSize);
dataSource.setMaxIdleTime(maxIdleTime);
return dataSource;
}
基於我們這個方法配置的嵌入式資料庫h2會在程式啟動時在記憶體中建立資料庫,並且執行schema.sql
和test-data.sql
,這兩個檔案不貼出來了。
然後就是設定當前啟用的profile,在java配置中有兩種方式設定這個profile,如下是其中一種。
public class SpittrWebApplicationInitializer
extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
super.onStartup(servletContext);
// 設定環境為dev
servletContext.setInitParameter("spring.profiles.active", "dev");
}
// ...
}
jdbcTemplate
下面就是使用jdbcTemplate進行開發了。
先定義一個jdbcTemplate
的Bean
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
Repository
Repository是我們程式中定義的持久層介面,介面中提供最基本的資料庫訪問,比如如下兩個介面。
public interface SpitterRepository {
int save(Spitter spitter);
Spitter getOne(Long id);
Spitter getOneByUserName(String username);
}
public interface SpittleRepository {
List<Spittle> findSpittles(long max, int count);
Spittle findOne(long id);
}
我們的Spittr程式之前就是通過這兩個介面來操作資料訪問層的。在學習第十章之前,我們都沒有連線過資料庫,但是我們的程式依舊可以使用虛擬的資料正常執行,就像真的有資料庫在那一樣。這個好處就是介面帶來的。
程式只知道我們有一個SpitterRepository
介面,並且能通過save
方法儲存一個Spitter物件,通過getOne
方法來根據id獲取一個Spitter物件等,但是對於實現類是如何實現的,程式並不知道也不關心,這才給了我們機會使用虛擬的資料。
貼出之前SpittleRepository實現類的程式碼:
@Component
public class SpittleRepositoryImpl implements SpittleRepository{
private List<Spittle> spittles = Arrays.asList(
new Spittle(1l,"First Spittle!!!!", new Date(), null, null),
new Spittle(2l,"Another Spittle!!!", new Date(), null, null),
new Spittle(3l,"Spittle!! Spittle!! Spittle!!", new Date(), null, null),
new Spittle(4l,"Spittles go forth!!", new Date(), null, null)
);
@Override
public List<Spittle> findSpittles(long max, int count) {
return spittles;
}
@Override
public Spittle findOne(long id) {
return spittles.stream().filter(s->s.getId().equals(id)).findFirst().get();
}
}
很簡單,在這個類中虛擬了四條資料,並且findSpittles
方法直接簡單粗暴的返回了全部四條資料,忽略了引數。findOne
方法在虛擬的四條資料中進行過濾篩選,返回匹配的資料。
介面讓我們在還沒有學習,沒有將資料庫系統接入到程式中之前,不影響其他功能的開發。這便是提供Repository
介面的好處。
使用jdbcTemplate實現Repository介面
用於開發的嵌入式資料庫和用於生產的mysql資料庫都已經接入到程式中,沒理由再使用這些虛擬的資料。
下面是使用jdbcTemplate來實現的SpittleRepository
,這其中有一些需要注意的點
@Repository
@Primary
@DependsOn("jdbcTemplate")
public class JdbcSpittleRepository implements SpittleRepository{
JdbcOperations jdbcOperations;
SpittleRowMapper mapper;
@Autowired
public JdbcSpittleRepository(JdbcOperations operations) {
this.jdbcOperations = operations;
this.mapper = new SpittleRowMapper();
}
@Override
public List<Spittle> findSpittles(long max, int count) {
return jdbcOperations.query(
"SELECT * FROM spittle WHERE id <= ? LIMIT 0, ?",
mapper,
max, count
);
}
@Override
public Spittle findOne(long id) {
return jdbcOperations.queryForObject(
"SELECT * FROM spittle WHERE id=?",
mapper,
id
);
}
}
@Repository
註解讓該類作為Bean被Spring掃描到,其實它就是一個@Component
@Primary
註解告訴Spring如果遇到多個SpittleRepository
實現類,使用這個,這樣我們就不需要刪除之前的實現類以防以後還會使用到@DependsOn
註解表明該Bean依賴名為jdbcTemplate
的Bean。預設情況下的載入順序好像是@Configuration
中的@Bean
晚於這些@Component
宣告的Bean被載入,所以該類被載入時jdbcTemplate
還沒被載入,就會出錯。但是我使用了@DependsOn
後有時也會出現該類先於jdbcTemplate
前被載入的情況。JdbcOperations
是一個介面,JdbcTemplate
是它的一個實現類,Spring處處都使用介面。這裡宣告成JdbcTemplate
也行SpittleRowMapper
是將SQL查詢的ResultSet轉換成Spittle物件的一個對映類,後面會放出來,JdbcTemplate中使用這種對映類進行處理結果集,和DBUtils差不多。
看這個方法,jdbcOperations.queryForObject
是查詢並返回一個物件的,第一個引數是SQL,第二個引數就是mapper,第三個引數是可變長引數,是SQL中的引數
@Override
public Spittle findOne(long id) {
return jdbcOperations.queryForObject(
"SELECT * FROM spittle WHERE id=?",
mapper,
id
);
}
比較坑的是,該方法有這麼一個過載方法:
/**
* The query is expected to be a single row/single column query; the returned result will be directly mapped to the corresponding object type.
*/
queryForObject(String sql, Class<T> requireClass, Object...args);
這個single row/single column
,我以為是單行或者單列就能夠直接通過反射對映到對應的Pojo類上,結果是單行並且單列,這個方法可以用於Integer.class
、String.class
等等值的獲取,比如如下獲取總spittle數量
queryForObject("SELECT count(*) FROM spittle", Integer.class);
RowMapper
上面我們定義了一個SpittleRowMapper
將結果集轉換成了Spittle
物件。
public class SpittleRowMapper implements RowMapper<Spittle> {
@Override
public Spittle mapRow(ResultSet resultSet, int i) throws SQLException {
Date date = new Date(resultSet.getLong("createTime"));
Long id = resultSet.getLong("id");
String message = resultSet.getString("message");
Double latitude = resultSet.getDouble("latitude");
Double longtitude = resultSet.getDouble("longtitude");
Spittle spittle = new Spittle(id,message,date,latitude,longtitude);
return spittle;
}
}
下面我們來解釋下,定義的RowMapper必須實現RowMapper
介面並指定泛型,重寫mapRow
方法將結果集中的一行對映成物件。換行的操作也是屬於模板程式碼,jdbcTemplate也幫我們做了。
下面是mapRow方法,由於資料庫中的createTime
欄位和Spittle中的資料型別不一致,Spittle
中是java.sql.Date
,而資料庫中儲存的是unix時間戳,所以這裡轉換了一下,其它的沒啥可說的。
BeanPropertyRowMapper
如果資料庫和Pojo類的欄位都能對上,能夠直接通過反射對映過去,那麼可以不用自己定義RowMapper,使用BeanPropertyRowMapper
下面的SpitterRepository
使用BeanPropertyRowMapper
。
@Repository
@Primary
@DependsOn("jdbcTemplate")
public class JdbcSpitterRepository implements SpitterRepository{
JdbcOperations jdbcOperations;
BeanPropertyRowMapper<Spitter> mapper;
@Autowired
public JdbcSpitterRepository(JdbcOperations jdbcOperations) {
this.jdbcOperations = jdbcOperations;
this.mapper = new BeanPropertyRowMapper<Spitter>(Spitter.class);
}
@Override
public int save(Spitter spitter) {
if (spitter.getId()!=null) {
return jdbcOperations.update(
"UPDATE spitter SET firstName=?, lastName=?, userName=?, password=? where id=?",
spitter.getFirstName(),spitter.getLastName(),spitter.getUserName(),spitter.getPassword(),spitter.getId()
);
}else{
return jdbcOperations.update(
"INSERT INTO spitter (firstName, lastName, userName, password) VALUES (?,?,?,?)",
spitter.getFirstName(),spitter.getLastName(),spitter.getUserName(),spitter.getPassword()
);
}
}
@Override
public Spitter getOne(Long id) {
return jdbcOperations.queryForObject(
"SELECT * FROM spitter WHERE id=?", mapper, id
);
}
@Override
public Spitter getOneByUserName(String username) {
return jdbcOperations.queryForObject(
"SELECT * FROM spitter WHERE userName=?",
mapper,
username
);
}
}