MyBatis #{}和${} |與數據庫連接池使用詳解

前言
前面我們學習了 MyBatis 的基礎操作,其中參數的傳遞使用到了#{},而參數的傳遞不僅僅只有這一個可以使用,還有 ${} 可以使用,那么今天這篇文章我將為大家說說關于 #{} 和 ${},這個是 MyBatis 在面試中最常問的面試題,以及數據庫連接池相關的知識。
#{}和${}的使用
首先我們還是需要在 YAML 配置文件中配置數據庫的相關配置。
# 配置數據庫相關信息
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mybatis_test?characterEncoding=utf8&useSSL=false
username: root
password: lmh041105666
# 打印mybatis日志
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImplUserInfoMapper文件中的代碼
@Mapper
public interface UserInfoMapper {
//這里是使用#{}作為參數的傳遞
@Select("select * from userinfo where id=#{id}")
public UserInfo selectById(Integer id);
}單元測試中代碼
@Slf4j
@SpringBootTest
class UserInfoMapperTest {
@Autowired
private UserInfoMapper userInfoMapper;
@Test
void selectById() {
log.info(userInfoMapper.selectById(3).toString());
}
}
然后我們講這里的 #{} 改為 ${} 看看有什么效果。
@Select("select * from userinfo where id=${id}")
public UserInfo selectById(Integer id);
這里可以看到 #{} 和 ${} 的一個區(qū)別:使用 #{} 進行參數的傳遞的時候,不是直接將輸入的參數拼接到后面,而是先使用 ? 進行占位,這種 SQL 我們稱之為“預編譯SQL”。而使用 ${} 進行參數傳遞的時候,會將輸入的參數直接拼接到 SQL 的后面,這種 SQL 我們稱之為“即時SQL”。這兩個 SQL 的區(qū)別我們后面再說。
上面我們傳入的參數都是 Integer 類型的參數,那么如果我們傳入的參數類型是 String 的話,會發(fā)生什么呢?
@Select("select * from userinfo where username=#{username}")
public UserInfo selectByName(String username);@Test
void selectByName() {
log.info(userInfoMapper.selectByName("小美").toString());
}
可以看到,使用 #{} 進行參數的傳遞的時候,會對傳遞的參數的類型進行識別,我們傳入的類型是 String 類型的話,就會識別為 String 類型,我們再來看看使用 ${} 傳遞參數,參數的類型為 String 類型的情況。
@Select("select * from userinfo where username=${username}")
public UserInfo selectByName(String username);
通過觀察 MyBatis 打印出來的日志可以發(fā)現,傳遞參數的時候,直接將我們傳遞的參數拼接到了 SQL 的末尾,而且沒有加上引號,那么在 SQL 中沒有加引號的字符串會被認為是表中的列,而我們當前表中沒有 小美 這個列,所以就會報錯,那么我們如何解決這個問題呢?
因為使用 ${} 進行參數的傳遞的時候是直接將傳遞的參數拼接到 SQL 的對應位置,所以我們可以手動在 SQL 中加入引號。
@Select("select * from userinfo where username='${username}'")
public UserInfo selectByName(String username);
通過上面的兩個例子,我們可以發(fā)現:#{} 使用的是預編譯 SQL,通過 ? 占位的方式,提前對 SQL 進行編譯,然后把參數填充到 SQL 語句中,并且 #{} 會根據參數類型,自動拼接 ‘’。而 ${} 則會直接進行字符的替換,一起對 SQL 進行編譯,如果參數為字符串類型,需要我們手動在 SQL 中加入引號。
#{}和${}的區(qū)別
當別人問我們 #{} 和 ${} 的區(qū)別的時候,實際上是在問預編譯 SQL 和即時 SQL 的區(qū)別。
當一個客戶發(fā)送一條 SQL 給服務器之后,大致流程是這樣的:
- 解析語法和語義,校驗 SQL 語句是否正確
- 優(yōu)化 SQL 語句,制定執(zhí)行計劃
- 執(zhí)行并返回結果
一個 SQL 如果執(zhí)行流程是這樣的話,那么這個 SQL 就被成為 即時SQL。
但是呢,絕大多數情況下,某一條 SQL 語句可能會被反復調用執(zhí)行,或者每次執(zhí)行的時候只有個別的值不同(比如 selelct 的 where 子句值不同,update 的 set 子句不同,insert 的 values 值不同)。如果我們每次調用這種 SQL 都按照上面的語法解析、SQL 優(yōu)化、SQL 編譯等,那么效率就會降低了。
類似這種 SQL,我們就可以使用預編譯 SQL 的方法來提升效率。

