![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5SY4QjNzQmZiVzNxgTO5E2NiNWO0kzM0UjZiVDNlVGO48CX0JXZ252bj91Ztl2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
MyBatis 加載政策
什麼是延遲加載?
實際開發過程中很多時候并不需要總是在加載使用者資訊時就一定要加載他的訂單資訊。此時就是我們所說的延遲加載。
在一對多中,當有一個使用者,它有個100個訂單;在查詢使用者時,使用者下的訂單應該是,什麼時候用,什麼時候查詢;在查詢訂單時,訂單所屬的使用者資訊應該是随着訂單一起查詢出來。
延遲加載:就是在需要用到資料時才進行加載,不需要用到資料時就不加載資料。延遲加載也稱懶加載。
- 優點:先從單表查詢,需要時再從關聯表去關聯查詢,大大提高資料庫性能,因為查詢單表要比關聯查詢多張表速度要快。
- 缺點:因為隻有當需要用到資料時,才會進行資料庫查詢,這樣在大批量資料查詢時,因為查詢工作也要消耗時間,是以可能造成使用者等待時間變長,造成使用者體驗下降。
一對多,多對多:通常情況下采用延遲加載。
一對一(多對一):通常情況下采用立即加載。
注意:延遲加載是基于嵌套查詢來實作的。
實作
局部延遲加載
在
association
和
collection
标簽中都有一個
fetchType
屬性,通過修改它的值,可以修改局部的加載政策。
OrderMapper.xml
<!--
fetchType="lazy" : 延遲加載政策
fetchType="eager": 立即加載政策
-->
<resultMap id="orderMap2" type="com.renda.domain.Orders">
<id property="id" column="id"/>
<result property="ordertime" column="ordertime"/>
<result property="total" column="total"/>
<result property="uid" column="uid"/>
<association property="user" javaType="com.renda.domain.User"
select="com.renda.mapper.UserMapper.findById" column="uid" fetchType="lazy"/>
</resultMap>
<select id="findAllWithUser2" resultMap="orderMap2">
SELECT * FROM orders
</select>
設定觸發延遲加載的方法
在配置了延遲加載政策後,發現即使沒有調用關聯對象的任何方法,在調用目前對象的
equals
、
clone
、
hashCode
、
toString
方法時也會觸發關聯對象的查詢。
OrderMapper orderMapper = sqlSession.getMapper(OrderMapper.class);
List<Orders> allWithUser2 = orderMapper.findAllWithUser2();
for (Orders orders : allWithUser2) {
// 因為 Orders 的 toString 沒有開啟延遲加載
// 配置了延遲加載的關聯對象 User 還是被列印出來
System.out.println(orders);
}
可以在配置檔案
sqlMapConfig.xml
中使用
lazyLoadTriggerMethods
配置項覆寫掉上面四個方法。
<settings>
<!-- 所有 toString 方法都會觸發延遲加載 -->
<setting name="lazyLoadTriggerMethods" value="toString()"/>
</settings>
全局延遲加載
在 MyBatis 的核心配置檔案中可以使用 setting 标簽修改全局的加載政策。
<settings>
<!-- 開啟全局延遲加載功能 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 所有 toString 方法都會觸發延遲加載 -->
<setting name="lazyLoadTriggerMethods" value="toString()"/>
</settings>
配置完全局延遲加載功能後,需要加載關聯的對象就需要調用它的
toString
方法:
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> allWithOrder2 = userMapper.findAllWithOrder2();
for (User user : allWithOrder2) {
System.out.println(user);
// 需要用到使用者關聯的訂單
System.out.println(user.getOrdersList());
}
注意:局部的加載政策優先級高于全局的加載政策;
是以,在開啟全局延遲加載後,為了實作訂單能立即加載關聯的使用者資訊,就可以在局部開啟立即加載政策:
<!--
fetchType="lazy" : 延遲加載政策
fetchType="eager": 立即加載政策
-->
<resultMap id="orderMap2" type="com.renda.domain.Orders">
<id property="id" column="id"/>
<result property="ordertime" column="ordertime"/>
<result property="total" column="total"/>
<result property="uid" column="uid"/>
<association property="user" javaType="com.renda.domain.User"
select="com.renda.mapper.UserMapper.findById" column="uid" fetchType="eager"/>
</resultMap>
<select id="findAllWithUser2" resultMap="orderMap2">
SELECT * FROM orders
</select>
MyBatis 緩存
為什麼使用緩存?
當使用者頻繁查詢某些固定的資料時,第一次将這些資料從資料庫中查詢出來,儲存在緩存中。當使用者再次查詢這些資料時,不用再通過資料庫查詢,而是去緩存裡面查詢。減少網絡連接配接和資料庫查詢帶來的損耗,進而提高我們的查詢效率,減少高并發通路帶來的系統性能問題。
一句話概括:經常查詢一些不經常發生變化的資料,使用緩存來提高查詢效率。
像大多數的持久化架構一樣,MyBatis 也提供了緩存政策,通過緩存政策來減少資料庫的查詢次數,進而提高性能。 MyBatis 中緩存分為一級緩存,二級緩存。
一級緩存
介紹
一級緩存是
SqlSession
級别的緩存,是預設開啟的。
是以在參數和 SQL 完全一樣的情況下,我們使用同一個
SqlSession
對象調用一個 Mapper 方法,往往隻執行一次 SQL,因為使用
SqlSession
第一次查詢後,MyBatis 會将其放在緩存中,以後再查詢的時候,如果沒有聲明需要重新整理,并且緩存沒有逾時的情況下,
SqlSession
都會取出目前緩存的資料,而不會再次發送 SQL 到資料庫。
驗證
資源目錄
resources
下增加
log4j.properties
:
### 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{ABSOLUTE} %5p %c{1}:%L - %m%n
### direct messages to file mylog.log ###
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=C:/mylog.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n
### set log levels - for more verbose logging change 'info' to 'debug' ###
log4j.rootLogger=debug, stdout
pom.xml
中導入 log4j 的依賴,進而可以檢視底層調用 JDBC 的 log:
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.7</version>
</dependency>
編寫代碼驗證 MyBatis 中的一級緩存:
@Test
public void testOneCache() throws IOException {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 根據 id 查詢使用者資訊
// 第一次查詢,查詢的資料庫
User user1 = userMapper.findById(1);
System.out.println(user1);
// 第二次查詢,查詢的是一級緩存
User user2 = userMapper.findById(1);
System.out.println(user2);
}
可以發現,雖然在上面的代碼中查詢了兩次,但最後隻執行了一次資料庫操作,這就是 MyBatis 提供的一級緩存在起作用了。因為一級緩存的存在,導緻第二次查詢 id 為 1 的記錄時,并沒有發出 SQL 語句從資料庫中查詢資料,而是從一級緩存中查詢。
分析
一級緩存是
SqlSession
範圍的緩存,執行
SqlSession
的 C(增加)U(更新)D(删除)操作,或者調用
clearCache()
、
commit()
、
close()
方法,都會清空緩存。
- 第一次發起查詢使用者 id 為 41 的使用者資訊,先去找緩存中是否有 id 為 41 的使用者資訊,如果沒有,從資料庫查詢使用者資訊。
- 得到使用者資訊,将使用者資訊存儲到一級緩存中。
- 如果
去執行sqlSession
操作(執行插入、更新、删除),清空commit
中的一級緩存,這樣做的目的為了讓緩存中存儲的是最新的資訊,避免髒讀。SqlSession
- 第二次發起查詢使用者 id 為 41 的使用者資訊,先去找緩存中是否有 id 為 41 的使用者資訊,緩存中有,直接從緩存中擷取使用者資訊。
清除
sqlSession.clearCache()
手動清空一級緩存:
@Test
public void testOneCache() throws IOException {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 根據 id 查詢使用者資訊
// 第一次查詢,查詢的資料庫
User user1 = userMapper.findById(1);
System.out.println(user1);
// clearCache: 手動清空緩存
sqlSession.clearCache();
// 第二次查詢,查詢的是一級緩存
User user2 = userMapper.findById(1);
System.out.println(user2);
}
flushCache="true"
自動清空一級緩存:
<select id="findById" resultType="com.renda.domain.User" parameterType="int" flushCache="true">
SELECT * FROM `user` WHERE id = #{id}
</select>
二級緩存
介紹
二級緩存是
namspace
級别(跨
sqlSession
)的緩存,是預設不開啟的。
實作二級緩存的時候,MyBatis 要求傳回的 POJO 必須是可序列化的,也就是要求實作 Serializable 接口。
二級緩存的開啟需要進行配置,配置方法很簡單,隻需要在映射 XML 檔案配置
<cache/>
就可以開啟二級緩存了。
驗證
配置核心配置檔案
<settings>
...
<!--
cacheEnabled 的取值預設為 true,是以這一步可以省略不配置。
為 true 代表支援二級緩存;為 false 代表不支援二級緩存。
-->
<setting name="cacheEnabled" value="true"/>
</settings>
配置 UserMapper.xml 映射
<mapper namespace="com.renda.mapper.UserMapper">
<!-- 目前映射開啟二級緩存 -->
<cache></cache>
<!--
根據 id 查詢使用者
useCache="true" 代表目前這個 statement 是使用二級緩存
-->
<select id="findById" resultType="com.renda.domain.User" parameterType="int" useCache="true">
SELECT * FROM `user` WHERE id = #{id}
</select>
</mapper>
修改 User 實體
public class User implements Serializable {
private static final long serialVersionUID = 7898016747305399302L;
private Integer id;
private String username;
private Date birthday;
private String sex;
private String address;
private List<Orders> ordersList;
private List<Role> roleList;
...
}
測試結果
@Test
public void testTwoCache() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession1 = sqlSessionFactory.openSession();
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
// 第一次查詢
User user = userMapper1.findById(1);
System.out.println(user);
// 隻有執行 sqlSession.commit 或者 sqlSession.close,那麼一級緩存中内容才會重新整理到二級緩存
sqlSession1.close();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
// 第二次查詢
User user2 = userMapper2.findById(1);
System.out.println(user2);
sqlSession2.close();
}
分析
二級緩存是
mapper
映射級别的緩存,多個
SqlSession
去操作同一個
Mapper
映射的
sql
語句,多個
SqlSession
可以共用二級緩存,二級緩存是跨
SqlSession
的。
- 映射語句檔案中的所有
語句将會被緩存。select
- 映射語句檔案中的所有
、insert
和update
語句會重新整理緩存。delete
注意問題:MyBatis 的二級緩存因為是
namespace
級别,某個
namespace
的增删改隻會重新整理它自己的緩存,會導緻不同
namespace
緩存了别的
namespace
的舊值,是以在進行多表查詢時會産生髒讀問題。
小結
- MyBatis 的緩存,都不需要手動存儲和擷取資料,是 MyBatis 自動維護的。
- MyBatis 開啟了二級緩存後,那麼查詢順序:二級緩存 --> 一級緩存 --> 資料庫。
- 注意:因為 MyBatis 的二級緩存會存在髒讀問題,是以實際開發中會使用第三方的緩存技術
解決問題。Redis
MyBatis 注解
MyBatis 常用注解
這幾年來注解開發越來越流行,MyBatis 也可以使用注解開發方式,這樣我們就可以減少編寫 Mapper 映射檔案了。我們先圍繞一些基本的 CRUD 來學習,再學習複雜映射多表操作。
-
:實作新增,代替了@Insert
<insert></insert>
-
:實作删除,代替了@Delete
<delete></delete>
-
:實作更新,代替了@Update
<update></update>
-
:實作查詢,代替了@Select
<select></select>
-
:實作結果集封裝,代替了@Result
<result></result>
-
:可以與@Result 一起使用,封裝多個結果集,代替了@Results
<resultMap></resultMap>
-
:實作一對一結果集封裝,代替了@One
<association></association>
-
:實作一對多結果集封裝,代替了@Many
<collection></collection>
MyBatis注解的增删改查
建立 UserMapper
接口
UserMapper
public interface UserMapper {
/**
* 查詢使用者
*/
@Select("select * from user")
List<User> findAll();
/**
* 添加使用者
*/
@Insert("insert into user(username,birthday,sex,address) values(#{username},#{birthday},#{sex},#{address})")
void save(User user);
/**
* 更新使用者
*/
@Update("update user set username=#{username}, birthday=#{birthday}, sex=#{sex} where id = #{id}")
void update(User user);
/**
* 删除使用者
*/
@Delete("delete from user where id = #{id}")
void delete(Integer id);
}
編寫核心配置檔案
sqlMapConfig.xml
<?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>
<!-- 加載 properties 檔案 -->
<properties resource="jdbc.properties"></properties>
<settings>
<!-- 開啟全局延遲加載功能 -->
<setting name="lazyLoadingEnabled" value="false"/>
<!-- 所有方法都會延遲加載 -->
<setting name="lazyLoadTriggerMethods" value="toString()"/>
<!--
因為 cacheEnabled 的取值預設就為 true,是以這一步可以省略不配置。
為 true 代表開啟二級緩存;為 false 代表不開啟二級緩存。
-->
<setting name="cacheEnabled" value="true"/>
</settings>
<!-- 設定别名 -->
<typeAliases>
<package name="com.renda.domain"/>
</typeAliases>
<!-- environments: 運作環境 -->
<environments default="development">
<environment id="development">
<!-- 目前的事務事務管理器是 JDBC -->
<transactionManager type="JDBC"></transactionManager>
<!-- 資料源資訊 POOLED:使用 mybatis 的連接配接池 -->
<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 類所在的包 -->
<package name="com.renda.mapper"/>
</mappers>
</configuration>
測試代碼
public class MyBatisTest {
private SqlSessionFactory sqlSessionFactory;
private SqlSession sqlSession;
@Before
public void before() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
sqlSession = sqlSessionFactory.openSession();
}
@After
public void after(){
sqlSession.commit();
sqlSession.close();
}
/*
測試查詢方法
*/
@Test
public void testSelect() {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> all = userMapper.findAll();
for (User user : all) {
System.out.println(user);
}
}
/*
測試添加方法
*/
@Test
public void testInsert(){
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = new User();
user.setUsername("布萊爾");
user.setSex("女");
user.setBirthday(new Date());
user.setAddress("江蘇");
mapper.save(user);
}
@Test
public void testUpdate(){
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = new User();
user.setUsername("柳岩");
user.setBirthday(new Date());
user.setSex("女");
user.setId(2);
mapper.update(user);
}
@Test
public void testDelete(){
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
mapper.delete(8);
}
}
使用注解實作複雜映射開發
在映射檔案中通過配置
<resultMap>
、
<association>
、
<collection>
來實作複雜關系映射。
使用注解開發後,可以使用
@Results
、
@Result
,
@One
、
@Many
注解組合完成複雜關系的配置。
@Results
:
- 代替的是标簽
該注解中可以使用單個<resultMap>
注解,也可也使用@Result
聚合。@Result
- 使用格式 -
或@Results ({@Result () , @Result() })
@Results (@Result () )
@Result
:
- 代替了
标簽和<id>
标簽<result>
-
中屬性介紹:@Result
- 資料庫的列名,column
- 需要裝配的屬性名,property
- 需要使用的one
注解@One
,@Result ([email protected]) ()
- 需要使用的many
注解@Many
@Result ([email protected]) ()
@One
(一對一):
- 代替了
标簽,是多表查詢的關鍵,在注解中用來指定子查詢傳回單一對象。<association>
-
屬性介紹:select - 指定用來多表查詢的 SQL Mapper,使用格式 -@One
@Result(column="", property="", [email protected](select=""))
@Many
(一對多):
- 代替了
标簽,是多表查詢的關鍵,在注解中用來指定子查詢傳回對象集合。<collection>
- 使用格式 -
@Result(property="", column="", [email protected](select=""))
一對一查詢
介紹
需求:查詢一個訂單,與此同時查詢出該訂單所屬的使用者
一對一查詢語句
SELECT * FROM orders;
SELECT * FROM `user` WHERE id = #{ 訂單的 uid };
代碼實作
OrderMapper
接口
@Select("select * from orders")
@Results({
@Result(property="id",column = "id",id = true),
@Result(property="ordertime",column = "ordertime"),
@Result(property="total",column = "total"),
@Result(property="uid",column = "uid"),
@Result(property="user",column = "uid", javaType = User.class,
one = @One(select = "com.renda.mapper.UserMapper.findById",fetchType = FetchType.EAGER))
})
List<Orders> findAllWithUser();
UserMapper
接口
@Select("select * from user where id = #{uid}")
User findById(Integer uid);
測試代碼
@Test
public void testOneToOne() {
OrderMapper mapper = sqlSession.getMapper(OrderMapper.class);
List<Orders> allWithUser = mapper.findAllWithUser();
for (Orders orders : allWithUser) {
System.out.println(orders);
}
}
一對多查詢
介紹
需求:查詢一個使用者,與此同時查詢出該使用者具有的訂單
一對多查詢語句
SELECT * FROM `user`;
SELECT * FROM orders where uid = #{ 使用者 id };
代碼實作
UserMapper
接口
@Select("select * from user")
@Results({
@Result(property="id",column="id",id=true),
@Result(property="username",column="username"),
@Result(property="birthday",column="birthday"),
@Result(property="sex",column="sex"),
@Result(property="address",column="address"),
@Result(property="ordersList",column="id",javaType=List.class,
[email protected](select="com.renda.mapper.OrderMapper.findOrderByUid",fetchType=FetchType.LAZY))
})
List<User> findAllWithOrder();
OrderMapper
接口
@Select("select * from orders where uid = #{uid}")
List<Orders> findOrderByUid(Integer uid);
測試代碼
@Test
public void testOneToMany(){
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> allWithOrder = mapper.findAllWithOrder();
for (User user : allWithOrder) {
System.out.println(user);
System.out.println(user.getOrdersList());
}
}
多對多查詢
介紹
需求:查詢所有使用者,同時查詢出該使用者的所有角色
多對多查詢語句
SELECT * FROM `user`;
SELECT * FROM sys_role r INNER JOIN sys_user_role ur ON r.`id` = ur.`rid` WHERE ur.`uid` = #{ 使用者 id };
代碼實作
UserMapper
接口
@Select("select * from user")
@Results({
@Result(property="id",column="id",id=true),
@Result(property="username",column="username"),
@Result(property="birthday",column="birthday"),
@Result(property="sex",column="sex"),
@Result(property="address",column="address"),
@Result(property="roleList",column="id",javaType=List.class,
[email protected](select="com.renda.mapper.RoleMapper.findAllByUid")),
})
List<User> findAllWithRole();
RoleMapper
接口
@Select("SELECT * FROM sys_role r INNER JOIN sys_user_role ur ON ur.roleid = r.id WHERE ur.userid = #{uid}")
List<Role> findAllByUid(Integer uid);
測試代碼
@Test
public void testManyToMany(){
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> allWithRole = mapper.findAllWithRole();
for (User user : allWithRole) {
System.out.println(user);
System.out.println(user.getRoleList());
}
}
基于注解的二級緩存
配置
SqlMapConfig.xml
檔案開啟二級緩存的支援
<settings>
...
<!--
因為 cacheEnabled 的取值預設就為 true,是以這一步可以省略不配置。
為 true 代表開啟二級緩存;為 false 代表不開啟二級緩存。
-->
<setting name="cacheEnabled" value="true"/>
</settings>
在 Mapper 接口中使用注解配置二級緩存
@CacheNamespace
public interface UserMapper {
...
}
測試代碼
@Test
public void cacheTest(){
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
User user1 = userMapper1.findById(1);
System.out.println(user1);
// 關閉 sqlSession, 将内容從一級緩存重新整理到二級緩存
sqlSession1.close();
User user2 = userMapper2.findById(1);
System.out.println(user2);
sqlSession2.close();
}
注解延遲加載
不管是一對一還是一對多 ,在注解配置中都有
fetchType
的屬性
-
表示懶加載fetchType = FetchType.LAZY
-
表示立即加載fetchType = FetchType.EAGER
-
表示使用全局配置fetchType = FetchType.DEFAULT
小結
注解開發和 XML 配置優劣分析
- 注解開發開發效率更高:注解編寫和 XML 配置相比更簡單。
- XML 維護性更強:注解如果要修改,必須修改源碼,會導緻維護成本增加。
想了解更多,歡迎關注我的微信公衆号:Renda_Zhang