1. MyBatis框架的作用
MyBatis是持久層框架。
在Java實現數據庫編程時,主要通過JDBC來實現,而JDBC相關代碼在實現過程中流程相對固定,所以就出現了各種減化開發的持久層框架,常見的有Hibernate和MyBatis。
使用MyBatis框架時無需編寫JDBC相關代碼,只需要爲某些抽象方法配置其對應的需要執行的SQL語句即可。
2. 使用MyBatis實現數據訪問
2.1. 案例目標:通過MyBatis實現用戶註冊
假設用戶數據有:用戶名、密碼、年齡、手機號碼、電子郵箱,通過使用MyBatis框架實現向數據庫的表中插入用戶數據。
2.2. 創建數據庫與數據表
創建數據庫ums-->使用數據庫ums-->創建數據表t_user
2.3. 創建項目
創建項目時必須做的5件事情(添加web.xml,添加Tomcat環境,添加依賴(pom.xml),複製web.xml中的DispatcherServlet和CharacterEncodingFilter的配置,複製spring-mvc.xml)。
完成後,打開spring-mvc.xml,刪除與本次項目不相關的配置,例如攔截器的配置等等。
在使用MyBatis時,還需要添加相關依賴。首先在pom.xml中,將spring-webmvc的依賴複製一份,修改爲spring-jdbc。
在同一個項目中,如果需要使用多個以`spring-`作爲前綴的依賴,則應該使用相同的版本。
然後,還應該檢查是否已經添加mysql-connector-java依賴,用於操作mysql數據庫:
繼續檢查是否已經添加commons-dbcp依賴,用於處理數據庫連接池等:
繼續檢查是否已經添加junit依賴,用於單元測試:
然後,添加mybatis依賴。
通常,會將mybatis與spring整合起來使用,添加mysql-spring依賴:
注意:後續代碼運行時,如果本應該存在的類/接口卻提示ClassNotFoundException,或提示ZipException,則意味着新添加的依賴是下載失敗的,需要刪除對應的jar文件甚至所在的文件夾,然後重新更新項目。
2.4. 建立連接
在resources下創建db.properties文件,用於配置數據庫連接。
將spring-mvc.xml複製,並粘貼爲spring-dao.xml,刪除文件中原有的配置,並加載db.properties,將配置的屬性注入到BasicDataSource中:
<!-- 讀取db.properties中的配置 -->
<util:properties id="db" location="classpath:db.properties" />
<!-- 配置數據源 -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="url" value="#{db.url}" />
<property name="driverClassName" value="#{db.driver}" />
...
</bean>
然後,編寫並執行單元測試:
@Test
public void getConnection() throws SQLException {
// 加載Spring配置文件,獲取Spring容器
AbstractApplicationContext ac= new ClassPathXmlApplicationContext("spring-dao.xml");
// 從Spring容器中獲取對象
BasicDataSource dataSource = ac.getBean("dataSource", BasicDataSource.class);
// 測試
Connection conn = dataSource.getConnection();
System.out.println(conn);
// 釋放資源
ac.close();
}
2.5.創建類和接口
1)創建cn.tedu.mybatis.User類,在類中聲明與數據表對應的6個屬性:
public class User {
private Integer id;
...// SET/GET/toString
}
> 每張數據表都應該有一個與之對應的實體類。
2)創建cn.tedu.mybatis.UserMapper接口,並在接口中聲明抽象方法:
public Integer addnew(User user);
在使用MyBatis時,如果抽象方法對應需要執行的數據操作是增、刪、改,則抽象方法的返回值應該是Integer,表示受影響的行數;抽象方法的名稱可以自由定義,符合語法規範並能表達語義即可;抽象方法的參數應該根據執行SQL語句時所需要的參數來決定。
然後,需要在Spring的配置文件中配置MapperScannerConfigurer,用於掃描接口文件。
<!-- MapperScannerConfigurer -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--指定接口文件所在的根包-->
<property name="basePackage" value="cn.tedu.mybatis" />
</bean>
2.6.配置映射
從FTP下載somemapper.zip並解壓得到SomeMapper.xml文件。在resources下創建名爲mappers的文件夾,將SomeMapper.xml複製到該文件夾下,並修改文件名爲UserMapper.xml。
關於UserMapper.xml的配置:
<!DOCTYPE mapper PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN"
"http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd">
<!-- namespace:匹配的Java接口 -->
<mapper namespace="cn.tedu.mybatis.UserMapper">
<!-- 根據將要執行的SQL語句的類型選擇節點 -->
<!-- id:對應的抽象方法的方法名 -->
<!-- 在SQL語句中的?應該使用#{}來佔位 -->
<!-- 在#{}中的名稱是參數User類中的屬性名 -->
<insert id="addnew">
INSERT INTO t_user (username, password,age, phone,email
) VALUES (#{username}, #{password},#{age}, #{phone},#{email})
</insert>
</mapper>
爲了使得MyBatis框架能找到這些XML的映射文件並執行其中的SQL語句,還需要在Spring的配置文件中配置:
<!-- SqlSessionFactoryBean -->
<bean class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 映射的XML文件的位置 -->
<property name="mapperLocations" value="classpath:mappers/*.xml" />
<!-- 指定數據源,取值將引用(ref)以上配置的數據源的id -->
<property name="dataSource" ref="dataSource" />
</bean>
至此開發任務已經完成,可以編寫並執行單元測試:
@Test
public void addnew() {
AbstractApplicationContext ac= new ClassPathXmlApplicationContext("spring-dao.xml");
UserMapper userMapper= ac.getBean("userMapper", UserMapper.class);
System.out.println(userMapper);
User user = new User();
user.setUsername("root");
...
Integer rows = userMapper.addnew(user);
System.out.println("rows=" + rows);
ac.close();
}
3. 多參數的處理
MyBatis框架只識別抽象方法中的1個參數,即:抽象方法應該最多隻有1個參數。
當某個抽象方法的參數只有1個時,例如:
Integer deleteById(Integer uid);
在配置映射時,佔位符中的名稱可以是隨意的名字,例如:
<delete id="deleteById">
DELETE FROM t_user WHERE id=#{helloworld}
</delete>
因爲參數只有1個,框架會自動的讀取對應位置的參數並應用於執行SQL語句,所以參數的名稱根本就不重要。
另外,每個.java源文件在被編譯成爲.class文件之後,也都會丟失參數名稱。
可以將抽象方法的參數設計爲1個Map,則可以滿足“參數只有1個”的需求,但使用時非常不方便,因爲方法的調用者並不明確向Map中封裝數據時應該使用什麼名稱的key。
爲了解決這個問題,MyBatis中提供了@Param註解,在設計抽象方法時,允許使用多個參數,但是每個參數之前都應該添加該註解,並在註解中設置參數的名稱,後續在執行時,MyBatis會基於這些註解將調用時的參數封裝爲Map來執行:
【小結】 當需要多個參數時(超過1個參數時),每個參數前都添加@Param註解,並且註解中的配置的名稱、方法的參數名稱、XML中佔位符中的名稱應該都使用相同的名稱!
### 2. 查詢數據
與增、刪、改相差不大,查詢時也應該先設計抽象方法,再配置映射。
在配置抽象方法時,返回值的類型應該根據查詢需求來決定,即:調用該方法後希望得到什麼的數據。
例如:**根據用戶的id查詢用戶詳情**,則對應的抽象方法應該是:
User findById(Integer id);
在配置映射時,查詢所使用的`<select>`必須配置resultType屬性或resultMap屬性(二選一):
<select id="findById" resultType="cn.tedu.mybatis.User">
SELECT id, username,password, age,phone, email FROM t_user WHERE id=#{id}
</select>
例如:**查詢當前表中所有的數據**:
List<User> findAll();
在配置映射時,使用的resultType值依然是User的類型,因爲所有的查詢得到的都可以是一系列的數據,所以查詢多條數據時,無需告訴MyBatis結果將是List集合,只需要告訴它List集合內部的元素是哪種類型就可以了:
<select id="findAll" resultType="cn.tedu.mybatis.User">
SELECT id, username,password, age,phone, email FROM t_user
</select>
例如:**獲取當前表中的數據的數量**:
Integer countById();
在配置映射時,需要注意:無論多麼簡單的返回值類型,都必須配置resultType。
<select id="countById" resultType="java.lang.Integer">
SELECT COUNT(id) FROM t_user
</select>
### 3. 關於別名
在數據表中添加is_delete INT字段,用於表示該數據是否標記爲刪除:
ALTER TABLE t_user ADD COLUMN is_delete INT;
執行以上代碼後,數據表中會出現新的is_delete字段,原有的各數據的該字段值均爲NULL,再執行:
UPDATE t_user SET is_delete=0;
則可以把所有數據的is_delete都設爲0。
當數據表的結構發生變化時,實體類cn.tedu.mybatis.User和配置文件也應該一併調整:
在增加時,需要注意填寫字段名與屬性名:
is_delete 與 #{isDelete}
對應關係應該是:
在查詢操作中,需要添加新的字段並定義別名:is_delete as isDelete
對應關係是:
4. MyBatis中的動態SQL
#### 4.1. 基本概念
在配置映射時,可以添加例如<if>、<foreach>等節點,可以根據參數的不同,從而生成不同的SQL語句。
#### 4.2. <foreach>
例如有某個需求:批量刪除用戶的數據,需要執行的SQL語句例如:
DELETE FROM t_user WHERE id=3 OR id=4 OR id=5;
通常,更推薦使用IN關鍵字:
DELETE FROM t_user WHERE id IN (3,4,5)
以上SQL語句中,IN右側的括號中的id的數量是未知的,可能由用戶的操作來決定。這就需要使用動態SQL來實現。
在MyBatis的應用中,如果需要實現以上功能,首先,還是先設計抽象方法:
Integer deleteByIds(Integer[] ids);
在以上抽象方法中,參數既可以使用數組,也可以使用`List`集合來表示。
配置映射例如:
<delete id="deleteByIds">
DELETE FROM t_user
WHERE
id IN (
<foreach collection="array"
item="id" separator=",">
#{id}
</foreach>
)
</delete>
關於`<foreach>`節點中各屬性的配置:
- collection:被遍歷的集合,當對應的抽象方法只有1個參數時,取值將根據參數的類型來選擇`array`或`list`;當對應的抽象方法有多個參數時,取值應該是@Param註解中的名稱。
- item:遍歷過程中元素的名稱;
- seperator:分隔符,例如在`IN`語句中,分隔符應該是逗號`,`;
- open和close:遍歷生成的代碼區域左側與右側的值,例如:當沒有在<foreach>外部使用括號框住時,可以配置open="("`和close=")"`。
#### 4.3. <if>
使用<if>標籤可以實現SQL語句中的判斷,例如當某參數有值或沒有值時,如何執行後續的SQL語句:
<select id="findUserList" resultType="cn.tedu.mybatis.User">
SELECT * FROM t_user
<if test="where != null">
WHERE ${where}
</if>
<if test="orderBy != null">
ORDER BY ${orderBy}
</if>
<if test="offset != null">
LIMIT #{offset}, #{count}
</if>
</select>
**以上代碼幾乎可以實現任何的單表且不包含聚合函數的查詢,但是,並不推薦在實際開發中這樣使用,會造成效率低下、浪費內存資源等相關問題。**
#### 4.4. 關於#{}和${}佔位符
在MyBatis中支持#{}和${}佔位符。
在SQL語句中,可以填寫?實現預編譯的位置,需要使用#{}佔位符,例如:
DELETE FROM t_user WHERE id=?
而不可以寫`?`的位置,其內容也表現爲SQL語句中的部分語法,需要使用${}佔位符。
使用#{}在執行時是預編譯的,而${}在執行時只是單純的通過字符串拼接形成最終的SQL語句,則可能存在SQL注入的風險。
### 5.關於<resultMap>
#### 5.1 使用需求
在設計查詢時,<select>節點必須配置resultType或resultMap其中的一項。通常,只有在多表查詢時,才需要使用resultMap。
#### 5.2. 準備工作
創建新的數據表,表示**部門信息表**:
CREATE TABLE t_department (
did INT AUTO_INCREMENT,
name VARCHAR(20) NOT NULL,
PRIMARY KEY (did)
) DEFAULT CHARSET=UTF8;
然後,應該創建與之對應的實體類`cn.tedu.mybatis.Department`:
public class Department {
private Integer did;
private String name;
// SET/GET/toString
}
由於**部門信息表**應該與**用戶數據表**存在關聯,則需要在**用戶信息表**中添加新的字段,用於表示每個用戶所歸屬的部門:
ALTER TABLE t_user ADD COLUMN did INT;
最後,完善一下測試數據:
INSERT INTO t_department (name) VALUES ('軟件研發部'), ('市場部'), ('人力資源部');
以及**用戶數據表**中的數據:
UPDATE t_user SET did=3 WHERE id=4;
UPDATE t_user SET did=1 WHERE id=5;
UPDATE t_user SET did=1 WHERE id=9;
假設存在需求:**顯示某用戶的詳情,其中,部門應該顯示部門的名稱,而不是部門的id值**,則SQL語句應該是:
SELECT id, username, password, age, phone, email, name
FROM t_user
LEFT JOIN t_department
ON t_user.did=t_department.did
WHERE t_user.id=5
在使用MyBatis時,首先,應該設計抽象方法:
?? findUserById(Integer id);
可以發現,並沒有某個實體類能夠存儲以上查詢結果,在處理這種關聯查詢時,應該在項目中創建對應的VO類(Value Object),通常,VO類都是與實際的查詢需求相對應的:
所以,關於這個查詢功能對應的抽象方法應該是:
UserVO findUserById(Integer id);
> 實體類與VO類從類的結構上來說,是高度相似的,區別在於:實體類是與數據表結構相對應的,VO類是與查詢結果和實際使用相對應的。
然後,配置映射:
<select id="findUserById" resultType="cn.tedu.mybatis.UserVO">
SELECT id, username,password, age,phone, email,
name AS departmentName
FROM t_user
LEFT JOIN t_department
ON t_user.did=t_department.did
WHERE t_user.id=#{id}
</select>
假設存在需求:**顯示某部門的詳情,其中,應該包括該部門的所有員工**,則SQL語句應該是:
SELECT
id, username, password, age, phone, email,
name AS departmentName
FROM t_department
LEFT JOIN t_user
ON t_user.did=t_department.did
WHERE t_department.did=1
如果部門id爲1的有多個用戶,在執行SQL查詢後,將得到多條查詢結果,但是,需求本身是**顯示某部門的詳情**,應該只有1個查詢結果!
首先,還是應該先創建對應的VO類cn.tedu.mybatis.DepartmentVO。
則查詢時的抽象方法應該是:
DepartmentVO findDepartmentById(Integer did);
配置映射:
<!-- 當前resultMap用於指導mybatis將多條查詢結果封裝到同1個對象中 -->
<resultMap id="DepartmentVOMap" type="cn.tedu.mybatis.DepartmentVO">
<!-- id節點:用於配置主鍵 -->
<!-- column屬性:查詢結果中的列名 -->
<!-- property屬性:resultMap中type對應的數據類型中的名稱 -->
<id column="did" property="did" />
<!-- result節點:用於配置主鍵以外的其它字段的查詢 -->
<result column="name" property="name" />
<!-- collection節點:用於配置1對多關係的數據,也就是List類型的屬性 -->
<!-- ofType屬性:List集合中的元素類型 -->
<collection property="users" ofType="cn.tedu.mybatis.User">
<id column="id" property="id" />
<result column="username" property="username" />
<result column="password" property="password" />
<result column="age" property="age" />
<result column="phone" property="phone" />
<result column="email" property="email" />
</collection>
</resultMap>
<select id="findDepartmentById" resultMap="DepartmentVOMap">
SELECT did, id, username, password, age, phone, email, name
FROM t_department
LEFT JOIN t_user
ON t_user.did=t_department.did
WHERE t_department.did=#{did}
</select>