預編譯SQL是一種常見的數據庫優(yōu)化技術,其主要目的是提高數據庫查詢的效率和安全性。在預編譯SQL中,SQL語句和參數被分開處理。首先,SQL語句被編譯成一個中間形式,這個中間形式通常是與數據庫管理系統(tǒng)(DBMS)的內部表示相對應的。然后,參數值與編譯后的SQL語句進行動態(tài)綁定,最終生成可以直接執(zhí)行的SQL語句。后面再傳入參數執(zhí)行這條語句的時候(只是輸入的參數不同),省去了解析優(yōu)化的過程,一次來提高效率。
預編譯 SQL 的優(yōu)點不止是可以提高效率,還可以防止 SQL 注入。
什么是 SQL 注入?
SQL注入是一種常見的網絡攻擊方式,發(fā)生在Web應用程序中。它利用了Web應用程序對用戶輸入數據合法性沒有進行充分驗證或過濾不嚴的漏洞。攻擊者可以在Web應用程序中事先定義好的查詢語句的結尾上添加額外的SQL語句,從而實現在管理員不知情的情況下執(zhí)行非法操作。
當用戶在Web頁面上輸入數據時,如果這些數據未經過適當的驗證和處理,攻擊者可以在其中插入或“注入”惡意的SQL代碼。當這些數據被用來構建SQL查詢時,這些惡意的SQL代碼就會被執(zhí)行。攻擊者可以執(zhí)行非授權的任意查詢,從而進一步得到相應的數據信息。
例如,假設一個Web應用程序的登錄頁面允許用戶輸入用戶名和密碼。如果應用程序沒有對用戶輸入進行適當的驗證,攻擊者可以在用戶名或密碼字段中輸入如’ OR ‘1’='1之類的SQL代碼。當應用程序將這些數據用于構建SQL查詢時,這些代碼會使得查詢變成SELECT * FROM users WHERE username='' OR '1'='1' AND password='password',從而繞過正常的驗證,獲得所有用戶的訪問權限。
因為 ${} 傳遞參數的方式也就是即時 SQL 不會對用戶的輸入進行充分檢查,而 SQL 又是直接拼接而成的,在用戶輸入參數的時候,在參數中添加一些 SQL 關鍵字,達到改變 SQL 運行結果的目的,從而實現惡意攻擊。
給大家舉個例子:
@Select("select * from userinfo where username='${username}'")
public List<UserInfo> selectByName(String username);@Test
void selectByName() {
log.info(userInfoMapper.selectByName("'or 1='1").toString());
}通過這樣傳遞參數的話,那么將我們傳遞的參數直接拼接在 SQL 中,就會出現這樣的 SQL:select * from userinfo where username='' or 1='1',在這里 1 會被解釋為字符串 ‘1’,那么這時 ‘1’='1’為真,那么這時就會將數據庫中該表中的所有信息都給顯示出來,這樣就會導致數據的泄露。
通過觀察代碼的執(zhí)行結果,我們也可以發(fā)現,表中的所有信息都被顯示出來了。

而如果我們使用 #{} 來實現 SQL 的注入,看看可以達到效果嗎?
@Select("select * from userinfo where username=#{username}")
public List<UserInfo> selectByName(String username);@Test
void selectByName() {
log.info(userInfoMapper.selectByName("'or 1='1").toString());
}
通過預編譯 SQL 占位的方式,我們輸入的參數會被認為是表中的 username 列中的一個名稱,而不會被解析成為上面的 SQL,表中沒有一個叫“or 1='1"的username,所以查詢結果為0,也就是說,通過預編譯 SQL 的方式可以有效的解決 SQL 注入的情況。
所以為了防止 SQL 注入,我們應盡量選擇 #{} 來使用,那么是否就代表著 ${} 就不被使用了了呢?不是這樣的,既然出現了,那么它就有存在的意義。
排序功能
有一些情況下,只能使用 ${},而不能使用 #{}。其中很典型的情況就是涉及到排序的時候,根據我們輸入的參數決定是升序排列還是降序排列。
@Select("select * from userinfo order by id #{sort}")
public List<UserInfo> selectBySort(String sort);@Test
void selectBySort() {
log.info(userInfoMapper.selectBySort("desc").toString());
}

可以看到,本來我們進行排序查詢的話,排序規(guī)則是不需要加上引號的,而在預編譯占位的情況下,因為傳遞來的參數是字符串類型,所以在拼接 SQL 的時候,該參數就被加上了引號,所以就導致出現錯誤,在這種情況下我們就得使用 ${} 來進行 SQL 的直接拼接。
@Select("select * from userinfo order by id ${sort}")
public List<UserInfo> selectBySort(String sort);
所以當我們進行傳遞排序規(guī)則的時候,需要使用到 ${}。
模糊查詢
MySQL 中的模糊查詢使用 like 關鍵字,% 用來匹配任意任意數量的字符(包含0個),_ 用來匹配單個字符,那么如果我們想用參數傳遞的方式來指定模糊查詢的規(guī)則的話,就不能使用 #{} 的。
@Select("select * from userinfo where username like '%#{like}%'")
public List<UserInfo> selectByLike(String like);@Test
void selectByLike() {
log.info(userInfoMapper.selectByLike("小").toString());
}

