Spring Boot 2.0 讀書筆記_07:Spring JDBC Template
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物件的成員屬性對應。
-
-