5. Spring JDBC Template
寫在開頭,JDBC Template 是 Spring 框架在JDBC基礎上做了一定的封裝。相比當下的DAO層框架,封裝度相對較低,很早之前用過幾次,由於SQL注入的Web攻擊場景,JDBC Template具有很好的防範。
關於SQL注入:JDBC Template中對參數化的SQL查詢有着良好的驗證機制,因此建議使用參數化SQL的方式,切勿採用SQL拼裝的方式。
JDBC Template 模板測試Demo
-
配置類:
DataSourceConfig.java
@Configuration public class DataSourceConfig { @Bean(name = "dataSource") public DataSource datasource(Environment env) { HikariDataSource ds = new HikariDataSource(); ds.setJdbcUrl(env.getProperty("spring.datasource.url")); ds.setUsername(env.getProperty("spring.datasource.username")); ds.setPassword(env.getProperty("spring.datasource.password")); ds.setDriverClassName(env.getProperty("spring.datasource.driver-class-name")); return ds; } }
Environment 類在 Spring Boot 中代表了環境上下文,包含了 application.properties 配置屬性、JVM系統屬性和操作系統環境變量。
這裏的數據庫連接池採用了:HikariCP
-
JDBC模板注入(Dao層)
@Repository public class UserDao ( @Autowired JdbcTemplate jdbcTempalte; )
-
基礎操作
-
查詢
關於查詢的返回結果,均採用包裝類型。比如,查詢count、sum等返回的數據,此外還可以將返回結果包裝爲POJO、List等。
例如:
返回 department_id 下 user 數目總和的查詢String sql = "select count(*) from user where department_id = ?"; Integer res = jdbcTeplate.queryForObject(sql, Integer.class, 1);
上述例子中含有參數綁定:department_id --> 1
返回POJO實例,JDBC Template需要一個RowMapper,將結果集ResultSet映射成POJO對象。
RowMapper 從字面意思上講 [行映射] ,可以針對業務層次去實現該接口,進行結果集元組向對象的映射配置。@FunctionalInterface public interface RowMapper<T> { @Nullable T mapRow(ResultSet rs, int rowNum) throws SQLException; }
在案例ch5中採用了內部靜態類的方式創建了 UserRowMapper:
static class UserRowMapper implements RowMapper<User> { public User mapRow(ResultSet rs, int rowNum) throws SQLException { User user = new User(); user.setId(rs.getInt("id")); user.setName(rs.getString("name")); user.setDepartmentId(rs.getInt("department_id")); return user; } }
可以看出,在UserRowMapper中,創建了User對象,並根據ResultSet結果集進行數據的獲取,通過User對象的setter方法進行對象屬性的填充。
結合開頭提到的SQL注入問題,做一個小測試:
final String sql_1 = "select * from user where id = ?"
–> User getUserById
final String sql_2 = "select * from user where department_id = ?"
–> List<User> getUserList1. Controller層中採用GET方式進行數據請求:
@GetMapping("/sql/test") @ResponseBody public Object getUserById(@RequestParam(value = "id") String id) { return userDao.getUserById(id); } @GetMapping("/sql/test1") @ResponseBody public List<User> getUserList(@RequestParam(value = "id") String d_id) { return userDao.getUserList(d_id); }
2. Dao層中注入JDBC Template對象,並分別採用query和queryForObject方法進行操作,由於操作的數據集合爲POJO(集合),所以這裏採用了上述的 UserRowMapper 對返回的Result結果進行封裝。
public User getUserById(String id) { String sql = "select * from user where id = ?"; return jdbcTempalte.queryForObject(sql, new UserRowMapper(), id); } public List<User> getUserList(String d_id) { String sql = "select * from user where department_id = ?"; return jdbcTempalte.query(sql, new UserRowMapper(), d_id); }
提到的參數化的SQL,就是將SQL的可變參數部分和SQL語句主幹區分開,通過方法的方式進行參數的配置。
在JDBC中並不提倡拼寫SQL的做法,相比之下更推薦PreparedStatement:Statement的子接口,可以傳入帶佔位符的SQL語句,提供了補充佔位符變量的方法,同時也可以對SQL注入語句進行規避處理,是不是和JDBC Template的方法參數簽名很像呢?3. 啓動項目,使用Postman進行分別測試,這裏將請求的參數進行處理,模擬SQL注入情景:id = value
or 1=1
1=1爲永真,邏輯表達式中or的成立規則爲:一真則真、都假才假。上述情況在id不存在或錯誤時仍可以實現where條件的正確性。類比登錄等場景,username、password在做驗證的時候被SQL注入後,也會出現上述類似場景,從而實現越過登錄驗證,進入操作頁面或系統內部。
數據現狀:
先進行queryForObject單個對象的查詢小測試:- 查詢數據庫中存在的數據
- 查詢數據庫中不存在的數據
控制檯輸出的錯誤爲:org.springframework.dao.EmptyResultDataAccessException: Incorrect result size: expected 1, actual 0
起初並沒有對這個異常很感興趣,後來進行query方法測試的時候,若返回數據爲空,則返回一個空數組[ ],於是對這個queryForObject產生了興趣。該異常的大體意思是空的結果集,後面還追加說明了結果集的大小,期望返回size=1,實際卻爲0。此外,在瀏覽器端的500錯誤也是客戶端用戶不想看到的,於是帶着問題Debug了一下,順便對返回結果進行異常捕獲,返回null。
query方法的測試,返回List<User>
補充:queryForMap方法可以返回map,使用Map作爲查詢結果有非常多的弊端,最嚴重的的弊端是Map本身不適合程序閱讀。通過Map瞭解ResultSet結果集難度較大,以及針對多種數據庫,數據庫字段類型,Map的弊端越顯嚴重。
- 查詢數據庫中存在的數據
-
修改
JDBC Template 提供 update 方法來實現SQL的修改語句,包括新增、修改、刪除、執行存儲過程等。
public void updateInfo(User user) { String sql = "update user set name=? and departmet_id=? where id = ?"; jdbcTempalte.update(sql, user.getName(), user.getDepartmentId(), user.getId()); }
數據庫記錄插入操作語句同上,對於MySQL、SqlServer等數據庫,含有自增的主鍵序列,此時需要提供一個
KeyHolder
來放置返回序列。
update方法簽名中需要傳入兩個參數:PreparedStatement對象,keyHolder對象。其中,keyHolder中包含了自增長字段的結果。但是此處的結果序列無法確定序列類型,需要根據具體業務轉換其序列類型。
-
NamedParameterJdbcTemplate (提供命名參數綁定的功能)
相比傳統的JDBCTemplate,用戶只能通過?佔位符
聲明參數,並使用索引
綁定參數,必須確保方法參數中的索引
和?佔位符
的位置匹配正確,纔可以使得方法執行無誤。NamedParameterJdbcTemplate模板了支持命名參數變量的SQL,同理在Dao層自動注入NamedParameterJdbcTemplate即可。
上述:department_id 下 user 數目總和的查詢 可以變更爲:
public Integer totalUserInDepartment2(Long departmentId) { String sql = "select count(1) from user where department_id = :deptId"; // SQL參數映射map:k-v形式存儲參數與值 MapSqlParameterSource namedParameters = new MapSqlParameterSource(); // key:SQL參數;value:方法接受參數 namedParameters.addValue("deptId", departmentId); // 執行方法,將上述參數映射map對象傳入即可 Integer count = namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class); return count; }
總結上述代碼:
-
在SQL語句中,使用
:paramName
形式替代? 佔位符
-
MapSqlParameterSource,SQL參數映射map對象,以 k-v 形式存儲參數與值
-
Spring 提供 SQLParameterSource 類來封裝任意的 JavaBean,爲 NamedParameterJdbcTemplate 提供參數。
public void updateInfoByNamedJdbc(User user) { String sql = "update user set name = :name and departmet_id = :departmentId where id = :id"; SqlParameterSource source = new BeanPropertySqlParameterSource(user); namedParameterJdbcTemplate.update(sql, source); }
這裏需要注意:sql語句中的參數:name、departmentId、id 需要與 user對象的成員屬性對應。
-
-