同樣也是,使用 #{} 進行參數的傳遞的話,因為傳遞過來的參數的類型是字符串類型,所以在進行 SQL 拼接的時候就會自動加上引號也就是 ‘%‘小’%’,這樣的形成的 SQL,就是有問題的 SQL。
我們使用 ${} 的話就不會出現這種問題:
@Select("select * from userinfo where username like '%${like}%'")
public List<UserInfo> selectByLike(String like);
但是在這種情況下還是會發(fā)生 SQL 注入的情況,所以我們不推薦使用 ${},而是使用 SQL 內置的函數 concat()。
@Select("select * from userinfo where username like concat('%',#{like},'%')")
public List<UserInfo> selectByLike(String like);
總結 #{} 和 ${} 的區(qū)別
- 在使用 #{} 進行參數的傳遞的時候,會根據參數的類型,在拼接 SQL 的時候做出調整,而 ${} 則是直接進行 SQL 的拼接。
- 在排序,傳遞排序規(guī)則的時候不能使用 #{},因為 SQL 編譯的時候會根據傳遞的字符串類型,在 SQL 中加入引號,所以這種情況下只能使用 ${}。
- 進行模糊查詢的時候,雖然直接使用 #{} 會出現問題,使用 ${} 不會出現問題,但是我們不使用 ${},因為會發(fā)生 SQL 注入,而是使用 SQL 內置函數 concat()
- 最重要的就是預編譯 SQL 和即時 SQL 的區(qū)別:
- 預編譯 SQL 性能更高
- 預編譯 SQL 不存在 SQL 注入的問題
所以在實際開發(fā)的過程中,能使用 #{} 的情況就盡量使用 #{},如果要使用 ${},就一定要考慮到 SQL 注入的問題。
數據庫連接池
數據庫連接池是程序在啟動時創(chuàng)建足夠多的數據庫連接,并將這些連接放入一個池子中。這個池子中的連接可以被程序動態(tài)地申請、使用和釋放。其作用是分配、管理和釋放數據庫連接,使得應用程序可以重復使用現有的同一個數據庫連接,而不是每次都重新建立連接,從而避免了資源的消耗,提高了程序執(zhí)行的效率。

當沒有數據庫連接池的話,當客戶端向服務器發(fā)送 SQL 請求的話,會先創(chuàng)建一個 Connection 連接,然后通過這個連接訪問到服務器,當請求完成之后,這個連接又會被銷毀,下次再請求的時候,還是需要先創(chuàng)立連接,然后再釋放連接,這樣是比較消耗資源的。

當使用數據庫連接池的情況,當程序啟動的時候,就會在數據庫連接池中創(chuàng)建一定數量的 Connection 對象,當客戶端需要訪問服務器的時候,可以直接使用數據庫連接池中的 Connection 對象,并且當使用完成之后,會將 Connection 對象歸還給數據庫連接池,這樣就極大的節(jié)省了 CConnection 對象的創(chuàng)建和銷毀所需要的時間。
總的來說,使用數據庫連接池具有以下優(yōu)點:
- 減少網絡開銷
- 資源重用
- 提升系統(tǒng)的性能
常見的數據庫連接池有:C3P0、DBCP、Druid、Hikari,當前比較流行的是 Hikari和Druid。
而在我們的 SpringBoot 中,默認使用的數據庫連接池是 Hikari。

當我們啟動 MyBatis 的 SpringBoot 項目的時候,就會啟動 Hikari 數據庫連接池,建立 Connection 對象。
如果我們想要更換 SpringBoot 使用的數據庫連接池的話,只需要將要更換到的數據庫連接池的依賴添加到 pom.xml 文件即可。
這里假設我們更換 SpringBoot 的數據庫連接池為 Druid:
將下面這個坐標添加到 pom.xml 文件中。并且刷新 pom.xml 文件,將新添加的依賴下載下來。
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.17</version> </dependency>

到此這篇關于MyBatis #{}和${} | 數據庫連接池的文章就介紹到這了,更多相關MyBatis #{}和${}內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java中getParameterTypes()方法的使用與原理分析
本文詳細介紹了Java中getParameterTypes()方法的使用方式、工作原理及其在實際開發(fā)中的應用,該方法用于獲取方法的參數類型列表,并通過反射機制在運行時動態(tài)地獲取這些信息,感興趣的朋友跟隨小編一起看看吧2025-01-01
基于SpringBoot + Redis實現密碼暴力破解防護
在現代應用程序中,保護用戶密碼的安全性是至關重要的,密碼暴力破解是指通過嘗試多個密碼組合來非法獲取用戶賬戶的密碼,為了保護用戶密碼不被暴力破解,我們可以使用Spring Boot和Redis來實現一些防護措施,本文將介紹如何利用這些技術來防止密碼暴力破解攻擊2023-06-06
spring+springmvc+mybatis 開發(fā)JAVA單體應用
這篇文章主要介紹了spring+springmvc+mybatis 開發(fā)JAVA單體應用的相關知識,本文通過圖文實例代碼的形式給大家介紹的非常詳細 ,需要的朋友可以參考下2018-11-11

