# MyBatis入門
[TOC]
## 導學
MyBatis是一個大名鼎鼎的ORM框架,對于我們進行數據庫開發有著非常優秀的支持。
首先我們要了解,**什么是框架**?
>[info]框架,即 framework。其實就是某種應用的半成品,就是一組組件,供你選用完成你自己的系統。簡單說就是使用別人搭好的舞臺,你來做表演。而且,框架一般是成熟的,不斷升級的軟件。
打個比方,Java 框架跟建筑中的框架式結構是一樣的。使用了框架(鋼筋+混凝土)以后,你所專著的只是業務(非承重墻構建不同格局),當然是在遵守框架的協議上開發業務。
**為什么要使用框架**?
因為軟件系統發展到今天已經很復雜了,特別是服務器端軟件,涉及到的知識,內容,問題太多。在某些方面使用別人成熟的框架,就相當于讓別人幫你完成一些基礎工作,你只需要集中精力完成系統的業務邏輯設計。而且框架一般是成熟,穩健的,你可以處理系統很多細節問題,比如,事物處理,安全性,數據流控制等問題。還有框架一般都經過很多人使用,所以結構很好,所以擴展性也很好,而且它是不斷升級的,你可以直接享受別人升級代碼帶來的好處。
比如,我們可以自己DIY一臺電腦,這就是因為我們可以使用一個現成的主板,在這個主板上有著許多規范的接口可供其他設備加入。

**軟件開發中的框架**
* 框架是可被應用開發者定制的應用骨架
* 框架是一種規則,保證開發者遵守相同的方式開發程序
* 框架提倡“不要重復造輪子”,對基礎功能進行封裝
**使用軟件框架的優點總結**
* 極大的提高了開發的效率
* 統一的編碼規則,利于團隊管理
* 靈活配置的應用,擁有更好的維護性
**SSM框架介紹**

>[success]1. Spring 對象容器框架,提供底層的對象管理,是框架的框架,其他的框架都要基于該框架進行開發。
>2. Spring MVC web開發框架,用于替代servlet,提供Web底層的交互,進行更有效的web開發。
>3. Mybatis 數據庫框架,用于簡化數據庫操作,對JDBC進行了封裝及擴展,提供了數據庫的增刪改查的便捷操作
>[info]補充介紹:SSH框架其實指的是Spring+Struts2+Hibernate框架,該框架更貼近于我們之前的Java Web學習內容,較為老舊,需要較多的配置文件,并不怎么方便。
>[warning]補充介紹:常用的數據庫框架其實還有MyBatis Plus和iBatis框架等。
## MyBatis框架介紹
* MyBatis是優秀的持久層框架 --將內存中的數據保存在數據庫中
* MyBatis使用XML將SQL與程序解耦,便于維護
* MyBatis學習簡單,執行高效,是JDBC的延伸
>[info]對象的兩種狀態:
>1. 瞬時狀態:程序中運行的對象,對象保存在內存中,當程序中斷或者結束(計算機關閉或重啟),該狀態對象不會保留。
>2. 持久化狀態:把對象數據保留在文件中,文件存儲在永久的存儲介質里(光盤、硬盤),當程序中斷或者計算機重啟斷電,該狀態的對象會永久保留。
>所謂的持久化就是把瞬時狀態的對象轉換為持久化狀態的對象。
**MyBatis開發流程-非xml形式**
* 引入MyBatis依賴
* 創建核心配置文件
* 創建實體(Entity)類
* 創建Mapper映射文件
* 初始化SessionFactory
* 利用SqlSession對象操作數據
**ORM框架**
O:java Object 即 Java 中的對象;
R:relationship 即關系數據庫;
M:mapping 將JAVA中的對象映射成關系型數據庫中的表;
>[info] MyBatis 框架是一個可以自定義 SQL 和 OR 映射的持久化框架;
> 框架拋棄了大部分的 JDBC 代碼,也不需要手工設置參數以及結果集的操作;
> 框架使用簡單的 XML 配置或者注解來映射數據類型和關系,相對于 Hibernate 框架,MyBatis 是一種半自動化的 ORM 實現。
## MyBatis配置
在本課程中,MyBatis將依賴于Maven進行管理。
在MyBatis中,使用xml進行配置,有一個約定俗成的文件名叫做`mybatis-config.xml`,它是mybatis的一個核心配置文件。
~~~Markdown
1. Mybatis采用xml文件配置數據庫環境信息
2. Mybatis環境配置標簽<environment>
3. environment配置包含數據庫驅動,URL,用戶名和密碼
~~~

