Mybatis 懶載入的使用 什麼是懶載入?懶載入的意思就是在使用的時候才去載入,不使用不去載入,相反的就叫饑餓載入或者立即載入。懶載入在Mybatis中一般是存在與聯合查詢的情況,比如查詢一個對象的同時連帶查詢相關的表對應的數據。在Mybatis中查詢可以通過ResultMap設置查詢對象返回一個 ...
Mybatis
懶載入的使用
什麼是懶載入?懶載入的意思就是在使用的時候才去載入,不使用不去載入,相反的就叫饑餓載入或者立即載入。懶載入在Mybatis
中一般是存在與聯合查詢的情況,比如查詢一個對象的同時連帶查詢相關的表對應的數據。在Mybatis
中查詢可以通過ResultMap
設置查詢對象返回一個集合屬性,也就是說像這樣的:
@Data
public class User implements Serializable {
private int id;
private int age;
private String name;
private List<Order> orderList;
}
這裡的orderList
就是一個集合,在mapper.xml
中配置如下:
<resultMap id="userMap" type="mybatis.model.User">
<id column="id" property="id"/>
<result property="age" column="age"/>
<result property="name" column="name"/>
<collection property="orderList" ofType="mybatis.model.Order" column="id" select="findByUid"/>
</resultMap>
<select id="findByUid" resultType="mybatis.model.Order">
select * from `order` where uid = #{id}
</select>
<select id="selectById" resultMap="userMap">
select * from user where id = #{id}
</select>
可以看到這裡查詢User
對象的時候還查詢了Order
列表,這個用戶關聯的訂單信息。如果只是這樣查詢那麼結果是饑餓載入:
@Test
public void testLazyLoad(){
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.selectById(1);
System.out.println(user.getName());
}
輸出結果,執行了兩個sql
語句查詢,說明查詢User
的同時也查詢了Order
09:52:56.575 [main] INFO mybatis.plugins.MyPlugin - 對方法進行增強....
==> Preparing: select * from user where id = ?
==> Parameters: 1(Integer)
<== Columns: id, age, name
<== Row: 1, 18, 靈犀
Cache Hit Ratio [mybatis.mapper.UserMapper]: 0.0
09:52:56.613 [main] INFO mybatis.plugins.MyPlugin - 對方法進行增強....
====> Preparing: select * from `order` where uid = ?
====> Parameters: 1(Integer)
<==== Columns: id, uid, order_name, price
<==== Row: 1, 1, 蘋果, 8.00
<==== Row: 3, 1, 筆記本電腦, 8000.00
<==== Total: 2
<== Total: 1
靈犀
Process finished with exit code 0
配置懶載入:
<resultMap id="userMap" type="mybatis.model.User">
<id column="id" property="id"/>
<result property="age" column="age"/>
<result property="name" column="name"/>
<collection property="orderList" ofType="mybatis.model.Order" column="id" select="findByUid" fetchType="lazy"/>
</resultMap>
這裡的collection
標簽中的fetchType
屬性可以設置為lazy
或者eager
,預設就是eager
饑餓載入,配置完之後執行:
09:56:22.649 [main] INFO mybatis.plugins.MyPlugin - 對方法進行增強....
==> Preparing: select * from user where id = ?
==> Parameters: 1(Integer)
<== Columns: id, age, name
<== Row: 1, 18, 靈犀
<== Total: 1
靈犀
可以看到只執行了查詢user
的sql
語句,而查詢訂單order
的sql
語句沒有執行,只有在使用orderList
這個屬性的時候才會去執行sql
查詢:
@Test
public void testLazyLoad(){
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.selectById(1);
System.out.println(user.getName());
// 懶載入
System.out.println(user.getOrderList());
}
輸出結果:
09:58:02.681 [main] INFO mybatis.plugins.MyPlugin - 對方法進行增強....
==> Preparing: select * from user where id = ?
==> Parameters: 1(Integer)
<== Columns: id, age, name
<== Row: 1, 18, 靈犀
<== Total: 1
靈犀
Cache Hit Ratio [mybatis.mapper.UserMapper]: 0.0
09:58:02.746 [main] INFO mybatis.plugins.MyPlugin - 對方法進行增強....
==> Preparing: select * from `order` where uid = ?
==> Parameters: 1(Integer)
<== Columns: id, uid, order_name, price
<== Row: 1, 1, 蘋果, 8.00
<== Row: 3, 1, 筆記本電腦, 8000.00
<== Total: 2
[Order(id=1, uid=1, orderName=蘋果, price=8.00), Order(id=3, uid=1, orderName=筆記本電腦, price=8000.00)]
Process finished with exit code 0
可以看到執行查詢訂單的sql
語句並且列印了訂單信息
Mybatis
懶載入原理及源碼解析
Mybatis
懶載入的原理要搞清楚的話,就需要去找到返回結果的時候看看Mybatis
是如何封裝的,找到ResultSetHandler
,因為這個介面就是專門用於結果集封裝的,預設實現為DefaultResultSetHandler
,根據查詢數據流程不難發現封裝結果集的時候調用的是handleResultSets
方法:
@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
final List<Object> multipleResults = new ArrayList<>();
int resultSetCount = 0;
// 獲取ResultSet的包裝器
ResultSetWrapper rsw = getFirstResultSet(stmt);
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
// 驗證結果數量
validateResultMapsCount(rsw, resultMapCount);
while (rsw != null && resultMapCount > resultSetCount) {
ResultMap resultMap = resultMaps.get(resultSetCount);
// 處理結果集
handleResultSet(rsw, resultMap, multipleResults, null);
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
點擊處理結果集的方法:
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
try {
if (parentMapping != null) {
// 處理每行的數據
handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
} else {
if (resultHandler == null) {
DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
multipleResults.add(defaultResultHandler.getResultList());
} else {
handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
}
}
} finally {
// issue #228 (close resultsets)
closeResultSet(rsw.getResultSet());
}
}
點擊處理每行的數據方法:
public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
// 如果存在嵌套的結果集
if (resultMap.hasNestedResultMaps()) {
// 安全行約束檢查,如果是嵌套查詢需要關閉安全行約束條件
ensureNoRowBounds();
// 檢查結果處理器是否符合嵌套查詢約束
checkResultHandler();
// 執行嵌套查詢結果集處理
handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
} else {
// 簡單的結果集分裝處理
handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
}
}
由於我們寫的這個結果是簡單結果集,所以進入handleRowValuesForSimpleResultMap
:
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
throws SQLException {
DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
ResultSet resultSet = rsw.getResultSet();
skipRows(resultSet, rowBounds);
while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
// 獲取每行的值
Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
}
}
挑重點,直接進入獲取每行值方法中:
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
final ResultLoaderMap lazyLoader = new ResultLoaderMap();
// 創建結果值
Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
final MetaObject metaObject = configuration.newMetaObject(rowValue);
boolean foundValues = this.useConstructorMappings;
if (shouldApplyAutomaticMappings(resultMap, false)) {
foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
}
foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
foundValues = lazyLoader.size() > 0 || foundValues;
rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
}
return rowValue;
}
繼續進入獲取每行結果值的方法,createResultObject
:
private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
this.useConstructorMappings = false; // reset previous mapping result
final List<Class<?>> constructorArgTypes = new ArrayList<>();
final List<Object> constructorArgs = new ArrayList<>();
// 創建結果對象 ,使用ObjectFactory 反射進行創建
Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
for (ResultMapping propertyMapping : propertyMappings) {
// issue gcode #109 && issue #149
// 檢查屬性是否是懶載入的屬性
if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
// 使用動態代理創建一個代理對象作為結果對象返回出去,預設使用javassist 進行創建
resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
break;
}
}
}
this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result
return resultObject;
}
這裡就先是通過反射創建出這個對象resultObject
,然後遍歷去檢查這些屬性是否是懶載入的,如果是那麼就通過代理工廠去創建一個代理對象,由於這裡創建的是一個返回對象,不是一個介面因此動態代理實現是通過cglib
實現的,Mybatis
這裡使用javassist
包下的代理進行創建代理對象,代理工廠預設就是JavassistProxyFactory
:
static Object crateProxy(Class<?> type, MethodHandler callback, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
ProxyFactory enhancer = new ProxyFactory();
enhancer.setSuperclass(type);
try {
type.getDeclaredMethod(WRITE_REPLACE_METHOD);
// ObjectOutputStream will call writeReplace of objects returned by writeReplace
if (LogHolder.log.isDebugEnabled()) {
LogHolder.log.debug(WRITE_REPLACE_METHOD + " method was found on bean " + type + ", make sure it returns this");
}
} catch (NoSuchMethodException e) {
enhancer.setInterfaces(new Class[] { WriteReplaceInterface.class });
} catch (SecurityException e) {
// nothing to do here
}
Object enhanced;
Class<?>[] typesArray = constructorArgTypes.toArray(new Class[constructorArgTypes.size()]);
Object[] valuesArray = constructorArgs.toArray(new Object[constructorArgs.size()]);
try {
// 創建代理對象
enhanced = enhancer.create(typesArray, valuesArray);
} catch (Exception e) {
throw new ExecutorException("Error creating lazy proxy. Cause: " + e, e);
}
((Proxy) enhanced).setHandler(callback);
return enhanced;
}
實際上這裡也是通過反射進行創建,只是在外面封裝成了ProxyFactory
這個對象,當我們調用getOrderList
方法的時候就會執行到invoke
方法中,並且判斷是否是延遲載入的,如果是那麼就會執行lazyLoader.load
方法執行延遲載入,也就是執行sql
查詢數據:
@Override
public Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable {
final String methodName = method.getName();
try {
synchronized (lazyLoader) {
if (WRITE_REPLACE_METHOD.equals(methodName)) {
Object original;
if (constructorArgTypes.isEmpty()) {
original = objectFactory.create(type);
} else {
original = objectFactory.create(type, constructorArgTypes, constructorArgs);
}
PropertyCopier.copyBeanProperties(type, enhanced, original);
if (lazyLoader.size() > 0) {
return new JavassistSerialStateHolder(original, lazyLoader.getProperties(), objectFactory, constructorArgTypes, constructorArgs);
} else {
return original;
}
} else {
if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
lazyLoader.loadAll();
} else if (PropertyNamer.isSetter(methodName)) {
final String property = PropertyNamer.methodToProperty(methodName);
lazyLoader.remove(property);
//判斷方法是否是get方法
} else if (PropertyNamer.isGetter(methodName)) {
final String property = PropertyNamer.methodToProperty(methodName);
// 判斷屬性是否是延遲載入的。如果是那麼執行載入
if (lazyLoader.hasLoader(property)) {
lazyLoader.load(property);
}
}
}
}
}
// 執行原方法
return methodProxy.invoke(enhanced, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
}
load
方法就會執行真正的查詢sql
語句,將數據賦值給User
對象,這樣就完成了真正的懶載入操作,所以Mybatis
的懶載入實際上就是利用動態代理將對象的參數封裝進行了延遲載入,當需要時再去調用真正的查詢操作並返回數據。