mybatis動態代理學習感悟
目的:最近學習了mybatis框架的使用,所以寫個部落格用來記錄mybatis動態代理學習中的問題以及感悟,本部落格中的專案是基於mybatis動態代理高階查詢的demo
對應的sql語句也放在了resources目錄下:專案地址
目錄
一、為什麼我們要使用mybatis?
之前我們操作資料庫都是使用的Jdbc連線池,而Jdbc存在著各種各樣的問題。
而mybatis卻能更好的解決這些問題,mybatis的特點又有哪些呢?
- 支援自定義SQL、儲存過程、及高階對映
- 實現自動對SQL的引數設定
- 實現自動對結果集進行解析和封裝
- 通過XML或者註解進行配置和對映,大大減少程式碼量
- 資料來源的連線資訊通過配置檔案進行配置
可以發現,MyBatis是對JDBC進行了簡單的封裝,幫助使用者進行SQL引數的自動設定,以及結果集與Java物件的自動對映。
二、mybatis整體架構
1.配置檔案
全域性配置檔案(核心配置檔案):mybatis-config.xml,作用:配置資料來源(配置資料庫連線資訊),引入對映檔案
對映檔案:XxMapper.xml,作用:配置sql語句、引數、結果集封裝型別等
2.SqlSessionFactory
作用:獲取SqlSession
通過newSqlSessionFactoryBuilder().build(inputStream)來構建,inputStream:讀取配置檔案的IO流
3.SqlSession
作用:執行CRUD操作
它是執行緒不安全的。
4.Executor
執行器,SqlSession通過呼叫它來完成具體的CRUD
它是一個介面,提供了兩種實現:快取的實現、資料庫的實現
5.Mapped Statement
在對映檔案裡面配置,包含3部分內容:
具體的sql,sql執行所需的引數型別,sql執行結果的封裝型別
引數型別和結果集封裝型別包括3種:
HashMap,基本資料型別,pojo
三、Mapper動態代理快速入門
Mapper介面的動態代理實現,需要遵循以下規範:
- 對映檔案中的名稱空間(名稱空間)與Mapper介面的全路徑一致
- 對映檔案中的statement的Id與Mapper介面的方法名保持一致
- 對映檔案中的statement的ResultType必須和mapper介面方法的返回型別一致(即使不採用動態代理,也要一致)
- 對映檔案中的statement的parameterType必須和mapper介面方法的引數型別一致(不一定,該引數可省略)
3.1 在pom.xml中新增mybatis的依賴
注意:每次新增依賴後都要重新整理一下。
<?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.demon.ibatis</groupId>
<artifactId>ibatis</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.32</version>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<!--junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!--日誌-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
</dependencies>
<build>
<plugins>
<!--用來設定jdk版本的-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
3.2 編寫pojo實體類
實體類中的屬性名需要對應sql中的屬性欄位名,遇到sql中的欄位名有下劃線可以使用駝峰命名法命名。
3.3 建立配置檔案
在resources下建立mybatis、jdbc和log4j的全域性配置檔案:
編寫mybatis配置檔案:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--引入jdbc配置檔案-->
<properties resource="jdbc.properties"/>
<settings>
<!--開啟自動駝峰命名規則-->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!--全域性開啟延遲載入,開啟時所有關聯物件都會延遲載入-->
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
<!--指定一個包名,MyBatis 會在包名下面搜尋需要的 Java Bean-->
<typeAliases>
<package name="com.demon.pojo"/>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<!--引入mapper對映檔案,編寫一個mapper對映檔案就需要在這裡引入一次-->
<mapper resource="mapper/UserMapper.xml"/>
</mappers>
</configuration>
configuration配置:
編寫順序一定要嚴格按照:propertes屬性、settings設定、typeAliases型別命名、typeHandlers型別處理器、objectFactory物件工廠、plugins外掛、environments環境、databaseIdProvier資料庫廠商標誌、mappers對映器
-
properties 屬性
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
-
settings 設定
設定引數 | 描述 | 有效值 | 預設值 |
cacheEnabled | 該配置影響的所有對映器中配置的快取的全域性開關。 | true | false | true |
lazyLoadingEnabled | 延遲載入的全域性開關。當開啟時,所有關聯物件都會延遲載入。特定關聯關係中可通過設定fetchType屬性來覆蓋該項的開關狀態。 | true | false | false |
aggressiveLazyLoading | 當啟用時,帶有延遲載入屬性的物件的載入與否完全取決於對任意延遲屬性的呼叫;反之,每種屬性將會按需載入。 | true | false | true |
multipleResultSetsEnabled | 是否允許單一語句返回多結果集(需要相容驅動)。 | true | false | true |
useColumnLabel | 使用列標籤代替列名。不同的驅動在這方面會有不同的表現,具體可參考相關驅動文件或通過測試這兩種不同的模式來觀察所用驅動的結果。 | true | false | true |
useGeneratedKeys | 允許 JDBC 支援自動生成主鍵,需要驅動相容。如果設定為 true 則這個設定強制使用自動生成主鍵,儘管一些驅動不能相容但仍可正常工作(比如 Derby)。 | true | false | false |
autoMappingBehavior | 指定 MyBatis 是否以及如何自動對映指定的列到欄位或屬性。NONE 表示取消自動對映;PARTIAL 只會自動對映沒有定義巢狀結果集對映的結果集。FULL 會自動對映任意複雜的結果集(包括巢狀和其他情況)。 | NONE, PARTIAL, FULL | PARTIAL |
defaultExecutorType | 配置預設的執行器。SIMPLE 就是普通的執行器;REUSE 執行器會重用預處理語句(prepared statements);BATCH 執行器將重用語句並執行批量更新。 | SIMPLE REUSE BATCH | SIMPLE |
defaultStatementTimeout | 設定超時時間,它決定驅動等待資料庫響應的秒數。 | Any positive integer | Not Set (null) |
safeRowBoundsEnabled | 允許在巢狀語句中使用行分界(RowBounds)。 | true | false | false |
mapUnderscoreToCamelCase | 是否開啟自動駝峰命名規則(camel case)對映,即從經典資料庫列名 A_COLUMN 到經典 Java 屬性名 aColumn 的類似對映。 | true | false | false |
localCacheScope | MyBatis 利用本地快取機制(Local Cache)防止迴圈引用(circular references)和加速重複巢狀查詢。預設值為 SESSION,這種情況下會快取一個會話中執行的所有查詢。若設定值為 STATEMENT,本地會話僅用在語句執行上,對相同 SqlSession 的不同調用將不會共享資料。 | SESSION | STATEMENT | SESSION |
jdbcTypeForNull | 當沒有為引數提供特定的 JDBC 型別時,為空值指定 JDBC 型別。某些驅動需要指定列的 JDBC 型別,多數情況直接用一般型別即可,比如 NULL、VARCHAR 或 OTHER。 | JdbcType enumeration. Most common are: NULL, VARCHAR and OTHER | OTHER |
lazyLoadTriggerMethods | 指定哪個物件的方法觸發一次延遲載入。 | A method name list separated by commas | equals,clone,hashCode,toString |
defaultScriptingLanguage | 指定動態 SQL 生成的預設語言。 | A type alias or fully qualified class name. | org.apache.ibatis.scripting.xmltags. XMLDynamicLanguageDriver |
callSettersOnNulls | 指定當結果集中值為 null 的時候是否呼叫對映物件的 setter(map 物件時為 put)方法,這對於有 Map.keySet() 依賴或 null 值初始化的時候是有用的。注意原始型別(int、boolean等)是不能設定成 null 的。 | true | false | false |
logPrefix | 指定 MyBatis 增加到日誌名稱的字首。 | Any String | Not set |
logImpl | 指定 MyBatis 所用日誌的具體實現,未指定時將自動查詢。 | SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING | Not set |
proxyFactory | 為 Mybatis 用來建立具有延遲載入能力的物件設定代理工具。 | CGLIB | JAVASSIST | CGLIB |
-
typeAliases 型別命名
編寫jdbc配置檔案:
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/user
jdbc.username=root
jdbc.password=root
編寫log4j配置檔案:
### direct log messages to stdout ###
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} [%t] [%c]-[%p] %m%n
### direct messages to file mylog.log ###
log4j.appender.file=org.apache.log4j.FileAppender
### 存放log日誌的檔案路徑
log4j.appender.file.File=E:/workspace/ibatis1/src/main/resources/mylog.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} [%t] [%c]-[%p] %m%n
### set log levels - for more verbose logging change 'info' to 'debug' ###
log4j.rootLogger=debug, stdout,file
3.4 編寫Mapper介面
public interface UserMapper {
/**
* 通過訂單編號20140921001查詢order並延遲載入user
* @param orderNumber 訂單編號
* @return
*/
Order queryOrderUserLazy(@Param("orderNumber")String orderNumber);
/**
* 多對多查詢
* @param orderNumber 訂單編號
* @return
*/
Order queryOrderAndUserAndOrderDetailsAndItemByOrderNumber(@Param("orderNumber") String orderNumber);
/**
* 一對多查詢
* @param orderNumber 訂單編號
* @return
*/
Order queryOrderAndUserAndOrderDetailsByOrderNumber(@Param("orderNumber") String orderNumber);
/**
* 一對一查詢
* @param orderNumber 訂單編號
* @return
*/
Order queryOrderAndUserByOrderNumber(@Param("orderNumber") String orderNumber);
}
3.5 在resources目錄下建立mapper對映檔案
Mapper對映檔案中定義了操作資料庫的sql,每一個sql都被包含在一個statement中。對映檔案是mybatis操作資料庫的核心。
編寫UserMapper對映檔案
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--
mapper標籤:配置各類宣告
namespace:名稱空間,由於對映檔案可能有多個,為了防止crud語句的唯一標識被重複,需要設定空間名稱。
-->
<mapper namespace="com.demon.mapper.UserMapper">
<!--
select:查詢的statement(宣告),用來編寫查詢語句
id:語句的唯一標識,與mapper介面中的方法名保持一致
resultType:配置返回的結果集型別,如沒配置typeAliases則需要編寫全路徑名
parameterType:傳遞的引數型別,可以省略
-->
<!--通過Order延遲載入User-->
<resultMap id="orderUserLazyResultMap" type="Order">
<!--
select屬性:呼叫指定sql語句來執行延遲載入
column屬性:延遲載入的sql語句中所需的引數
-->
<association property="user" javaType="User" select="queryUserByIdOfOrder" column="{id=user_id}"/>
</resultMap>
<!--通過訂單編號查詢訂單-->
<select id="queryOrderUserLazy" resultMap="orderUserLazyResultMap">
select * from tb_order where tb_order.order_number = ${orderNumber}
</select>
<select id="queryUserByIdOfOrder" resultType="User">
select * from tb_user WHERE tb_user.id = ${id}
</select>
<!--多對多查詢-->
<resultMap id="orderAndUserAndOrderDetailsAndItemResultMap" type="Order" autoMapping="true">
<id column="id" property="id"/>
<association property="user" javaType="User" autoMapping="true">
<id column="uid" property="id"/>
</association>
<collection property="orderdetails" javaType="List" ofType="Orderdetail" autoMapping="true">
<id column="detail_id" property="id"/>
<!--訂單詳情和商品的一對一的關係-->
<association property="item" javaType="Item" autoMapping="true">
<id column="iid" property="id"/>
</association>
</collection>
</resultMap>
<select id="queryOrderAndUserAndOrderDetailsAndItemByOrderNumber" resultMap="orderAndUserAndOrderDetailsAndItemResultMap">
SELECT
*, u.id as uid, od.id as detail_id, i.id as iid
FROM
tb_order o
INNER JOIN tb_user u ON o.user_id = u.id
INNER JOIN tb_orderdetail od ON o.id = od.order_id
INNER JOIN tb_item i ON od.item_id = i.id
WHERE
o.order_number = ${orderNumber}
</select>
<!--一對多查詢-->
<resultMap id="orderAndUserAndOrderDetailsResultMap" type="Order" autoMapping="true">
<id column="id" property="id"/>
<association property="user" javaType="User" autoMapping="true">
<id column="uid" property="id"/>
</association>
<collection property="orderdetails" javaType="List" ofType="Orderdetail" autoMapping="true">
<id column="detail_id" property="id"/>
</collection>
</resultMap>
<select id="queryOrderAndUserAndOrderDetailsByOrderNumber" resultMap="orderAndUserAndOrderDetailsResultMap">
SELECT
*, u.id as uid, od.id as detail_id
FROM
tb_order o
INNER JOIN tb_user u ON o.user_id = u.id
INNER JOIN tb_orderdetail od ON o.id = od.order_id
WHERE
o.order_number = ${orderNumber}
</select>
<!--
配置自定義結果集
id屬性:自定義結果集的唯一標識
type屬性:結果集型別
autoMapping屬性:多表查詢時,必須設定為true,Order物件和tb_order表的屬性和欄位才會進行自動對映
-->
<resultMap id="orderAndUserResultMap" type="Order" autoMapping="true">
<!--配置Order的主鍵對映-->
<id column="id" property="id"/>
<!--
association標籤:用於對一的對映
property屬性:類中的關聯屬性的名稱
javaType屬性:屬性對應的型別
autoMapping屬性:autoMapping屬性:多表查詢時,必須設定為true,User物件和tb_user表的屬性和欄位才會進行自動對映
-->
<association property="user" javaType="User" autoMapping="true">
<id column="uid" property="id"/>
</association>
</resultMap>
<!--一對一查詢-->
<select id="queryOrderAndUserByOrderNumber" resultMap="orderAndUserResultMap">
SELECT
*, u.id as uid
FROM
tb_order o
INNER JOIN tb_user u ON o.user_id = u.id
WHERE
o.order_number = ${orderNumber}
</select>
</mapper>
3.5.1 CRUD標籤
1、Select
Select標籤:用來編寫查詢語句的statement
id屬性:唯一標識
resultType:查詢語句返回的結果集型別
parameterType:引數型別,使用動態代理之後,需要和mapper介面中的引數型別一致,可以省略。
<!--
select:編寫查詢語句
id:語句的唯一標識
resultType:配置返回的結果集型別
parameterType:插入語句的引數型別,可以省略
-->
<select id="queryUserById" resultType="User">
select *,user_name as userName from tb_user where id = #{id}
</select>
2、Insert
Insert標籤:編寫新增語句的statement
insert:編寫插入語句
id:插入語句的唯一標識
parameterType:插入語句的引數型別,使用動態代理之後,需要和mapper介面中的引數型別一致,可以省略。
useGeneratedKeys:開啟主鍵自增回顯,將自增長的主鍵值回顯到形參中(即封裝到User物件中)
keyColumn:資料庫中主鍵的欄位名稱
keyProperty:pojo中主鍵對應的屬性
<!--
insert:編寫插入語句
id:插入語句的唯一標識
parameterType:插入語句的引數型別,可以省略。
useGeneratedKeys:開啟主鍵自增回顯,將自增長的主鍵值回顯到形參中(即封裝到User物件中)
keyColumn:資料庫中主鍵的欄位名稱
keyProperty:pojo中主鍵對應的屬性
-->
<insert id="insertUser" useGeneratedKeys="true" keyColumn="id" keyProperty="id">
INSERT INTO tb_user (
user_name,
password,
name,
age,
sex,
birthday,
created,
updated
)
VALUES
(
#{userName},
#{password},
#{name},
#{age},
#{sex},
#{birthday},
now(),
now()
);
</insert>
3、Update
Update標籤:編寫更新語句的statement
id:語句的唯一標識
parameterType:語句的引數型別,使用動態代理之後,需要和mapper介面中的引數型別一致,可以省略。
<!--
update:更新的statement,用來編寫更新語句
id:語句的唯一標識
parameterType:語句的引數,可以省略
-->
<update id="updateUser">
UPDATE tb_user
SET user_name = #{userName},
password = #{password},
name = #{name},
age = #{age},
sex = #{sex},
birthday = #{birthday},
updated = now()
WHERE
(id = #{id});
</update>
4、Delete
delete標籤:編寫刪除語句的statement
id:語句的唯一標識
parameterType:語句的引數型別,使用動態代理之後,需要和mapper介面中的引數型別一致,可以省略。
<!--
delete:刪除的statement,用來編寫刪除語句
id:語句的唯一標識
parameterType:語句的引數,可以省略
-->
<delete id="deleteUserById">
delete from tb_user where id = #{id};
</delete>
5、引數問題
當mapper介面要傳遞多個引數時,有兩種傳遞引數的方法:
- 預設規則獲取引數{arg0,arg1,param1,param2}
- 使用@Param註解指定引數名
方法一:
<select id="login" resultType="User">
select * from tb_user where user_name = #{arg0} and password = #{arg1}
</select>
方法二:
/**
* 使用者登陸
* @param userName
* @return
*/
User login(@Param("userName") String userName,@Param("password") String password);
<!--
通過引數的固定名稱傳遞引數:param1,param2,param3依次類推
-->
<select id="login" resultType="User">
select * from tb_user where user_name = #{userName} and password = #{password}
</select>
#{}和${}的區別總結:
#{}:
- 是預編譯
- 編譯成佔位符
- 可以防止sql注入
- 自動判斷資料型別
- 一個引數時,可以使用任意引數名稱進行接收
${}:
- 非預編譯
- sql的直接拼接
- 不能防止sql注入
- 需要判斷資料型別,如果是字串,需要手動新增引號。
- 一個引數時,引數名稱必須是value,才能接收引數。
6、resultMap
- resultMap標籤的作用:自定義結果集,自行設定結果集的封裝方式
- id屬性:resultMap標籤的唯一標識,不能重複,一般是用來被引用的
- type屬性:結果集的封裝型別
- autoMapping屬性:操作單表時,不配置預設為true,如果pojo物件中的屬性名稱和表中欄位名稱相同,則自動對映。
3.6 編寫測試類
public class UserMapperTest {
private UserMapper userMapper;
@Before
public void setUp() throws Exception {
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//將openSession方法中傳遞true,可以設定自動提交事務,就不需要使用sqlSession.commit方法提交事務了
SqlSession sqlSession = sqlSessionFactory.openSession(true);
userMapper = sqlSession.getMapper(UserMapper.class);
}
@Test
public void queryOrderUserLazy() {
Order order = userMapper.queryOrderUserLazy("20140921001");
System.out.println(JSON.toJSONString(order.getUser()));
}
@Test
public void queryOrderAndUserAndOrderDetailsAndItemByOrderNumber() {
Order order = userMapper.queryOrderAndUserAndOrderDetailsAndItemByOrderNumber("20140921001");
System.out.println(JSON.toJSONString(order));
}
@Test
public void queryOrderAndUserAndOrderDetailsByOrderNumber() {
Order order = userMapper.queryOrderAndUserAndOrderDetailsByOrderNumber("20140921001");
System.out.println(JSON.toJSONString(order));
}
@Test
public void queryOrderAndUserByOrderNumber() {
Order order = userMapper.queryOrderAndUserByOrderNumber("20140921003");
System.out.println(JSON.toJSONString(order));
}
}
四、延遲載入
- 延遲載入:就是在需要用到資料時才進行載入,不需要用到資料時就不載入資料。延遲載入也稱懶載入.
- 好處:先從單表查詢,需要時再從關聯表去關聯查詢,大大提高資料庫效能,因為查詢單表要比關聯查詢多張錶速度要快。。
- 壞處:因為只有當需要用到資料時,才會進行資料庫查詢,這樣在大批量資料查詢時,因為查詢工作也要消耗時間,所以可能造成使用者等待時間變長,造成使用者體驗下降。
4.1 需求
延遲載入需求:通過訂單編號20140921001查詢order並延遲載入user
如果通過訂單編號查詢order並且查詢user資訊,在正常情況下的查詢語句應該是
如果改成延遲載入,也就意味著,先查詢order,等需要的時候再去查詢user,那就相當於將上面的一條語句變成了兩條語句:
1、通過訂單編號查詢order
2、通過查詢出來的order中的user_id查詢user
4.2 編寫介面方法
/**
* 通過訂單編號20140921001查詢order並延遲載入user
* @param orderNumber 訂單編號
* @return
*/
Order queryOrderUserLazy(@Param("orderNumber")String orderNumber);
4.3 編寫mapper對映
<!--通過Order延遲載入User-->
<resultMap id="orderUserLazyResultMap" type="Order">
<!--
select屬性:呼叫指定sql語句來執行延遲載入
column屬性:延遲載入的sql語句中所需的引數
-->
<association property="user" javaType="User" select="queryUserByIdOfOrder" column="{id=user_id}"/>
</resultMap>
<!--通過訂單編號查詢訂單-->
<select id="queryOrderUserLazy" resultMap="orderUserLazyResultMap">
select * from tb_order where tb_order.order_number = ${orderNumber}
</select>
<select id="queryUserByIdOfOrder" resultType="User">
select * from tb_user WHERE tb_user.id = ${id}
</select>
4.4 測試
@Test
public void queryOrderUserLazy() {
Order order = userMapper.queryOrderUserLazy("20140921001");
System.out.println(JSON.toJSONString(order.getUser()));
}
測試結果:發現雖然已經將語句進行了分開查詢,但是並沒有延遲載入,還是一起查詢了。
原因:沒有開啟延遲載入的開關
4.5 開啟延遲載入
<settings>
<!--全域性開啟延遲載入,開啟時所有關聯物件都會延遲載入-->
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
如果要使用Debug來演示延遲載入,需要設定如下:將自動toString的選項去掉就行。