**前期準備-新建項目**
pom.xml
~~~
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.dodoke</groupId>
<artifactId>mybatis</artifactId>
<version>1.0.0-SNAPSHOT</version>
<repositories>
<repository>
<id>aliyun</id>
<name>aliyun</name>
<!-- 可能阿里云倉庫的地址會發生變化,需要查找最新的地址 -->
<url>https://maven.aliyun.com/repository/public</url>
</repository>
</repositories>
<dependencies>
<!-- 數據庫驅動依賴 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>
<!-- mybatis依賴 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.5</version>
</dependency>
</dependencies>
</project>
~~~
**前期準備-數據庫設計**
下載地址
https://pan.baidu.com/s/1xgxXH9tPn0O_Qf5QfmmbBg
提取碼
mso3
**設置idea連接數據庫**

**resources目錄下設置mybatis的核心配置文件mybatis-config.xml**
~~~xml
<?xml version="1.0" encoding="utf-8" ?>
<!-- 官網復制DTD約束 -->
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<!-- 設置根節點 -->
<configuration>
<!-- 設置環境配置總標簽,會有一些必填項出現 -->
<!-- 通過改變environments的default屬性的值來選擇使用什么環境 -->
<environments default="dev">
<!-- 環境配置標簽,id屬性為唯一標識 -->
<!-- 說明:在我們的開發中,可能會用到很多的數據庫 -->
<!-- 比如在開發環境中是一套數據庫,在實際生產環境中又是另一套數據庫 -->
<!-- environment就能指明要使用的不同數據庫信息 -->
<!-- id屬性不能相同 -->
<!-- 比如這個環境配置標簽就可以代表開發環境 -->
<environment id="dev">
<!-- 采用JDBC方式對數據庫事務進行管理-commit/rollback -->
<!-- 其實是指,由jdbc來決定提交事務或者回滾事務 -->
<transactionManager type="JDBC"></transactionManager>
<!-- 數據源節點,設置type="POOLED"采用數據庫連接池的方式管理數據庫連接 -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/babytun?serverTimezone=UTC&characterEncoding=UTF-8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
<!-- 比如下面這個環境標簽就代表生產環境 -->
<environment id="prd">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://192.168.1.155:3306/babytun?serverTimezone=UTC&characterEncoding=UTF-8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
</configuration>
~~~
## SqlSessionFactory & SqlSession
`SqlSessionFactory`是`MyBatis`中的一個重要的對象,它是用來創建`SqlSession`對象的,而`SqlSession`用來操作數據庫的。
**介紹:**
* `SqlSessionFactory`是`MyBatis`的核心對象
* `SqlSessionFactory`用于初始化`MyBatis`,讀取配置文件。是一個工廠方法,用于創建`SqlSession`對象。
* 要保證`SqlSessionFactory`在應用全局中只存在唯一的對象,通常會使用靜態類的方式對其進行初始化。
`SqlSession`是`MyBatis`用來操作數據庫的一個核心對象,不那么嚴謹的說,可以將SqlSession看做類似于我們之前學習過的JDBC的連接接口對象(`Connection`)執行接口對象(`PreparedStatement`)的組合,用來執行CRUD操作。
**介紹:**
* `SqlSession`是`MyBatis`操作數據庫的核心對象
* `SqlSession`使用JDBC的方式與數據庫交互
* `SqlSession`對象提供了數據表的CRUD方法
**示例**
引入Junit組件進行測試應用
依賴:
~~~
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
~~~
測試代碼:
~~~
package com.dodoke.mybatistest;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;
import java.io.IOException;
import java.io.Reader;
import java.sql.Connection;
/**
* Junit單元測試用例類
* 規范存放在maven項目的test文件夾中
*/
public class MyBatisTest {
@Test
public void sqlSessionFactoryTest() throws IOException {
//通過MyBatis提供的資源類,獲取對應的配置文件作為字符流讀取
//getResourceAsReader方法會默認的從當前classpath類路徑下加載文件
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
//初始化SqlSessionFactory,并同時解析mybatis-config.xml文件
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
System.out.println("SqlSessionFactory對象加載成功");
//創建SqlSession對象,用于與數據庫產生交互,注意SqlSession它是JDBC的擴展類
SqlSession sqlSession = null;
try {
sqlSession = sqlSessionFactory.openSession();
//在SqlSession對象底層存在Connection(java.sql)連接對象,可以通過getConnection方法得到該對象
Connection connection = sqlSession.getConnection();
//該connection對象的創建僅做演示測試用,在mybatis中,無需使用任何與JDBC有關的類
System.out.println(connection);
} catch (Exception e) {
e.printStackTrace();
} finally {
if(sqlSession != null) {
//在mybatis-config.xml文件中,dataSource節點type屬性:
//如果type="POOLED",代表使用連接池,close則是將連接回收到連接池中
//如果type="UNPOOLED",代表直連,close則會調用Connection.close()方法關閉連接
//這是配置帶來的底層處理機制的不同
sqlSession.close();
}
}
}
}
~~~
## 設置MybatisUtils工具類
在之前的課程中,我們提到需要保證`SqlSessionFactory`在全局中保證唯一,那么如何保證該`SqlSessionFactory`在應用全局保證唯一呢?
通過額外創建的工具類`MybatisUtils`對`SqlSessionFactory`對象的初始化以及`SqlSession`對象的創建和釋放方法進行封裝 。
**說明:**
1. 一般工具類放在`utils`包下;
2. 用`static`代碼塊對靜態對象進行初始化;
3. 這邊我們在異常捕獲后將類的初始化的過程中產生的異常拋出,為了外界能捕獲到這個異常信息并進行后續處理,而不是直接終止運行程序,我們需要將異常拋出;
4. 提供`SqlSession`對象的創建與釋放方法,工具類的大多數方法要使用`static`進行描述。
**工具類代碼**
~~~
package com.dodoke.mybatis;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.Reader;
/**
* MyBatisUtils工具類,創建全局唯一的SqlSessionFactory對象
*/
public class MyBatisUtils {
//設置私有靜態屬性,因為靜態內容屬于類而不屬于對象,且擁有全局唯一的特性
private static SqlSessionFactory sqlSessionFactory = null;
//利用靜態代碼塊在初始化類時實例化sqlSessionFactory屬性
static {
try {
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
} catch (IOException e) {
e.printStackTrace();
//需要拋出初始化的異常,并且傳入捕捉到的異常,形成一條完整的異常鏈
//以便于通知調用者
throw new ExceptionInInitializerError(e);
}
}
/**
* 獲取數據庫交互SqlSession
* @return SqlSession對象
*/
public static SqlSession openSqlSession() {
return sqlSessionFactory.openSession();
}
/**
* 釋放一個有效的SqlSession對象
* @param sqlSession 準備釋放的SqlSession對象
*/
public static void closeSqlSession(SqlSession sqlSession) {
if(sqlSession != null) {
sqlSession.close();
}
}
}
~~~
**測試類單元測試代碼**
~~~
/**
* MyBatisUtils使用指南
* @throws Exception
*/
@Test
public void testMyBatisUtils() throws Exception {
SqlSession sqlSession = null;
try {
sqlSession = MyBatisUtils.openSqlSession();
Connection connection = sqlSession.getConnection();
System.out.println(connection);
}catch (Exception e){
throw e;
} finally {
MyBatisUtils.closeSqlSession(sqlSession);
}
}
~~~
## MyBatis數據查詢
在MyBatis中,雖然我們可以使用MyBatis之前的舊形式,寫出如同JDBC那樣Java代碼和SQL代碼混合的數據操作命令,但是我們不建議大家這么做!
對于MyBatis數據查詢,可以總結出如下的步驟:
**1. 創建實體類(Entity)**
在`main/java`下創建`com.dodoke.mybatis.entity`包,`entity`包下創建數據庫中`t_goods`表對應的`Goods`商品實體類,將數據表中的字段對應在實體類中增加一系列的私有屬性及getter/setter方法,屬性采用駝峰命名。

