Java框架-mybatis延遲載入、快取和註解開發
1. 延遲載入
1.1 概念
- 在需要用到資料時才進行載入,不需要用到資料時就不載入資料。也稱作懶載入
- 好處:先從單表查詢,需要時再從關聯表去關聯查詢,大大提高資料庫效能
- 缺點:在大批量資料查詢時,由於查詢會耗時,可能導致使用者等待時間變長,影響使用者體驗
其中:mybatis的association、collection標籤具備延遲載入功能
及時載入:一次載入所有資料。
1.2 一對一實現延時載入
以賬戶表和使用者表一對一關係為例
需求:查詢賬戶時只查詢到賬戶,只有使用賬戶查詢對應的使用者時才查詢使用者
1.2.1 建立專案和建立實體類
按照“mybatis-CERD操作”裡的架構建立專案,新增pom依賴、新增SqlMapConfig.xml、log4j.properties、jdbc.properties配置檔案。引入“mybatis-多表查詢”中的User類、Account類
1.2.2 建立IUserDao介面和IUserDao.xml
IUserDao介面
public interface IUserDao {
/*根據使用者id查詢使用者,一個賬戶只對應一個使用者*/
User findUserById(int id);
}
IUserDao.xml對映檔案
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namesppace名稱空間,用於定義是哪個類的對映檔案,這裡需要寫對映介面的類全名-->
<mapper namespace="com.azure.dao.IUserDao">
<select id="findUserById" parameterType="int" resultType="user">
select * from user where id=#{id}
</select>
</mapper>
1.2.3 建立IAccount介面和對映配置檔案
在一對一關係中使用延遲載入,需要利用到association標籤的select屬性和column屬性呼叫延遲載入的方法
- select屬性:格式:介面類全名.方法名,用於設定需要查詢一對一關係對應的表資料時執行哪個介面的哪個方法
- column屬性:如果使用select標籤,則column用於將指定的資料傳遞給select方法作為引數。如果引數有多個,則使用集合形式,如column="{引數1=accountsMap的某column欄位,引數2=accountsMap的某column欄位,…}"
- fetchType=“lazy”:區域性開啟延遲載入,會覆蓋全域性延遲載入設定。預設不開啟。如果全域性延遲載入已設定,這裡可省略不設定
<?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="com.azure.dao.IAccountDao">
<!--返回集的型別是Account型別,裡面需封裝account表資料和User物件-->
<resultMap id="accountsMap" type="account">
<!--先建立account物件屬性與account表字段的對映關係-->
<id property="accountId" column="accountId"></id>
<result property="uid" column="uid"></result>
<result property="money" column="money"></result>
<!--建立user物件與User表字段的對映關係-->
<!--
使用association標籤標示一對一關係
- property:一對一關係的對應物件屬性名(本例是指賬戶物件對應的user物件屬性);
- JavaType:對應物件屬性型別
- column:外來鍵欄位
配置延遲載入
- select:格式:介面類全名.方法名,用於設定需要查詢一對一關係對應的表資料時執行哪個介面的哪個方法
- column:如果使用select標籤,則column用於將指定的資料傳遞給select方法作為引數
- fetchType="lazy":區域性開啟延遲載入,會覆蓋全域性延遲載入設定。預設不開啟。如果全域性延遲載入已設定,這裡可省略不設定
-->
<association property="user" javaType="user" column="uid" select="com.azure.dao.IUserDao.findUserById" fetchType="lazy"></association>
</resultMap>
<!--使用resultMap明確一對一關係-->
<select id="findAll" resultMap="accountsMap">
select * from account
</select>
</mapper>
1.2.4 測試類
public class AccountDaoTest {
private InputStream is;
private SqlSession session;
private IAccountDao accountDao;
/**
* 每次執行Junit前都會執行
* @throws IOException
*/
@Before
public void before() throws IOException {
is = Resources.getResourceAsStream("SqlMapConfig.xml");
session = new SqlSessionFactoryBuilder().build(is).openSession();
accountDao = session.getMapper(IAccountDao.class);
}
/**
* 每次執行Junit後都會執行,提交事務和關閉資源
* @throws IOException
*/
@After
public void close() throws IOException {
//手動提交事務
session.commit();
//關閉資源
session.close();
is.close();
}
@Test
public void findAll() {
List<Account> list = accountDao.findAll();
for (Account account : list) {
System.out.println(account.getAccountId() +"--" + account.getMoney());
//使用賬戶查詢使用者資訊,此時才會查詢使用者表,實現延遲載入
System.out.println(account.getUser().getUsername());
}
}
}
注意:如果測試類直接列印list,由於Account類中包含User物件,此時系統會自動查詢user,無法顯示延遲載入的效果。實際上是有實現延遲載入。
1.2.5 延遲載入支援的開啟
1.2.5.1 使用延遲載入全域性開關進行開啟
開啟方式:在主配置檔案中設定settings標籤,將lazyLoadingEnabled的屬性值設定為true
1.2.5.2 使用區域性延遲載入
開啟方式:在dao對映配置檔案的<association>
或<collection>
中設定fetchType屬性值為lazy
1.2.5.3 兩種延遲載入方式的取捨
一般只要開啟全域性延遲載入即可。如果只是區域性需要延遲載入,那可以只配區域性延遲載入。
1.3 一對多實現延遲載入
以User與Account為一對多關係為例
需求:查詢使用者時候,只查詢使用者資訊; 使用使用者的賬戶資訊時候才查詢賬戶資訊
1.3.1 dao介面
1.3.1.1 IAccountDao介面
/*根據uid獲得賬戶*/
List<Account> findByUid(int uid);
1.3.1.2 IUserDao介面
/*查詢所有使用者*/
List<User> findAll();
1.3.2 dao對映檔案
1.3.2.1 IAccountDao.xml
<!--根據使用者id查詢賬戶資訊-->
<select id="findByUid" resultType="Account">
select * from account where UID=#{uid}
</select>
1.3.2.2 IUserDao.xml
在一對多關係中使用延遲載入,需要利用到collection標籤的select屬性和column屬性呼叫延遲載入的方法
- select屬性:格式:介面類全名.方法名,用於設定需要查詢一對多關係對應的表資料時執行哪個介面的哪個方法
- column屬性:如果使用select標籤,則column用於將指定的資料傳遞給select方法作為引數。如果引數有多個,則使用集合形式,如column="{引數1=accountsMap的某column欄位,引數2=accountsMap的某column欄位,…}"
- fetchType=“lazy”:區域性開啟延遲載入,會覆蓋全域性延遲載入設定。預設不開啟。如果全域性延遲載入已設定,這裡可省略不設定
<resultMap id="userMap" type="user">
<id property="id" column="id"></id>
<result property="username" column="username"></result>
<result property="birthday" column="birthday"></result>
<result property="sex" column="sex"></result>
<result property="address" column="address"></result>
<!--
select:用於指定延遲載入的資料進行呼叫的方法
column:在select標籤存在的情況下,column用於傳遞方法所需引數,引數是userMap中的column值。如果引數有多個,採用集合方式
-->
<collection property="accounts" ofType="account" column="id" select="com.azure.dao.IAccountDao.findByUid" fetchType="lazy"></collection>
</resultMap>
<select id="findAll" resultMap="userMap">
select * from user
</select>
1.3.3 測試類
@Test
public void findAll() {
List<User> list = userDao.findAll();
for (User user : list) {
System.out.println(user.getId() +"--"+ user.getUsername());
//利用使用者獲得賬戶資訊,此時才會載入對應的方法查詢,實現延遲載入
System.out.println(user.getAccounts());
}
}
注意:如果測試類直接列印list,由於User類中包含List<Account>
物件,此時系統會自動查詢account表,無法顯示延遲載入的效果。實際上是有實現延遲載入。
2. mybatis快取機制
Mybatis 提供了快取策略,通過快取策略來減少資料庫的查詢次數,從而提高效能。Mybatis 中快取分為一級快取,二級快取。
2.1 一級、二級快取
-
一級快取
- 基於SqlSession的快取,不能跨sqlSession;
- mybatis自動維護,無法認為影響哪些資料會存入一級快取;
- 關閉sqlSession後一級快取會失效;
- 效果:第一次查詢會到資料庫查詢,然後將查詢結果儲存到一級快取中。之後的查詢會先到快取中查詢,找到就不查詢資料庫,找不到再到資料庫查詢。
-
二級快取
-
基於對映檔案的快取,快取範圍比一級快取更大。不同的sqlsession可以操作同一個 Mapper 對映的 sql 語句、訪問二級快取的內容。可以跨sqlSession
-
哪些資料放入二級快取需要自行指定。一般存入二級快取的資料不會經常修改;
-
步驟:
-
開啟mybatis的二級快取:在主配置檔案中的settings中配置cacheEnabled屬性為true(預設已經開啟)
<settings> <!--配置二級快取,預設開啟,可選配置--> <setting name="cacheEnabled" value="true"></setting> </settings>
-
哪些對映檔案中的SQL查詢的結果需要放入二級快取,需要在對映檔案中配置
<cache/>
,並在需要使用二級快取的方法中配置userCache=true
-
實體類實現可序列化介面(Serializable)
-
-
2.2 Redis和Mybatis的快取使用場景
Redis可以叢集快取;Mybatis只適合單機快取,且快取不會持久化。故可以使用Mybatis操作redis
3. Mybatis註解開發
3.1 註解說明
@Insert:實現新增
@Update:實現更新
@Delete:實現刪除
@Select:實現查詢
@Result:實現結果集封裝
@Results:可以與@Result 一起使用,封裝多個結果集
@One:實現一對一結果集封裝
@Many:實現一對多結果集封裝
@Param 當方法引數有多個時候,建立sql語句中的佔位符引數值與方法引數的對映關係。
3.2使用註解實現CRUD操作
以IUserdao實現CRUD操作為例
3.2.1 dao介面
- 直接使用註釋配置sql語句
public interface IUserDao {
/*實現單表查詢功能*/
@Select("select * from user where id=#{id}")
User findById(int id);
/*實現修改功能,裡面的引數需要對應實體類的屬性,mybatis會自動將引數賦值到引數中*/
@Update("update user set username=#{username},birthday=#{birthday},sex=#{sex},address=#{address} where id=#{id}")
void update(User user);
/*實現新增功能,mybatis會自動封裝User*/
@Insert("insert into user values (null,#{username},#{birthday},#{sex},#{address})")
void save(User user);
/*實現刪除功能*/
@Delete("delete from user where id=#{id}")
void delete(int id);
3.2.2 測試類
public class UserDaoTest {
private InputStream is;
private SqlSession session;
private IUserDao userDao;
/**
* 每次執行Junit前都會執行
* @throws IOException
*/
@Before
public void before() throws IOException {
is = Resources.getResourceAsStream("SqlMapConfig.xml");
session = new SqlSessionFactoryBuilder().build(is).openSession();
userDao = session.getMapper(IUserDao.class);
}
/**
* 每次執行Junit後都會執行,提交事務和關閉資源
* @throws IOException
*/
@After
public void close() throws IOException {
//手動提交事務
session.commit();
//關閉資源
session.close();
is.close();
}
@Test
public void find() {
User user = userDao.findById(48);
System.out.println(user);
}
@Test
public void update(){
User user = new User();
user.setId(51);
user.setUsername("大狗");
user.setBirthday(new Date());
user.setSex("男");
user.setAddress("北極");
userDao.update(user);
}
@Test
public void insert(){
User user = new User();
user.setUsername("大狗");
user.setBirthday(new Date());
user.setSex("男");
user.setAddress("北極");
userDao.save(user);
}
@Test
public void delete() {
userDao.delete(51);
}
}
3.2.3 多引數的CRUD操作
-
sql語句中需要傳遞兩個及以上引數時,需要使用多引數CRUD操作
-
以查詢為例,其餘操作的寫法與此雷同
/*多引數實現單表查詢功能*/
//報錯org.apache.ibatis.exceptions.PersistenceException:
// Parameter 'start' not found. Available parameters are [arg1, arg0, param1, param2]
@Select("select * from user limit #{start},#{length}")
List<User> findByPage (int start, int length);
上述程式碼報錯的原因:程式碼沒有定義start和length,mybatis不會識別出#{start}是要傳入start還是length,所以要指定佔位符要傳入哪個引數
正確寫法1
雖然程式碼沒有定義start和length,但是根據報錯可知,mybatis有預設的引數定義,arg0、arg1…,表明第一個引數、第二個引數…那麼可以通過這種已定義的引數名進行引數值傳入
/*多引數解決方法1*/
@Select("select * from user limit #{arg0},#{arg1}")
List<User> findByPage (int start, int length);
正確寫法2
既然原來的程式碼報錯的原因是沒有定義引數名,如果我不改sql語句,如何實現功能。mybatis可以通過註解@Param
定義引數,注意:註解定義的引數名要與sql語句佔位符的名稱保持一致
/*多引數解決方法2,使用註解@Param定義方法引數
* 注意,註解定義的引數名要與sql語句佔位符的名稱保持一致*/
@Select("select * from user limit #{start},#{length}")
List<User> findByPage2 (@Param("start") int start,@Param("length") int length);
3.3 註解實現一對一對映及延遲載入
以Account與User的一對一關係為例
3.3.1 dao層介面
3.3.1.1 IUserDao
public interface IUserDao {
/*實現查詢使用者功能*/
@Select("select * from user where id=#{id}")
User findById(int id);
}
3.3.1.2 IAccountDao
配置一對一關係的註解標籤
public interface IAccountDao {
/**
* 使用註釋方法實現一對一和一對多關係
* @Results 建立多個查詢列與物件屬性的對映關係,格式:@Results({@Result(...),@Result(...),...})。如果查詢列只有一個,大括號可以省略
* @Result 建立每一個查詢列與物件屬性的對映關係
* - id 標記主鍵欄位,預設為false.如果是主鍵欄位,手動設定為true
* - property 物件屬性
* - column 物件屬性對應的查詢列
* - javaType 物件屬性型別,注意寫法:類名.class。與配置檔案寫法不同。可以省略
* - one 此屬性表示一對一關係
* @One one屬性的值的型別,就是一個註解型別,表示一對一
* - select 延遲載入查詢。對應使用者介面中,根據使用者id查詢使用者的方法,格式:介面類全名.方法名。
* - fetchType 是否開啟延遲載入支援
*/
@Select("select * from account")
@Results({
@Result(id = true,property = "accountId",column = "accountId"),
@Result(property = "uid",column = "uid"),
@Result(property = "money",column = "money"),
//配置一對一關係
@Result(property = "user",column = "uid",javaType = User.class,
one = @One(select = "com.azure.dao.IUserDao.findById",fetchType= FetchType.LAZY))
})
List<Account> findAll();
}
注意事項
- 註解寫法與配置檔案寫法有細微的差別,必須要留意寫法的不同。例如,註解寫法只有javaType屬性,而配置檔案有javaType和ofType兩種屬性。等等。
3.3.2 小結
3.4 註解實現一對多對映及延遲載入
以使用者與賬戶一對多為例
3.4.1 dao介面
3.4.1.1 IAccountDao
/*
根據使用者id查詢賬戶
*/
@Select("select * from account where uid=#{uid}")
List<Account> findByUserId(int uid);
3.4.1.2 IUserDao
配置一對多關係
/**
* 使用註釋方法實現一對多關係
* @Results 建立多個查詢列與物件屬性的對映關係,格式:@Results({@Result(...),@Result(...),...})。如果查詢列只有一個,大括號可以省略
* @Result 建立每一個查詢列與物件屬性的對映關係
* - id 標記主鍵欄位,預設為false.如果是主鍵欄位,手動設定為true
* - property 物件屬性
* - column 物件屬性對應的查詢列
* - javaType 物件屬性型別。與配置檔案寫法不同,沒有ofType屬性。且屬性型別是List,javaType的值為List.class,而不是User!可以省略
* - many 此屬性表示一對多關係
* @Many many屬性的值的型別,就是一個註解型別,表示一對多
* - select 延遲載入查詢。對應使用者介面中,根據使用者id查詢使用者的方法,格式:介面類全名.方法名。
* - fetchType 是否開啟延遲載入支援
*/
@Select("select * from user")
@Results({
@Result(id = true,property = "id",column = "id"),
@Result(property = "username",column = "username"),
@Result(property = "birthday",column = "birthday"),
@Result(property = "sex",column = "sex"),
@Result(property = "address",column = "address"),
//配置一對多關係
@Result(property = "accounts",column = "id",javaType = List.class,
many = @Many(select = "com.azure.dao.IAccountDao.findByUserId",fetchType = FetchType.LAZY))
})
List<User> findAllUsers();
疑問
為何配置一對一關係中,@Result(property = "user",column = "uid",javaType = User.classs,one = @One(...)
,column的值是uid?
解答:
1、column是傳入給@One中方法的引數,而One的方法是findById,所需要的引數是id。
2、但是要注意,findById查詢的是user表,user表自然有id列,而column是當前account表的查詢列,account表只有accountId、uid、money三個查詢列,沒有id列。如果此時column的值寫成id,mybatis沒有從account表中找到id列,就會報錯java.lang.NullPointerException。
3、留意到user表的id和account表的uid是主外來鍵關係,可以通過uid傳入id值,所以這裡寫的是uid。
4、一對多關係的column值同理