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對象的成員屬性對應。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章