~~~
/**
* 數據庫t_goods表對應映射的實體類
*/
public class Goods {
private Integer goodsId;//商品編號
private String title;//標題
private String subTitle;//子標題
private Float originalCost;//原始價格
private Float currentPrice;//當前價格
private Float discount;//折扣率
private Integer isFreeDelivery;//是否包郵 ,1-包郵 0-不包郵
private Integer categoryId;//分類編號
public Integer getGoodsId() {
return goodsId;
}
public void setGoodsId(Integer goodsId) {
this.goodsId = goodsId;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getSubTitle() {
return subTitle;
}
public void setSubTitle(String subTitle) {
this.subTitle = subTitle;
}
public Float getOriginalCost() {
return originalCost;
}
public void setOriginalCost(Float originalCost) {
this.originalCost = originalCost;
}
public Float getCurrentPrice() {
return currentPrice;
}
public void setCurrentPrice(Float currentPrice) {
this.currentPrice = currentPrice;
}
public Float getDiscount() {
return discount;
}
public void setDiscount(Float discount) {
this.discount = discount;
}
public Integer getIsFreeDelivery() {
return isFreeDelivery;
}
public void setIsFreeDelivery(Integer isFreeDelivery) {
this.isFreeDelivery = isFreeDelivery;
}
public Integer getCategoryId() {
return categoryId;
}
public void setCategoryId(Integer categoryId) {
this.categoryId = categoryId;
}
}
~~~
**2. 創建Mapper XML說明SQL語句**
第二步,第三步結合使用,具體內容在第三步中。
**3. 在Mapper XML中增加SQL語句對應標簽**
在`main/resources`下創建新的子目錄`mappers`,`mappers`代表映射器,里面存放的都是xml文件。創建`GoodsMapper.xml`文件來說明實體類和數據表的對應關系(和哪個數據表對應,屬性和字段怎么對應)。
說明:
`A.` 根節點通過增加不同的命名空間`namespace`來區分不同的`mapper`文件,通常我們會將針對一張表操作的`SQL`語句放置在一個`mapper`文件中。
`B.` 語句節點的`id`屬性為別名,相當于`SQL`名稱,同一個`namespace`下`id`要唯一,不同的`namespace`可以重名;因此`namespace`的設置就很有必要,不然調用`SQL`的時候分不清哪個`id`
`C.` 語句節點的`resultType`屬性代表返回的對象是什么,為實體類的完整路徑,在`SQL`語句執行完后會自動的將得到的每一條記錄包裝成對應的實體類的對象;
~~~
<?xml version="1.0" encoding="UTF-8" ?>
<!-- 將之前的config DTD約束改為mapper DTD約束 -->
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 為這個mapper指定一個唯一的namespace -->
<!-- 注意每個mapper文件的namespace是不能相同的 -->
<!-- namespace非常類似于Java類的包,同樣也是用于區分每個不同的mapper文件 -->
<!-- 所以namespace的值,習慣上設置成包名+sql映射文件名,這樣就能夠保證namespace的值是唯一的 -->
<!-- 例如namespace="com.dodoke.mybatis.resources.mappers.GoodsMapper" -->
<!--就是com.dodoke.mybatis.resources.mappers(包名/文件夾名)+GoodsMapper(GoodsMapper.xml文件去除后綴) -->
<mapper namespace="com.dodoke.mybatis.resources.mappers.GoodsMapper">
<!-- id屬性相當于為SQL語句起了個名稱 -->
<!-- 在一個mapper文件中是不允許出現相同的id屬性值的 -->
<!-- resultType代表返回結果的類型,它會將SQL語句執行完的每一條結果包裝為對應的屬性值指定的對象 -->
<select id="selectAll" resultType="com.dodoke.mybatis.entity.Goods">
select * from t_goods order by goods_id desc limit 10
</select>
</mapper>
~~~
**4. 在mybatis-config.xml中增加Mapper XML文件聲明**
其實就是讓`MyBatis`認識新創建的`GoodsMapper.xml`:
在`mybatis-config.xml`中添加`mappers`標簽,這樣`MyBatis`在初始化的時候才知道這個`GoodsMapper.xml`的存在。

~~~
<mappers>
<!-- 注冊GoodsMapper.xml文件 -->
<mapper resource="mappers/GoodsMapper.xml"></mapper>
</mappers>
~~~
**5. 利用SqlSession執行Mapper XML中的SQL語句**
~~~
/**
* select查詢語句執行
* @throws Exception
*/
@Test
public void testSelectAll() throws Exception {
SqlSession session = null;
try{
session = MyBatisUtils.openSqlSession();
//selectList代表查詢多條數據,selectOne代表查詢一條結果
List<Goods> list = session.selectList("com.dodoke.mybatis.resources.mappers.GoodsMapper.selectAll");
for(Goods g : list){
System.out.println(g.getTitle());
}
}catch (Exception e){
throw e;
}finally {
MyBatisUtils.closeSqlSession(session);
}
}
~~~
對于這樣的查詢,其實獲取到的數據是存在數據丟失的,這是因為我們的查詢結果類型字段和表中字段名不能匹配!
**6. 在mybatis-config.xml中開啟駝峰命名映射**
其實第六步應該放在第五步之前,這里只是給大家作為演示。

~~~
<settings>
<!-- 駝峰命名轉化設置 -->
<!-- 該設置表示將數據庫中表的字段,比如goods_id => goodsId -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
~~~
## MyBatis的SQL傳參
在實現CRUD等操作的時候,有很多的SQL條件數據其實是通過接受前臺動態傳遞過來的參數決定的。那么如何設置這些SQL語句的參數呢?
在數據操作節點中,可以添加`parameterType`屬性指定參數類型,并采用`#{param}`的形式接受傳入的參數。
示例:
GoodsMapper.xml
~~~xml
<!-- 單參數傳遞,使用parameterType指定參數的數據類型即可,SQL中#{value}提取參數-->
<select id="selectById" parameterType="Integer" resultType="com.dodoke.mybatis.entity.Goods">
select * from t_goods where goods_id = #{value}
</select>
<!-- 多參數傳遞時,使用parameterType指定Map接口,SQL中#{key}提取參數 -->
<select id="selectByPriceRange" parameterType="java.util.Map" resultType="com.dodoke.mybatis.entity.Goods">
select * from t_goods
where
current_price between #{min} and #{max}
order by current_price
limit 0,#{limt}
</select>
~~~
測試:
~~~
/**
* 傳遞單個SQL參數
* @throws Exception
*/
@Test
public void testSelectById() throws Exception {
SqlSession session = null;
try{
session = MyBatisUtils.openSqlSession();
//傳入的參數類型需要和對應數據操作節點中指明的參數類型一致
Goods goods = session.selectOne("com.dodoke.mybatis.resources.mappers.GoodsMapper.selectById" , 1603);
System.out.println(goods.getTitle());
}catch (Exception e){
throw e;
}finally {
MyBatisUtils.closeSqlSession(session);
}
}
/**
* 傳遞多個SQL參數
* @throws Exception
*/
@Test
public void testSelectByPriceRange() throws Exception {
SqlSession session = null;
try{
session = MyBatisUtils.openSqlSession();
Map param = new HashMap();
//map中的key-value的key值,需要和數據操作節點中參數名一致
param.put("min",100);
param.put("max" , 500);
param.put("limt" , 10);
List<Goods> list = session.selectList("com.dodoke.mybatis.resources.mappers.GoodsMapper.selectByPriceRange", param);
for(Goods g:list){
System.out.println(g.getTitle() + ":" + g.getCurrentPrice());
}
}catch (Exception e){
throw e;
}finally {
MyBatisUtils.closeSqlSession(session);
}
}
~~~
## 多表關聯查詢
在之前的學習中,我們針對的都是一個表的查詢,那么如何針對多表進行聯合查詢呢?
其實我們可以將返回的結果變為`Map`類型,這樣`MyBatis`就會將結果封裝為`Map`集合中對應的鍵值對
~~~
<select id="selectGoodsMap" resultType="java.util.Map" flushCache="true">
select g.* , c.category_name from t_goods g , t_category c
where g.category_id = c.category_id
</select>
~~~
~~~
/**
* 利用Map接收關聯查詢結果
* @throws Exception
*/
@Test
public void testSelectGoodsMap() throws Exception {
SqlSession session = null;
try{
session = MyBatisUtils.openSqlSession();
List<Map> list = session.selectList("com.dodoke.mybatis.resources.mappers.GoodsMapper.selectGoodsMap");
for(Map map : list){
System.out.println(map);
}
}catch (Exception e){
throw e;
}finally {
MyBatisUtils.closeSqlSession(session);
}
}
~~~
我們可以看到,該方法返回的結果為數據庫中表對應的原始字段名為key值,而且查詢到的結果的順序是混亂的。
為了保證我們等到的結果的順序和數據庫中的順序一致,我們需要使用`LinkedHashMap`。
1. `LinkedHashMap`是采用鏈表形式的`HashMap`,他在進行數據提取的時候是按照插入數據時的順序進行提取保存的,不會出現亂序的情況。
2. 使用`LinkedHashMap`來接收數據是常用的,因為公司的數據結構較為復雜,需要多表關聯查詢,`LinkedHashMap`可以有效進行數據的擴展,非常靈活。
3. 缺點:太過靈活,任何查詢結果都會被`LinkedHashMap`包裝在內,相比較實體類而言,缺少了編譯時檢查,是很容易出錯的。
~~~
<!-- 利用LinkedHashMap保存多表關聯結果
MyBatis會將每一條記錄包裝為LinkedHashMap對象
key是字段名 value是字段對應的值 , 字段類型根據表結構進行自動判斷
優點: 易于擴展,易于使用
缺點: 太過靈活,無法進行編譯時檢查
-->
<!-- 新增一個test字段看一下結果 -->
<select id="selectGoodsMap" resultType="java.util.LinkedHashMap">
select g.* , c.category_name,'1' as test from t_goods g , t_category c
where g.category_id = c.category_id
</select>
~~~
其實針對于這樣的多表查詢,我們還可以通過修改實體類來實現,顯得不夠靈活,但是卻可以保證在編譯的時候進行檢查。具體選用哪種方式可以根據實際情況進行選擇。
PS:注意,在之前我們的學習中,我們是利用在`mybaits-config.xml`文件中設置駝峰映射的方式,來解決字段和實體類屬性名稱不能匹配的問題的,但是我們也可以設置在查詢的時候起別名的方式,解決這個問題。
## ResultMap結果映射
介紹:
* ResultMap可以將查詢結果映射為復雜類型的Java對象。
* ResultMap適用于Java對象保存多表關聯結果
* ResultMap是MyBatis關聯的核心所在,支持對象關聯查詢等高級特性
在上節課程中,我們也提到過,可以為了查詢結果去修改實體類。但是,這種方式在標準的mybatis開發下是不太建議的。實體類僅僅和數據表對應就好,不要添加一些冗余的屬性,但是**在實際開發中,我們有時為了方便,實際上較多的還是采用修改實體類的形式**。
但是,采用DTO,數據擴展類開發的形式,我們同學們必須掌握。
在`com.dodoke.mybatis`包下面新建一個dto包,新建`GoodsDTO`類。
>[info]DTO是一個特殊的JavaBean,數據傳輸對象。對原始對象進行擴展,用于數據保存和傳遞。
~~~
/**
* 擴展類,數據傳輸對象
*/
public class GoodsDTO {
private Goods goods = new Goods();
private String categoryName;
private String test;
public Goods getGoods() {
return goods;
}
public void setGoods(Goods goods) {
this.goods = goods;
}
public String getCategoryName() {
return categoryName;
}
public void setCategoryName(String categoryName) {
this.categoryName = categoryName;
}
public String getTest() {
return test;
}
public void setTest(String test) {
this.test = test;
}
}
~~~
使用resultMap屬性,添加結果映射
~~~
<!--結果映射,通常id屬性設置為rm+...,type指明要轉化為的DTO -->
<resultMap id="rmGoods" type="com.dodoke.mybatis.dto.GoodsDTO">
<!--設置主鍵字段與屬性映射,必寫-->
<!-- property=goods.goodsId指的是,每次查到的goods_id字段的數據會為GoodsDTO類中goods屬性對象的goodsId屬性進行賦值 -->
<id property="goods.goodsId" column="goods_id"></id>
<!--設置非主鍵字段與屬性映射-->
<result property="goods.title" column="title"></result>
<result property="goods.originalCost" column="original_cost"></result>
<result property="goods.currentPrice" column="current_price"></result>
<result property="goods.discount" column="discount"></result>
<result property="goods.isFreeDelivery" column="is_free_delivery"></result>
<result property="goods.categoryId" column="category_id"></result>
<result property="categoryName" column="category_name"></result>
<result property="test" column="test"/>
</resultMap>
<select id="selectGoodsDTO" resultMap="rmGoods">
select g.* , c.*,'1' as test from t_goods g , t_category c
where g.category_id = c.category_id
</select>
~~~
測試
~~~
/**
* 利用ResultMap進行結果映射
* @throws Exception
*/
@Test
public void testSelectGoodsDTO() throws Exception {
SqlSession session = null;
try{
session = MyBatisUtils.openSqlSession();
List<GoodsDTO> list = session.selectList("com.dodoke.mybatis.resources.mappers.GoodsMapper.selectGoodsDTO");
for (GoodsDTO g : list) {
System.out.println(g.getGoods().getTitle());
}
}catch (Exception e){
throw e;
}finally {
MyBatisUtils.closeSqlSession(session);
}
}
~~~
其實我們可以繼續擴展,比如我現在不僅僅想要得到`category_name`產品名稱,還想要獲得其他屬性,那么我們該怎么辦呢?
新建`t_category`表的實體類
~~~
package com.dodoke.mybatis.entity;
public class Category {
private Integer categoryId;
private String categoryName;
private Integer parentId;
private Integer categoryLevel;
private Integer categoryOrder;
public Integer getCategoryId() {
return categoryId;
}
public void setCategoryId(Integer categoryId) {
this.categoryId = categoryId;
}
public String getCategoryName() {
return categoryName;
}
public void setCategoryName(String categoryName) {
this.categoryName = categoryName;
}
public Integer getParentId() {
return parentId;
}
public void setParentId(Integer parentId) {
this.parentId = parentId;
}
public Integer getCategoryLevel() {
return categoryLevel;
}
public void setCategoryLevel(Integer categoryLevel) {
this.categoryLevel = categoryLevel;
}
public Integer getCategoryOrder() {
return categoryOrder;
}
public void setCategoryOrder(Integer categoryOrder) {
this.categoryOrder = categoryOrder;
}
}
~~~
修改DTO數據對象
~~~
package com.dodoke.mybatis.dto;
import com.dodoke.mybatis.entity.Category;
import com.dodoke.mybatis.entity.Goods;
/**
* 擴展類,數據傳輸對象
*/
public class GoodsDTO {
private Goods goods = new Goods();
private Category category = new Category();
private String test;
public Goods getGoods() {
return goods;
}
public void setGoods(Goods goods) {
this.goods = goods;
}
public Category getCategory() {
return category;
}
public void setCategory(Category category) {
this.category = category;
}
public String getTest() {
return test;
}
public void setTest(String test) {
this.test = test;
}
}
~~~
修改映射結果集
~~~
<!--結果映射,通常id屬性設置為rm+...,type指明要轉化為的DTO -->
<resultMap id="rmGoods" type="com.dodoke.mybatis.dto.GoodsDTO">
<!--設置主鍵字段與屬性映射,必寫-->
<!-- property=goods.goodsId指的是,每次查到的goods_id字段的數據會為GoodsDTO類中goods屬性對象的goodsId屬性進行賦值 -->
<id property="goods.goodsId" column="goods_id"></id>
<!--設置非主鍵字段與屬性映射-->
<result property="goods.title" column="title"></result>
<result property="goods.originalCost" column="original_cost"></result>
<result property="goods.currentPrice" column="current_price"></result>
<result property="goods.discount" column="discount"></result>
<result property="goods.isFreeDelivery" column="is_free_delivery"></result>
<result property="goods.categoryId" column="category_id"></result>
<result property="category.categoryId" column="category_id"></result>
<result property="category.categoryName" column="category_name"></result>
<result property="category.parentId" column="parent_id"></result>
<result property="category.categoryLevel" column="category_level"></result>
<result property="category.categoryOrder" column="category_order"></result>
<result property="test" column="test"/>
</resultMap>
<select id="selectGoodsDTO" resultMap="rmGoods">
select g.* , c.*,'1' as test from t_goods g , t_category c
where g.category_id = c.category_id
</select>
~~~
## MyBatis數據寫入
在之前的課程中,我們實現了MyBatis的數據查詢工作,接下來,我們來看看如何實現數據的新增,修改和刪除工作。
### 數據庫事務
提到數據庫的寫入操作,就離不開數據庫的事務。
**數據庫事務是保證數據操作完整性的基礎**
所有從客戶端發來的新增修改刪除操作,都會被事務日志所記錄,我們形象的將事務日志看成流水賬,它記錄客戶端發來的所有寫操作的前后順序,
當客戶端向`MySQL`服務器發起了一個`commit`提交命令的時候,事務日志才會將這三個數據同時的寫入到數據表中,在`commit`的時候才是真正的往數據表寫入的過程,當這三條數據都被成功寫入到數據表中后,剛才所產生的事務日志都會被清空掉。


假設如果客戶端在處理這些數據的時候,數據1和數據2執行成功,數據3因為各種原因沒有執行成功的話,客戶端會發起一個`rollback`回滾命令,當`MySQL`收到了`rollback`回滾命令后,當前事務日志中的所有已經產生的數據都會被清除,這就意味著前面已經產生的數據1和數據2不會放入到數據表中,只有當所有數據都完成的時候,在由客戶端發起`commit`提交,數據才能成功的寫入。
**要么數據全部寫入成功,要么中間出現了任何問題,全部回滾,保證了數據的完整性**
### 案例
修改`MyBatisUtils`
~~~
/**
* 獲取數據庫交互SqlSession
* @return SqlSession對象
*/
public static SqlSession openSqlSession() {
//默認SqlSession對自動提交事務數據(commit)
//設置false代表關閉自動提交,改為手動提交事務數據
return sqlSessionFactory.openSession(false);
}
~~~
**新增**
~~~
<insert id="insert" parameterType="com.dodoke.mybatis.entity.Goods">
INSERT INTO t_goods(title, sub_title, original_cost, current_price, discount, is_free_delivery, category_id)
VALUES (#{title} , #{subTitle} , #{originalCost}, #{currentPrice}, #{discount}, #{isFreeDelivery}, #{categoryId})
<!-- 該語句可以不寫 -->
<!-- 該語句表示插入goods對象后,獲得插入后自動生成的主鍵值,并將該值保存到插入的goods對象的goodsId中 -->
<selectKey resultType="Integer" keyProperty="goodsId" order="AFTER">
select last_insert_id()
</selectKey>
</insert>
~~~
~~~
/**
* 新增數據
* @throws Exception
*/
@Test
public void testInsert() throws Exception {
SqlSession session = null;
try{
session = MyBatisUtils.openSqlSession();
Goods goods = new Goods();
goods.setTitle("測試商品");
goods.setSubTitle("測試子標題");
goods.setOriginalCost(200f);
goods.setCurrentPrice(100f);
goods.setDiscount(0.5f);
goods.setIsFreeDelivery(1);
goods.setCategoryId(43);
//insert()方法返回值代表本次成功插入的記錄總數
int num = session.insert("com.dodoke.mybatis.resources.mappers.GoodsMapper.insert", goods);
session.commit();//提交事務數據
System.out.println(goods.getGoodsId());
}catch (Exception e){
if(session != null){
session.rollback();//回滾事務
}
throw e;
}finally {
MyBatisUtils.closeSqlSession(session);
}
}
~~~
我們在上述代碼中可以利用`selectKey`標簽獲得對應的新增主鍵,其實我們還可以利用另外一個屬性`userGenerateKeys`實現獲得新增主鍵,它們的區別在哪里呢?
1. `SelectKey`適用于所有數據庫,但需要根據不同的數據庫編寫對應的獲得最后改變主鍵值得查詢語句
2. `userGenerateKeys`只支持“自增主鍵”的數據庫(DB2,Oracle等沒有自增主鍵約束),但使用簡單,會根據不同的數據庫驅動自動編寫查詢語句,以下是該屬性的使用方法
~~~
<insert id="insert" parameterType="com.dodoke.mybatis.entity.Goods" userGenerateKeys="true" keyProperty="goodsId" keyColumn="goods_id">
insert 語句
</insert>
~~~
如果要在Oracle中獲得新增后的主鍵,需要借助序列來實現,其實是通過序列在執行新增語句之前生成一個新的序列值并保存到主鍵字段中。

**更新與刪除**
~~~
<update id="update" parameterType="com.dodoke.mybatis.entity.Goods">
UPDATE t_goods
SET
title = #{title} ,
sub_title = #{subTitle} ,
original_cost = #{originalCost} ,
current_price = #{currentPrice} ,
discount = #{discount} ,
is_free_delivery = #{isFreeDelivery} ,
category_id = #{categoryId}
WHERE
goods_id = #{goodsId}
</update>
<delete id="delete" parameterType="Integer">
delete from t_goods where goods_id = #{value}
</delete>
~~~
~~~
/**
* 更新數據
* @throws Exception
*/
@Test
public void testUpdate() throws Exception {
SqlSession session = null;
try{
session = MyBatisUtils.openSqlSession();
Goods goods = session.selectOne("com.dodoke.mybatis.resources.mappers.GoodsMapper.selectById", 739);
goods.setTitle("更新測試商品");
int num = session.update("com.dodoke.mybatis.resources.mappers.GoodsMapper.update" , goods);
session.commit();//提交事務數據
}catch (Exception e){
if(session != null){
session.rollback();//回滾事務
}
throw e;
}finally {
MyBatisUtils.closeSqlSession(session);
}
}
/**
* 刪除數據
* @throws Exception
*/
@Test
public void testDelete() throws Exception {
SqlSession session = null;
try{
session = MyBatisUtils.openSqlSession();
int num = session.delete("com.dodoke.mybatis.resources.mappers.GoodsMapper.delete" , 739);
session.commit();//提交事務數據
}catch (Exception e){
if(session != null){
session.rollback();//回滾事務
}
throw e;
}finally {
MyBatisUtils.closeSqlSession(session);
}
}
~~~
## 預防SQL注入攻擊
在之前的學習中,我們了解到什么是SQL注入攻擊,并且在JDBC課程中也去實現了如何預防SQL注入攻擊。那么,在MyBatis中如何去進行SQL注入攻擊的預防呢?
其實,SQL注入攻擊的原理非常簡單,就在在接收用戶輸入的時候,不對接收的數據進行任何的判斷和轉義,導致在接收時可能全盤接收到諸如單引號、or等一些SQL關鍵字。
所以,預防SQL注入攻擊需要的是對接收數據進行判斷和轉義。在MyBatis中,這些工作其實早就已經為我們準備好了。
**MyBatis兩種傳值方式**
* `${}`文本替換,未經任何處理對SQL文本替換
* `#{}`預編譯傳值,使用預編譯傳值可以預防SQL注入
在我們的實際使用中,更多的還是通過`#{}`的形式進行傳值