MyBatis #{}和${} |與數(shù)據(jù)庫連接池使用詳解

前言
前面我們學習了 MyBatis 的基礎(chǔ)操作,其中參數(shù)的傳遞使用到了#{},而參數(shù)的傳遞不僅僅只有這一個可以使用,還有 ${} 可以使用,那么今天這篇文章我將為大家說說關(guān)于 #{} 和 ${},這個是 MyBatis 在面試中最常問的面試題,以及數(shù)據(jù)庫連接池相關(guān)的知識。
#{}和${}的使用
首先我們還是需要在 YAML 配置文件中配置數(shù)據(jù)庫的相關(guān)配置。
# 配置數(shù)據(jù)庫相關(guān)信息
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 {
//這里是使用#{}作為參數(shù)的傳遞
@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ū)別:使用 #{} 進行參數(shù)的傳遞的時候,不是直接將輸入的參數(shù)拼接到后面,而是先使用 ? 進行占位,這種 SQL 我們稱之為“預(yù)編譯SQL”。而使用 ${} 進行參數(shù)傳遞的時候,會將輸入的參數(shù)直接拼接到 SQL 的后面,這種 SQL 我們稱之為“即時SQL”。這兩個 SQL 的區(qū)別我們后面再說。
上面我們傳入的參數(shù)都是 Integer 類型的參數(shù),那么如果我們傳入的參數(shù)類型是 String 的話,會發(fā)生什么呢?
@Select("select * from userinfo where username=#{username}")
public UserInfo selectByName(String username);@Test
void selectByName() {
log.info(userInfoMapper.selectByName("小美").toString());
}
可以看到,使用 #{} 進行參數(shù)的傳遞的時候,會對傳遞的參數(shù)的類型進行識別,我們傳入的類型是 String 類型的話,就會識別為 String 類型,我們再來看看使用 ${} 傳遞參數(shù),參數(shù)的類型為 String 類型的情況。
@Select("select * from userinfo where username=${username}")
public UserInfo selectByName(String username);
通過觀察 MyBatis 打印出來的日志可以發(fā)現(xiàn),傳遞參數(shù)的時候,直接將我們傳遞的參數(shù)拼接到了 SQL 的末尾,而且沒有加上引號,那么在 SQL 中沒有加引號的字符串會被認為是表中的列,而我們當前表中沒有 小美 這個列,所以就會報錯,那么我們?nèi)绾谓鉀Q這個問題呢?
因為使用 ${} 進行參數(shù)的傳遞的時候是直接將傳遞的參數(shù)拼接到 SQL 的對應(yīng)位置,所以我們可以手動在 SQL 中加入引號。
@Select("select * from userinfo where username='${username}'")
public UserInfo selectByName(String username);
通過上面的兩個例子,我們可以發(fā)現(xiàn):#{} 使用的是預(yù)編譯 SQL,通過 ? 占位的方式,提前對 SQL 進行編譯,然后把參數(shù)填充到 SQL 語句中,并且 #{} 會根據(jù)參數(shù)類型,自動拼接 ‘’。而 ${} 則會直接進行字符的替換,一起對 SQL 進行編譯,如果參數(shù)為字符串類型,需要我們手動在 SQL 中加入引號。
#{}和${}的區(qū)別
當別人問我們 #{} 和 ${} 的區(qū)別的時候,實際上是在問預(yù)編譯 SQL 和即時 SQL 的區(qū)別。
當一個客戶發(fā)送一條 SQL 給服務(wù)器之后,大致流程是這樣的:
- 解析語法和語義,校驗 SQL 語句是否正確
- 優(yōu)化 SQL 語句,制定執(zhí)行計劃
- 執(zhí)行并返回結(jié)果
一個 SQL 如果執(zhí)行流程是這樣的話,那么這個 SQL 就被成為 即時SQL。
但是呢,絕大多數(shù)情況下,某一條 SQL 語句可能會被反復(fù)調(diào)用執(zhí)行,或者每次執(zhí)行的時候只有個別的值不同(比如 selelct 的 where 子句值不同,update 的 set 子句不同,insert 的 values 值不同)。如果我們每次調(diào)用這種 SQL 都按照上面的語法解析、SQL 優(yōu)化、SQL 編譯等,那么效率就會降低了。
類似這種 SQL,我們就可以使用預(yù)編譯 SQL 的方法來提升效率。

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

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

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

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

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

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

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

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

