在我們日常的開發過程中,肯定不可避免的會使用到資料庫以及 SQL 語句。比如,剛開始學習 Java 的時候可能會遇到 JDBC,它是連線 Java 和資料庫的橋樑,我們可以使用 JDBC 來建立與資料庫之間的連線並且執行相應的 SQL 語句。雖然 JDBC 的執行效率很高,但是其開發效率比較低。正是如此,市面上出現了一大批 ORM(物件關係對映)框架,例如 Hibernate,iBATIS 以及本文將要介紹的 MyBatis。
MyBatis 簡介
MyBatis 是一款優秀的持久層框架,它支援定製化 SQL、儲存過程以及高階對映。它避免了幾乎所有的 JDBC 程式碼和手動設定引數以及獲取結果集。因為 MyBatis 可以使用簡單的 XML 或註解來配置和對映原生資訊,將介面和 Java 的 POJOs (Plain Old Java Objects,普通的 Java 物件)對映成資料庫中的記錄。
通俗地說,MyBatis 就是我們使用 Java 程式操作資料庫時的一種工具,可以簡化我們使用 JDBC 時的很多操作,而且還簡化了資料庫記錄與 POJO 之間的對映方式。
為什麼要使用 MyBatis
前文提到過,目前市面上 有很多的 ORM 框架,例如 Hibernate,iBATIS 以及 Spring 全家桶的 Spring Data JPA。那麼我們為什麼要使用 MyBatis 呢?因為使用 MyBatis 有以下優勢:
可以簡化我們操作資料庫的步驟。
相對 Hibernate 來說學習成本較低,Hibernate 還需要學習其 HQL 查詢規範。
使用相對廣泛。
準備工作
本文將使用開源的資料庫連線池 DBCP(DataBase Connection Pool)連線 MySQL 資料庫,並在此基礎上整合 MyBatis 以及 MyBatis Plus,進而講解如何使用 MyBatis 和 MyBatis Plus 操作資料庫。所以在開始本教程的閱讀之前,需要如下準備:
一個 Spring Boot 的 Web 專案,你可以透過 Spring Initializr 頁面生成一個空的 Spring Boot 專案,當然也可以下載 springboot-pom.xml 檔案,然後使用 Maven 構建一個 Spring Boot 專案。
安裝 MySQL 資料庫或者一臺 MySQL 伺服器。
使用 DBCP 連線池
DBCP 資料庫連線池是 Apache 上的一個 Java 連線池專案,也是 Tomcat 使用的連線池元件。由於建立資料庫連線是一種非常耗時、耗資源的行為,所以透過連線池預先同資料庫建立一些連線,放在記憶體中,應用程式需要建立資料庫連線時直接到連線池中申請一個就行,使用完畢後再歸還到連線池中。
新增依賴
這一步很簡單,只需要在 pom.xml 中新增清單 1 的內容即可。
清單 1. 新增相關依賴
新增好依賴後,我們需要做的就是配置我們的資料來源。首先我們需要在配置檔案中新增資料來源相關的配置項的值,下面清單程式碼只給出了部分項,完整內容可以檢視本文原始碼:
清單 2. 資料來源配置檔案配置項
# 基本屬性spring:datasource:dbcp2:url: jdbc:mysql://localhost:3306/spring_tutorial?serverTimezone=GMT%2B8amp;characterEncoding=utf-8username: rootpassword: 123456driver-class-name: com.mysql.jdbc.Driver
新增好配置項後,我們在 cn.itweknow.sb-mybatis.config 包下新建了 DataSourceConfiguration 類,它是資料來源的配置類,其內容如下,可以看到在這個類裡面我們配置了資料來源。
清單 3. 資料來源配置
@Configuration@ConditionalOnProperty(name = "spring.datasource.dbcp2.url", matchIfMissing = false)@MapperScan(value = { "cn.itweknow.sbmybatis.mapper" }, sqlSessionFactoryRef = "sqlSessionFactory")public class DataSourceConfiguration {@Bean(name = "dataSource")@ConfigurationProperties(prefix = "spring.datasource.dbcp2")public DataSource dataSource() {return new BasicDataSource();}}
到這一步,如果我們能夠正常啟動專案就意味著我們的連線池配置成功了。
整合 MyBatis
下面我們來了解如何在 Spring Boot 專案中配置 MyBatis。只需要三大步驟就可以完成這個配置。第一步是新增依賴包,第二步是配置資料庫事務和會話工廠,最後一步就是配置 Mapper 的路徑。
新增 MyBatis 相關依賴包
我們只需要在 pom.xml 檔案的 <dependencies> 標籤下新增如下內容即可。
清單 4. 新增 MyBatis 依賴包
清單 5. 配置資料庫事務和會話工廠
@Bean(name = "transactionManager")public DataSourceTransactionManager dbOneTransactionManager(@Qualifier("dataSource") DataSource dataSource) {return new DataSourceTransactionManager(dataSource);}@Bean(name = "sqlSessionFactory")@ConditionalOnMissingBean(name = "sqlSessionFactory")public SqlSessionFactory dbOneSqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) throws Exception {final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();sessionFactory.setDataSource(dataSource);return sessionFactory.getObject();}
在 DataSourceConfiguration 類中新增上面的程式碼塊,我們就配置了一個事務管理器和一個會話工廠。
配置 Mapper 路徑
首先指定 *Mapper.java 的掃描路徑(即存放 *Mapper.java 的包地址)。我們可以透過在 DataSourceConfiguration 類或者 SbMybatisApplication 類上新增 @MapperScan 註解來指定掃描路徑:
清單 6. 配置 *Mapper.java 掃描路徑
@MapperScan(value = { "cn.itweknow.sbaop.db.mapper" }, sqlSessionFactoryRef = "sqlSessionFactory")public class DataSourceConfiguration {…}
透過會話工廠指定指定 *Mapper.xml 的路徑。修改 SqlSessionFactory 的 Bean 建立方法如下所示:
清單 7. 配置 *Mapper.xml 掃描路徑
@Bean(name = "sqlSessionFactory")@ConditionalOnMissingBean(name = "sqlSessionFactory")public SqlSessionFactory dbOneSqlSessionFactory(@Qualifier("dataSource") DataSource dataSource)throws Exception {final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();sessionFactory.setDataSource(dataSource);sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:sqlmap/*Mapper.xml"));return sessionFactory.getObject();}
至此,我們的 MyBatis 和 Spring Boot 就整合完成了。下面的章節將介紹具體如何使用 MyBatis 了。MyBatis 一共提供了兩種使用方式,一種是基於 XML 配置 的方式,一種是基於註解的方式。我們將分別從這兩個方面來了解如何使用 MyBatis。
基於 XML 方式使用 MyBatis
準備工作
我們將透過一個具體的場景來講解如何使用 MyBatis 進行資料庫操作:定義一個學生實體和班級實體,並建立對應的資料庫表,然後使用 MyBatis 對其進行增、刪、改、查操作。學生類的全路徑為 cn.itweknow.sbmybatis.model.dao.Student,班級類的全路徑為 cn.itweknow.sbmybatis.model.dao.Clazz,相對應的資料庫表結構點選這裡獲取。
建立 Mapper 類及 XML 檔案
建立好對應的實體類後,我們還需要為其建立對應的 Mapper.java 和 Mapper.xml 檔案,需要注意的是這兩類檔案需要放到前面配置資料來源時指定的路徑下,在本例中 Mapper.java 檔案是放在 cn.itweknow.sbmybatis.mapper 包下,而 Mapper.xml 則是放在 resources/sqlmap 目錄下。
建立 StudentMapper.java 檔案
清單 8. StudentMapper.java 類
public interface StudentMapper {int insert(Student student);int updateIgnoreNullById(@Param("student") Student student);Student selectById(@Param("id") int id);int deleteById(@Param("id") int id);}
在 StudentMapper.java 類中我們定義了新增,更新(忽略空值),根據 id 查詢學生,根據 id 刪除學生四個方法,覆蓋了簡單的 CURD (Create、Update、Retrieve、Deletev操作,但是這四個只是方法的定義,那麼具體實現呢?與我們平常所接觸到的 interface 不一樣的是,它的實現不是一個具體的 Java 類,而是一個與之對應的 mapper.xml檔案,也就是我們接下來要看到的 StudentMapper.xml 檔案。StudentMapper.java 類中定義的介面會對應 StudentMapper.xml 中的一段 SQL 語句。
建立 StudentMapper.xml 檔案
建立一個與 StudentMapper.java 類對應的 XML 檔案,裡面定義了 StudentMapper.java 類中定義的 4 個方法的 SQL 實現,具體程式碼如下所示,篇幅的原因,完整的 XML 內容可以檢視本文原始碼。
清單 9. StudentMapper.xml 檔案
<mapper namespace="cn.itweknow.sbmybatis.mapper.StudentMapper"><resultMap id="BaseResultMap" type="cn.itweknow.sbmybatis.model.dao.Student"><id column="id" jdbcType="INTEGER" property="id" /><result column="name" jdbcType="VARCHAR" property="name" />...</resultMap><insert id="insert" parameterType="cn.itweknow.sbmybatis.model.dao.Student" useGeneratedKeys="true" keyProperty="id">insert into t_student(name,age,clazz_id,number) values(#{name},#{age},#{clazzId},#{number})</insert><update id="updateIgnoreNullById">update t_student<set><if test="student.name != null">name = #{student.name}</if>...</set>where id = #{student.id}</update><select id="selectById" resultMap="BaseResultMap">select * from t_student where id = #{id}</select> ...</mapper>
其中 namespace 指定了該 XML 檔案對應的 java 類。您可能還發現除了四個方法的定義外,還有一個 resultMap 的標籤,這個其實定義的是我們 sql 查詢的欄位與實體類之間的對映關係。在 insert 方法中,我們使用了 useGeneratedKeys 和 keyProperty 兩個屬性,這兩個屬性的作用主要是將插入後資料的 id,賦值到傳進來的實體物件的某個欄位,keyProperty 就是指定那個欄位的名稱。
建立 ClazzMapper.java 檔案
在 StudentMapper.java 中我們定義了簡單的增刪改查,在下面的程式碼中我們定義了一個需要多表關聯才能實現的方法 ,selectWithStudentsById 方法會根據班級 id 查詢出來該班級的資訊以及該班級裡的所有學生資訊,我們定義了一個 ClazzExtend 類來接收查詢結果,ClazzExtend 類擴充套件了 Clazz,它包含了一個學生集合屬性,用來描述班級裡的所有學生。
清單 10. ClazzMapper.java 類
public interface ClazzMapper {ClazzExtend selectWithStudentsById(int id);}
建立 ClazzMapper.xml 檔案
清單 11. ClazzMapper.xml 檔案
<mapper namespace="cn.itweknow.sbmybatis.mapper.ClazzMapper"><resultMap id="ClazzExtendMap" type="cn.itweknow.sbmybatis.model.dao.ClazzExtend"><id column="id" jdbcType="INTEGER" property="id" /><result column="name" jdbcType="VARCHAR" property="name" /><collection property="students" ofType="cn.itweknow.sbmybatis.model.dao.Student"><result column="sId" property="id"/><result column="sName" property="name"/><result column="sAge" property="age"/><result column="sClazzId" property="clazzId"/><result column="sNumber" property="number"/></collection></resultMap><select id="selectWithStudentsById" resultMap="ClazzExtendMap">selectc.id as id,c.name as name,s.id as sId,s.name as sName,s.age as sAge,s.clazz_id as sClazzId,s.number as sNumberfrom t_clazz as cleft join t_student as s on s.clazz_id = c.idwhere c.id = #{id}</select></mapper>
注意 resultMap 和 StudentMapper.xml 中的 resultMap 有些不一樣,多了一個 collection 標籤,這個標籤是用來描述實體類中的集合屬性的,MyBatis 會自動將我們 left join 後產生的多條資料合成一個數組並返回,是不是很方便?
測試程式碼
這裡舉例 ClazzMapper. selectWithStudentsById 方法的查詢測試,其他方法的測試程式碼類似這裡就不列舉了,您可以嘗試一下自己編寫。
清單 12.測試方法程式碼
@RunWith(SpringRunner.class)@SpringBootTestpublic class XmlTest {@Autowiredprivate ClazzMapper clazzMapper;@Testpublic void testSelectWithStudentsById() {ClazzExtend result = clazzMapper.selectWithStudentsById(1);System.out.println(JSON.toJSONString(result));}}
執行以上單元測試後,會發現在控制檯輸出的結果中既包含班級資訊也包含班級下面的學生資訊,控制檯輸出的內容如下所示:
圖 1. Mapper 方法測試結果
基於註解方式使用 MyBatis
雖然 MyBatis 設計之初是一個 XML 驅動的 ORM 框架,其配置資訊都是基於 XML 的,但是從 MyBatis3 開始它基於強大的 Java 語言的配置 API 之上,支援使用註解來配置 SQL 以及查詢結果與實體之間的對映關係。那麼我們下面就來了解一下如何使用註解來使用 MyBatis。
注意本章節中所使用的表結構和例子與基於 XML 方式使用章節一致。話不多說,我們直接開始。Mapper.java 檔案還是需要放在資料來源指定的位置,我這裡就不重複建立 Mapper 檔案了,基於註解配置的程式碼也直接寫在上面建立的 Mapper 檔案中。
新增語句
清單 13. 基於註解的插入資料程式碼
@Insert("insert into t_student(name,age,clazz_id,number) values " +"(#{name},#{age},#{clazzId},#{number})")@Options(useGeneratedKeys=true, keyProperty="id", keyColumn="id")int annoInsert(Student student);
查詢語句
清單 14. 基於註解的查詢資料程式碼
@Select("select * from t_student where id = #{id}")Student annoSelectById(Integer id);
您可能會奇怪,這裡沒有定義資料庫欄位和實體類之間的對映關係,MyBatis 是如何將查詢結果轉換成物件的呢?其實 MyBatis 會預設以下劃線轉駝峰的方式建立一個對映關係來轉換。到這裡您可能會問了,在基於註解的方式下如果欄位名稱和實體類屬性名稱不符合這個對映規則的話又該如何呢?基於註解配置對映關係將會在多表關聯查詢小節中講解。
更新語句
清單 15. 基於註解的更新資料程式碼
@UpdateProvider(type = StudentDAOProvider.class, method = "updateIgnoreNullByPrimaryKey")int annoUpdateIgnoreNullById(@Param("student") Student student); int annoUpdateIgnoreNullById(@Param("student") Student student);
您應該會發現 Update 方法和前面有些不同,我們指定了一個 type 和一個 method 屬性,這是為什麼呢?因為我們在更新的時候需要去判斷該欄位是否為空來決定是否更新該欄位的內容,在 XML 的配置方式中我們可以透過 <if></if> 標籤實現。那麼透過註解呢?我們只能透過一個 Provider 類來動態生成 SQL 語句,下面的內容就是 StudentDAOProvider.class 的內容:
清單 16. StudentDAOProvider 的內容
public class StudentDAOProvider {public String updateIgnoreNullByPrimaryKey(Map<String, Object> map) throws Exception {Student student = (Student) map.get("student");if (student == null || student.getId() == null) {throw new Exception("the primaryKey can not be null.");}// 拼裝 sql 語句StringBuilder updateStrSb = new StringBuilder("update t_student set ");StringBuilder setStrSb = new StringBuilder();if (student.getName() != null) {setStrSb.append("name = #{student.name},");}if (student.getNumber() != null) {setStrSb.append("number = #{student.number},");}if (student.getAge() != null) {setStrSb.append("age = #{student.age},");}if (student.getClazzId() != null) {setStrSb.append("clazz_id = #{student.clazzId},");}if (setStrSb.length() > 0) {updateStrSb.append(setStrSb.substring(0, setStrSb.length()-1)).append(" where id = #{student.id}");} else {throw new Exception("none element to update.");}return updateStrSb.toString();}}
刪除語句
@Delete("delete from t_student where id = #{id}")int annoDeleteById(Integer id);
多表關聯查詢
多表關聯查詢的例子和基於 XML 的例子相同,我們也是根據班級 id 查詢班級資訊以及該班級下所有的學生資訊。基於註解的一對多的關聯查詢方式會比較繁瑣,下面是具體的實現步驟。
首先在 StudentMapper.java 中定義一個根據班級 id 查詢學生集合的方法。
清單 18. 根據班級 id 查詢學生的程式碼
@Select("select * from t_student where clazz_id = #{clazzId}")List<Student> findByClazzId(Integer clazzId);
然後再在 ClazzMapper.java 中定義複雜查詢的方法,如下所示:
清單 19. 多表關聯查詢的程式碼
@Select("select * from t_clazz where id = #{id}")@Results(id = "link", value = {@Result(column = "id", property = "id"),@Result(column = "name", property = "name"),@Result(property = "students", column = "id", javaType = List.class,many = @Many(fetchType = FetchType.EAGER, select = "cn.itweknow.sbmybatis.mapper.StudentMapper.findByClazzId"))})ClazzExtend annoSelectWithStudentsById(Integer id);
可以看到我們在基於註解的方式下可以透過 @Result 來制定查詢結果與實體之間的對映關係。你可能還注意到查詢班級中所有學生的屬性實際上是指定了 StudentMapper 中的 findByClazzId 的方法來實現的,many=@Many 是指的這是一個一對多的查詢,如果是一對一的查詢的話可以使用 one=@One。select 則指定了這個一對多查詢的具體方法,可能看到這裡您會有一個疑問:這個複雜查詢時使用的關聯表查詢還是分成了兩個查詢語句查詢來得到結果的呢?那我們就一起來看一下吧:我們修改一下日誌級別為 debug,這樣可以將 MyBatis 實際執行的 SQL 語句打印出來,如下圖所示。
圖 2. 基於註解複雜查詢的 SQL 列印
很明顯可以看出來,是分成了兩個 SQL 來查詢的,這樣如果關聯表很多的話可能會導致查詢效率比較低,可見覆雜 SQL 的查詢還是 XML 的方式好使一些。
XML 與註解的優劣對比
上面分別介紹了基於 XML 和註解兩種方式來使用 MyBatis,那麼到底使用哪種方式好呢?其實這個也需要看實際使用場景,比如說註解適合使用在一些簡單查詢的場景,而 XML 則在複雜查詢的時候表現更佳。下面分別列舉一下使用 XML 和註解的優劣之處供大家參考。
XML 的優劣點
優點:
排版能力強,特別是複雜 SQL 的排版,看起來更加清晰明瞭。
動態 SQL,在編寫動態 SQL 的時候不得不說 XML 確實比註解要強,註解還需要單獨定義一個 Provider 類來提供生成的 SQL。
缺點:
需要將 Mapper.xml 與 Mapper.java 類準確對應,容易出錯。
查詢一個介面對應的 SQL 語句不方便,還需要先找到對應的 XML 檔案。
註解的優劣點
優點:
SQL 查詢方便,SQL 直接放在介面上方的註解上,可以很容易找到。
缺點:
SQL 排版效果不好,複雜的 SQL 很難看明白其查詢邏輯。
對多表關聯查詢的支援度不好。
對動態 SQL 的支援不好,還需要單獨提供生成 SQL 的 Java 方法。
多表關聯查詢時,實際上是分成了多條 SQL 查詢,如果關聯比較多時可能會造成查詢的效率比較低。
MyBatis 分頁查詢
在實際的開發過程中,我們可能會經常遇到分頁查詢的場景。如果直接用 SQL 語句的話,MySQL 可以透過 Limit 實現,MSSQL 可以透過 TOP 或者 row_number() 實現。那麼 MyBatis 有沒有對分頁做支援呢?沒有。我們可以透過 PageHelper 這個外掛來實現這個需求。
新增依賴
首先新增 PagerHelper 相關的依賴包,在 pom.xml 中新增如下內容即可:
清單 20. 新增 PageHelper 依賴
<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper</artifactId><version>5.1.10</version></dependency>
使用方式
首先在 Spring Boot 的配置檔案中新增一些配置項:
清單 21. PageHelper 配置項
pagehelper:helperDialect: mysqlreasonable: true
雖然說不同的資料庫之間的 SQL 大體相同,但每種資料庫都會有自己的方言,比如說不同的資料實現分頁使用的關鍵字就不同,我們可以透過以配置 helperDialect 屬性來指定分頁外掛使用哪種方言。Reasonable 是配置是否使用分頁合理化引數,預設值為 false。當該引數設定為 true 時,pageNum<=0 時會查詢第一頁,pageNum>pages(超過總數時),會查詢最後一頁。預設 false 時,直接根據引數進行查詢。
為了能夠體現分頁外掛的效果,在使用之前先往 t_student 表中插入 15 條資料,然後在 Mapper 層定義一個查詢所有學生的方法如下所示。
清單 22. 查詢所有學生
@Select("select *from t_student")List<Student> findAll();
注意為了方便貼程式碼,這個方法的實現我使用的是註解的方式,如果您使用 XML 的配置方式也是完全可以的。
然後透過如下的程式碼就可以成功的實現分頁查詢學生列表了。
清單 23. 分頁外掛的使用
@Testpublic void testSelectPage() {//獲取第 1 頁,10 條內容,預設查詢總數 countPageHelper.startPage(1, 10);//緊跟著的第一個 select 方法會被分頁List<Student> list = studentMapper.findAll();PageInfo page = new PageInfo(list);System.out.println(JSON.toJSONString(page));}
可以看到實現起來很方便,實際上 PageHelper 在呼叫 startPage 方法的時候會將分頁資訊儲存在 ThreadLocal 中,然後對緊跟在 startPage 之後的一個查詢語句應用分頁,PageHelper 會去修改你的查詢語句以達到分頁的效果。PageInfo 裡面包含了非常全面的分頁屬性如總資料條數、頁大小、當前頁資料等等。
圖 3. PageInfo 物件的分頁屬性
結束語
在本教程中,您瞭解瞭如何在 Spring Boot 專案中整合 MyBatis,更深一步介紹瞭如何透過 XML 和註解兩種方式使用 MyBatis 以及兩種方式的優缺點,最後還了解了如何透過 PageHelper 來實現分頁查詢。您可以在 GitHub 上找到本教程的完整實現,如果您想對本教程做補充的話歡迎發郵件([email protected])給我或者直接在 GitHub 上提交 Pull Request。