我们在学习 MyBatis 框架时,会经常碰到一对一,一对多的使用场景。对于这样的场景, 通常我们可以用一条 SQL 进行多表查询完成任务。当然我们也可以使用关联查询,将一条 SQL 拆成两条去完成查询任务。MyBatis 提供了两个标签用于支持一对一和一对多的使用场 景,分别是和。
关联查询 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 /** 作者类 */ public class Author { private Integer id; private String name; private Integer age; private Integer sex; private String email; // 省略 getter/setter } /** 文章类 */ public class Article { private Integer id; private String title; // 一对一关系 private Author author; private String content; private Date createTime; // 省略 getter/setter }
看一下 Mapper 接口与映射文件的定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public interface ArticleDao { Article findOne(@Param("id") int id); Author findAuthor(@Param("id") int authorId); } <mapper namespace="xyz.coolblog.chapter4.dao.ArticleDao"> <resultMap id="articleResult" type="Article"> <result property="createTime" column="create_time"/> <association property="author" column="author_id" javaType="Author" select="findAuthor"/> </resultMap> <select id="findOne" resultMap="articleResult"> SELECT id, author_id, title, content, create_time FROM article WHERE id = #{id} </select> <select id="findAuthor" resultType="Author"> SELECT id, name, age, sex, email FROM author WHERE id = #{id} </select> </mapper>
测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class OneToOneTest { private SqlSessionFactory sqlSessionFactory; @Before public void prepare() throws IOException { String resource = "chapter4/mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); inputStream.close(); } @Test public void testOne2One() { SqlSession session = sqlSessionFactory.openSession(); try { ArticleDao articleDao = session.getMapper(ArticleDao.class); Article article = articleDao.findOne(1); Author author = article.getAuthor(); article.setAuthor(null); System.out.println("\narticles info:"); System.out.println(article); System.out.println("\nauthor info:"); System.out.println(author); } finally { session.close(); } } }
从上面的输出结果中可以看出,我们在调用 ArticleDao 的 findOne 方法时,MyBatis 执行了两条 SQL,完成了一对一的查询需求。
MyBatis 是如何实现关联查询的。接下里从 getNestedQueryMappingValue 方法开始 分析,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException { // 获取关联查询 id,id = 命名空间 + <association> 的 select 属性值 final String nestedQueryId = propertyMapping.getNestedQueryId(); final String property = propertyMapping.getProperty(); // 根据 nestedQueryId 获取 MappedStatement final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId); // 获取nestedQuery的参数类型 final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType(); /* * 生成关联查询语句参数对象,参数类型可能是一些包装类,Map 或是自定义的实体类, * 具体类型取决于配置信息。以上面的例子为基础,下面分析不同配置对 * 参数类型的影响: * 1. <association column="author_id"> * column 属性值仅包含列信息,参数类型为 author_id 列对应的类型, * 这里为 Integer * 2. <association column="{id=author_id, name=title}"> * column 属性值包含了属性名与列名的复合信息,MyBatis 会根据列名从 * ResultSet 中获取列数据,并将列数据设置到实体类对象的指定属性中,比如: * Author{id=1, name="MyBatis 源码分析系列文章导读", age=null, …} * 或是以键值对 <属性, 列数据> 的形式,将两者存入 Map 中。比如: * {"id": 1, "name": "MyBatis 源码分析系列文章导读"} * * 至于参数类型到底为实体类还是 Map,取决于关联查询语句的配置信息。比如: * <select id="findAuthor"> -> 参数类型为 Map * <select id="findAuthor" parameterType="Author"> * -> 参数类型为实体类 */ final Object nestedQueryParameterObject=prepareParameterForNestedQuery( rs, propertyMapping, nestedQueryParameterType, columnPrefix); Object value = null; if (nestedQueryParameterObject != null) { // 获取 BoundSql final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject); final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT, nestedBoundSql); final Class<?> targetType = propertyMapping.getJavaType(); // 检查一级缓存是否保存了关联查询结果 if (executor.isCached(nestedQuery, key)) { // 从一级缓存中获取关联查询的结果,并通过 metaResultObject // 将结果设置到相应的实体类对象中 executor.deferLoad(nestedQuery, metaResultObject, property, key, targetType); value = DEFERED; } else { // 创建结果加载器 final ResultLoader resultLoader = new ResultLoader( configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql); // 检测当前属性是否需要延迟加载 if (propertyMapping.isLazy()) { // 添加延迟加载相关的对象到 loaderMap 集合中 lazyLoader.addLoader( property, metaResultObject, resultLoader); value = DEFERED; } else { // 直接执行关联查询 value = resultLoader.loadResult(); } } } return value; }
根据 nestedQueryId 获取 MappedStatement
生成参数对象
获取 BoundSql
检测一级缓存中是否有关联查询的结果,若有,则将结果设置到实体类对象中
若一级缓存未命中,则创建结果加载器 ResultLoader
检测当前属性是否需要进行延迟加载,若需要,则添加延迟加载相关的对象到loaderMap 集合中
如不需要延迟加载,则直接通过结果加载器加载结果
getNestedQueryMappingValue方法中逻辑多是都是和延迟加载有关。除了延迟加载,以上流程中针对一级缓存的检查是十分有必要的
若缓存命中,可直接取用结果,无需再在执行关联查询SQL。
若缓存未命中,接下来就要按部就班执行延迟加载相关逻辑
延迟加载 添加延迟加载相关对象到loaderMap 集合中的逻辑,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 // -☆- ResultLoaderMap public void addLoader(String property, MetaObject metaResultObject, ResultLoader resultLoader) { // 将属性名转为大写 String upperFirst = getUppercaseFirstProperty(property); // 属性名不相等或者loaderMap包含该属性 if (!upperFirst.equalsIgnoreCase(property) && loaderMap.containsKey(upperFirst)) { throw new ExecutorException("……"); } // 创建 LoadPair,并将 <大写属性名,LoadPair 对象> 键值对添加到 loaderMap 中 loaderMap.put(upperFirst, new LoadPair(property, metaResultObject, resultLoader)); }
addLoader 方法的参数最终都传给了LoadPair,该类的load方法会在内部调用ResultLoader 的loadResult方法进行关联查询,并通过metaResultObject将查询结果设置到实体类对象中。
LoadPair 的 load 方法由谁调用呢?答案是实体类的代理对象。
实例 首先,我们需要在 MyBatis 配置文件的节点中加入或覆盖如下配置:
1 2 3 4 5 6 <!-- 开启延迟加载 --> <setting name="lazyLoadingEnabled" value="true"/> <!-- 关闭积极的加载策略 --> <setting name="aggressiveLazyLoading" value="false"/> <!-- 延迟加载的触发方法 --> <setting name="lazyLoadTriggerMethods" value="equals,hashCode"/>
测试类的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class OneToOneTest { private SqlSessionFactory sqlSessionFactory; @Before public void prepare() throws IOException {...} @Test public void testOne2One2() { SqlSession session = sqlSessionFactory.openSession(); try { ArticleDao articleDao = session.getMapper(ArticleDao.class); Article article = articleDao.findOne(1); System.out.println("\narticles info:"); System.out.println(article); System.out.println("\n 延迟加载 author 字段:"); // 通过 getter 方法触发延迟加载 Author author = article.getAuthor(); System.out.println("\narticles info:"); System.out.println(article); System.out.println("\nauthor info:"); System.out.println(author); } finally { session.close(); } } }
从上面结果中可以看出,我们在未调用 getAuthor 方法时,Article 对象中的 author 字段为 null。调用该方法后,再次输出 Article 对象,发现其 author 字段有值了,表明 author 字段的延迟加载逻辑被触发了。
既然调用 getAuthor 可以触发延迟加载,那么该方法一定被做过手脚了,不然该方法应该返回 null 才是。实际情况确实如此,MyBatis会为需要延迟加载的类生成代理类,代理逻辑会拦截实体类的方法调用。默认情况下,MyBatis会使用Javassist为实体类生成代理,代理逻辑封装在 JavassistProxyFactory 类中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 // -☆- JavassistProxyFactory 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)) { // 针对 writeReplace 方法的处理逻辑,与延迟加载无关,不分析了 } else { if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) { // 如果 aggressive 为 true,或触发方法(比如 equals, // hashCode 等)被调用,则加载所有的所有延迟加载的数据 if (aggressive || lazyLoadTriggerMethods.contains(methodName)) { lazyLoader.loadAll(); } else if (PropertyNamer.isSetter(methodName)) { final String property = PropertyNamer.methodToProperty(methodName); // 如果使用者显示调用了 setter 方法,则将相应的 // 延迟加载类从 loaderMap 中移除 lazyLoader.remove(property); // 检测使用者是否调用 getter 方法 } else if (PropertyNamer.isGetter(methodName)) { final String property = PropertyNamer.methodToProperty(methodName); // 检测该属性是否有相应的 LoadPair 对象 if (lazyLoader.hasLoader(property)) { // 执行延迟加载逻辑 lazyLoader.load(property); } } } } } // 调用被代理类的方法 return methodProxy.invoke(enhanced, args); } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } }
代理方法首先会检查 aggressive是否为true,如果不满足,再去检查lazyLoadTriggerMethods 是否包含当前方法名。这里两个条件只要一个为true,当���实体类中所有需要延迟加载
aggressive 和 lazyLoadTriggerMethods 两个变量的值取决于下面的配置。
1 2 <setting name="aggressiveLazyLoading" value="false"/> <setting name="lazyLoadTriggerMethods" value="equals,hashCode"/>
如果执行线程未进入第一个条件分支,那么紧接着,代理逻辑会检查使用者是不是调用了实体类的 setter 方法。如果调用了,就将该属性对应的 LoadPair 从loaderMap 中移除。
使用者既然手动调用setter方法,说明使用者想自定义某个属性的值。此时,延迟加载逻辑不应该再修改该属性的值,所以这里从loaderMap 中移除属性对于的LoadPair。
最后如果使用者调用的是某个属性的getter方法,且该属性配置了延迟加载,此时延迟加载逻辑就会被触发。
延迟加载逻辑是怎样实现的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 // -☆- ResultLoaderMap public boolean load(String property) throws SQLException { // 从 loaderMap 中移除 property 所对应的 LoadPair LoadPair pair = loaderMap.remove(property.toUpperCase(Locale.ENGLISH)); if (pair != null) { // 加载结果 pair.load(); return true; } return false; } // -☆- LoadPair public void load() throws SQLException { if (this.metaResultObject == null) { throw new IllegalArgumentException("metaResultObject is null"); } if (this.resultLoader == null) { throw new IllegalArgumentException("resultLoader is null"); } // 调用重载方法 this.load(null); } public void load(final Object userObject) throws SQLException { // 若 metaResultObject 和 resultLoader 为 null,则创建相关对象。 // 在当前调用情况下,两者均不为 null,条件不成立。篇幅原因,下面代码不分析了 if (this.metaResultObject == null || this.resultLoader == null) {...} // 线程安全检测 if (this.serializationCheck == null) { final ResultLoader old = this.resultLoader; // 重新创建新的 ResultLoader 和 ClosedExecutor, // ClosedExecutor 是非线程安全的 this.resultLoader = new ResultLoader(old.configuration, new ClosedExecutor(), old.mappedStatement, old.parameterObject, old.targetType, old.cacheKey, old.boundSql); } // 调用 ResultLoader 的 loadResult 方法加载结果, // 并通过 metaResultObject 设置结果到实体类对象中 this.metaResultObject.setValue(property,this.resultLoader.loadResult()); }
重点关注最后一行有效代码,ResultLoader 的 loadResult 方法逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public Object loadResult() throws SQLException { // 执行关联查询 List<Object> list = selectList(); // 抽取结果 resultObject = resultExtractor.extractObjectFromList(list, targetType); return resultObject; } private <E> List<E> selectList() throws SQLException { Executor localExecutor = executor; if (Thread.currentThread().getId() != this.creatorThreadId || localExecutor.isClosed()) { localExecutor = newExecutor(); } try { // 通过 Executor 就行查询,这个之前已经分析过了 return localExecutor.<E>query(mappedStatement, parameterObject, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER, cacheKey,boundSql); } finally { if (localExecutor != executor) { localExecutor.close(false); } } }
我们在 ResultLoader 中终于看到了执行关联查询的代码,即 selectList 方法中的 逻辑。该方法在内部通过 Executor 进行查询。