1. 程式人生 > >Spring Boot 2.0 讀書筆記_07:Spring JDBC Template

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

  1. 配置類: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

  2. JDBC模板注入(Dao層)

     @Repository
     public class UserDao (
    
       @Autowired
       JdbcTemplate jdbcTempalte;
     )
    
  3. 基礎操作

    • 查詢

      關於查詢的返回結果,均採用包裝型別。比如,查詢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> getUserList

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

      總結上述程式碼:

      1. 在SQL語句中,使用:paramName 形式替代 ? 佔位符

      2. MapSqlParameterSource,SQL引數對映map物件,以 k-v 形式儲存引數與值

      3. 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物件的成員屬性對應。