mybatis递归查询_MyBatis#MyBatis映射体系-上(五)

MetaObject

首先来看一下MetaObject,在MyBatis中很多地方都是用到了MetaObjet,例如BaseExecutor、BeanWapper等,只要是对Java属性的操作基本上都会涉及到MetaObject。

那么MetaObject到底有什么作用?

首先看一下是用反射对一个Blog类的方法调用

@Test

我们发现这样写是很麻烦的,那是否有一个东西可以更简便的来对属性进行赋值等操作呢 ?

@Test

20e6230eda81d3aafebd4da4886c3c6e.png

这只是一个简单的例子,至于MetaObject来说,里面的功能超级超级强大,比如说MetaObject还支持子属性中的子属性的赋值操作

@Test

c0824a863e088bbda13e2e479d11e372.png
@Test

0bde5d156d8e3eb703519165ab561606.png
@Test

b0e1d1459b4b5a9a11da99d94f83f4e7.png
@Test

0fa95124b01e9be2cac17b499348d29e.png
@Test

6d956e0db74905888c0db2e3d0ccaf4a.png
@Test

34292af69938a6ae78e422954e778959.png

总结:

  1. 直接操作属性
  2. 操作子属性
  3. 自动创建属性对象
  4. 自动查找属性名,支持下划线转驼峰
  5. 基于索引访问数组

43c68b89e3d3ab4a2badccf53ed672b2.png

MetaObject的底层实现是BeanWapper,基本上带有Wapper的类的作用都是装饰器

4d3f8b8133687f6b240c516e2dbd239f.png
BeanWapper只能操纵当前的属性,不能操作属性中的属性

MetaObject用于解析属性的表达式,例如:labels[res].name,BeanWapper的底层使用反射MetaClass用于处理反射操作

767bf39f450755213d5f4474110a1ed7.png

MetaObject的底层是Reflector反射器

c80ed639d88536de019a5dfedc1725f6.png

MetaClass和Reflector的区别?

MetaClass可以操作属性下的子属性,例:可以获取labels[res].name的get方法
Reflector只能获取当前属性的get方法

MetaObject获取属性值流程

360638537b0c1f3041507bb4e7bce144.png

假如现在要获取某个博客下面的某一个评论的某一个评论者的名称

e6d9a4feaba120820d8284e33385f1b2.png
  1. 首先判断表达式的下面是否还有子属性,如果有子属性,就先获取当前的自属性,再将当前的属性获取子属性,即将comments[0]转换成一个新的MetaObject,MetaObject( comments )

98ac7ff0bbce402444d60a1475a187f0.png

2. 继续获得下面的属性,如果要将其封装成MetaObject,那么就需要获取其对应的值,所以同样要调用getValue,此时通过comments调用

8aee2d40d188bf2aadda9c378e35ea1a.png

3. 拿到一个新的MetaObject

6ecb315fda82c26007937e74824e21cb.png

4. 拿到评论中的User信息

fb1434eedc689fe8d3160e3e9a323555.png

5. 同理,再经过一圈获取name属性

06cd6a7de85ab712f251ba05044d875d.png

6, 此时不存在子属性,调用BeanWrapper.get()获取当前属性值,判断是否有集合索引?如果不存在则直接调用集合反射获取属性值

df8d7bfdc71fb9291a994d63d71f64ea.png

7. 如果存在集合索引,那么首先先获取集合索引(下标index),再基于索引获取集合中的值,例如:首先获取comments,得到一个List索引,在根据List获取想要得到的集合中的值comments[0]

c4a071e74b25f23fa18904dbb5e3100c.png

下面结合源码来看一下上面所示的流程

@Test

d8b0d1d60b42fec5980ba5b0f81cfd6e.png

f63a00843994784974763dea1fd18e95.png

PropertyTokenizer:属性分词器,即将表达式进行分词

例如,comments[0].user.name

9a9d32f78774ebe8920ec0ef6f51b1a8.png
name -> comments
indexedName -> comments[0]
index -> 0
children -> user.name

在获取children时,又会变成一个PropertyTokenizer,继续向下解析

27843d6316faaf0e7effe4ed464e0c1b.png
  1. 判断是否存在子属性

a91272d86b074363750fdf806940e6f3.png

5362594f8f172606d7a9829c153bbdbd.png
此时children = user.name

如果不存在子属性,例如直接查找ID

b1274f5e00bc2124132bde656cfcca92.png

9e5e5406fe99f9af95ec366467ee0a67.png

0d625fa7144c39eb4c0c8f07cc9c39f9.png

在本例中,是存在子属性的(user.name),所以需要把自属性转换成MetaObject

5bf52dbfe7acc084e3dde76f724c880b.png

bc9f1fa2c207a2f07a4ec582b2db7e5b.png

eebf5ae80c968d5d328b2364a1b23857.png

e96e6dcb78041ba92f83c33027923a8c.png

0b528e51a011450ab421c565f5728299.png

即如果获取的是一个数组,则会返回一个List

698fca805d3f53699185876558abd740.png

如果获取的是一个数组中的一个值,则会返回一个对应的对象

004bf7a02cc3e888b89fc4c37f3c6305.png

将返回的结果包装成一个MetaObject

49e405832440ada06596f62fa02e7fa2.png

可以看到,返回的MetaObject已经包装了Comment

aced84f7f52da19ab35145d36fabb7ba.png

205b18463b4ab10aa5ebc42fcdc2904a.png

紧接着继续获取子属性,此时prop.getChildren() = “user.name“,即再走一次循环

5ba03f38cb5223796a3d4ac6e9fe2b97.png

一下就是大致流程,让大家明白即可,就是一个递归过程。

根据user得到User值

8507fd13b4fc4de6ba2ad71702113bc8.png

1a8298f5fd5c19129d22d5977b2a3604.png

再次循环,查询name,最终得到值

b31757adb0136670fc05aeb585f0ac31.png

13aaf77d3ae4fd79f52b1b9f6ccd33da.png

06b268293086fcc5c21af466e2847eba.png

是不是有点蒙,大家可以试一下看着上面的流程图debug一下,多试几次就可以理解了


对于BeanWapper,我们可以模拟几个例子

public 

首先断点打到第一个get

1710834e4f8a5915223b8ec761ff235d.png

如果有坐标说明是对集合的访问,那么接下来需要做两件事情。

  1. 获取集合的List
  2. 通过坐标获取List中具体的值

15a591617bcab2cbaabd968e486d117e.png

9447e52070487bce36ce827b75050ba4.png

又回到了getValue方法

82a1207d9dd317e2132516ed2eaefdda.png

28eee354adeffb7824e203f372f668b9.png

663599251ea9721f2684580fb84db54a.png

99802e006adc521a88d1a9753f9b8391.png

最后通过下标拿到返回值,底层是list.get(index)方法获取值

38bfef026963326f5a967d8f35dd380d.png

be73a2dcb5427f94cafcea3c448ab4d5.png

对于第二个get来说,其没有index下标值,则直接通过反射拿去相关的值

5d35477f982babc863d88f176c2984d3.png

adc16c11e86557267eb87f9eb9576d68.png

3856b89cb261d9b99462c6f1e8758b24.png
通过反射调用get和set方法、

ResultMap结果集映射

ResultMap的作用就是Java对象和表格行进行转化,即由属性名和数据库中列名进行对应

f812957d2eb05399026378b55cadce86.png
即就是将JavaBean中的属性指向数据库中的每一列

在ResultMap中进行映射操作,ResultMap#type表示映射到哪个JavaBean,所谓type就是JavaBean对应的Class,type中的多个属性对应表格行的多个列。那如何表达属性和列之间的关系?--- ResultMapping(其和ResultMap为一对多关系)

5ef05886da5585fe1a751d491f396329.png

什么是ResultMapping?

<resultMap 
在resultMap中包含的部分都属于ResultMapping

ResultMapping表现形式:

7cd56ba402cf6e617f904059438b6a94.png
association:一对一,类似于上面一个评论一个评论者
collection:一对多,类似于上面一个博客,一组评论

例:符合映射

<resultMap 

例:嵌套查询,如果设置了select,那么association中配置的id和result就失效了

<resultMap 

例:外部映射,如果设置了resultMap,那么select就不起作用了,并且如果同时设置运行时会报错

<resultMap 

缺点:手动映射写起来很麻烦,所以可以采用自动映射

自动映射

当属性名和列名相同时,将会触发自动映射,将其转换成UnMappedColumnAutoMapping,若属性名和列名相同时,我们同样手动设置了两者的关系,那么就自动忽略掉自动映射,优先以手动映射为主。

f85e6cf1c6100aba4d1b70c2a58444f1.png

ResultSetMetaData可以获取列的总数、名称、类别、类型......

bb0803f76e46771d12fd2751fd3ca345.png
生成的UnMappedColumnAutoMapping类似于刚才说的ResultMapping,只不过UnMappedColumnAutoMapping没有关联字段和集合关联字段,即只有基本的转换映射操作

例子:

<mapper 

测试代码:

@Test

2e8184edd7fbcb066257fa47b0ed5589.png
只查询出resultMap中所指出的字段

开启自动映射:

<resultMap 

68596b87be801c40f8e4d29710095613.png
因为author和comments都属于复杂属性,不能跟数据库的字段对应上,所以需要设置association

总之,最后resultMap中的所有属性都会转换成 ResultMap#ResultMapping

<mapper 

81f268e3503b73460b4be5bae08eec2d.png
即通过association查询得到复杂属性

通过配置Collection可以将comments信息也查找出来

<mapper 

6d973e36b44a346b32087f0253d76572.png

接下来就开始他们底层的原理到底是什么


嵌套子查询

首先搞清楚结构关系

5e46be331f2aeee66f68e19fb212fc30.png

ee279402f9ed7034d1f325b5e236e412.png
查询评论时,同样需要查询用户,因为需要知道这个评论时谁写的

此时,如果步骤(1)的useId和步骤(2)的userId相等,就没有必要再次查询相同的userId,直接去一级缓存中查找即可

假设,现在在Comment中添加一个Blog属性,即Blog和Comments互相依赖

172247150112f7bb4d428afa44f8b407.png

Q:在触发一级缓存时,是否会产生死循环?

d702ec97c9b69245778211bfbe937006.png

例:

<mapper 

25bba3bc73d2a04c90e936ecea65b81a.png

发现并不会触发死循环,MyBatis是如何解决这种问题的?

  1. 延迟加载
  2. 缓存占位符

循环依赖流程解析

4904dba94964c3c8d8e6f73e4759b850.png

在准备之后,判断一级缓存是否命中(通过延迟加载的方式)

即使一级缓存有值,也不填充进去,直到主查询结束之前再填充进去

0c5ec2e5d865c2375b085194e963b2ca.png

如果没有命中一级缓存,判断是否需要懒加载,否则去进行实时加载

71aa6f28ab03c3531aa32092a9561be7.png

结合源码分析:

填充属性

只有手动填充时才会触发懒加载

542c3dd692876a59d7c1ff1190a55b84.png

e205d7a989291ae4c5ebaa1b0c7474bd.png

获取嵌套查询的映射值:

f127c579a2a404209d4a2d0c0ca06bf5.png

延迟加载

在主查询结束后再填充属性值

c1e4ce7e766a11aba91468b0c1768d72.png
从Cachekey中拿出缓存值,给MetaObject填充property属性

延迟加载的实现就是BaseExecutor中

e063c58e398b5e68e31dd4c328082cf0.png
DeferredLoad:判断当前这个类是否是可加载的,如果是可加载的,直接在缓存中拿出值然后setValue即可

7e51f2953d787732bb7e02fa0cfad3b8.png
当前缓存有值,且当前的值不等于当前的占位符,那就说明是可加载的

b702e0dd0036fe072f90c922b5468c9c.png

那么这些跟循环依赖有什么联系呢?

在查询时,有一个queryStack = 0

8d00537c8eb2ec87582bb9db566925ac.png

这个queryStack对应的就是当前查询栈的层级(对应图中的0、1、2)

3c20474a1c3d01aded7319a5cfe916b7.png

接下来开始调试代码:

8d9a6f18a6cec557feb7fa9c9bee512a.png

c2c6c08062819351f2f907b0210bbd23.png

6e6e7a6401fd3e1cf01abea57855ef02.png

在查询数据库前,先给缓存的key添加一个占位符

ed48b0ad403142fda25f8ffd15ee0453.png

接下来去查询数据库,然后解析结果集,在解析结果集时同时也会解析comment结果集,所以会触发comment查询操作,在getNestedQueryMappingValue中进行触发

b8e2e6d2168e2ea30286c0f22fec51ba.png

在填充comment属性时,会触发缓存操作

9c869f98f80b903770a03ba38190992c.png

此时,开始解析comment

此时comment没有缓存值,由于我们已经关闭了懒加载,所以去执行查询操作

4d373ebae25be6133a9f9dd4fa653742.png

查询操作:

ef4289786cc14d9ddb7f21dc9428a32e.png
通过Executor直接发球query操作,那么query操作就又会到达我们之前说的BaseExecutor的query操作

1e935db7ce3835a20c5415fed2286b31.png

此时的query对应的是(1)comment

058a92acea164c8fc81dbb80f191759c.png

在comment中解析Blog

此时可以看到,缓存中已经有值了

821e001503e73b4df2df2da52e9e435f.png

此时缓存中存在的值就是一个占位符

f81b96cefd4370744c49d08cce4148fa.png

之后进行延迟加载操作

6baa37adfa4f6bec9410ce89f640e79f.png

可以看到所谓的延迟加载就是将其放入延迟加载的链表中

d9a30d359cab41c92a170253370af7f5.png

4e1d427e1039108f1bcc7d9b3f980634.png

当查询返回值主查询中,再将延迟加载的值真正的放入其中

只有是占位符的情况下,才会延迟加载,否则缓存有的情况下就直接加载

5f31ea6fdd2f8e68e0ed25c5ecc90783.png

在源码中可以发现,其在查询完数据库之后,将占位符删除,再存入真正的值

31ecc7ec6f1116f83e83230de4e254ad.png

在查询完数据库之后,再将stack-1,就相当于回到了主查询

568c2d99774eb0c3dedaf487e29cc763.png

最后将延迟加载的值填充进去,完成了主流程

de84bf18aebf08471d5b23b2944fcf4b.png

此时发现有三个延迟加载操作

e4260704ef1cd6c20f182e337b2b09bb.png

67f7fd81462b1399edecc5f94c222a86.png

总结:

通过一级缓存、延迟加载、占位符解决了循环依赖问题

这就是为什么有stack这个属性,为什么一级缓存不能关闭的原因(解决循环依赖)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
可以使用MyBatis递归查询实现树形结构的查询。具体步骤如下: 1. 定义实体类 定义一个实体类,表示树的节点,包含节点id、父节点id、节点名称等属性。 2. 定义Mapper接口 定义一个Mapper接口,包含一个方法,用于查询树形结构的节点。方法的返回值为List集合,表示查询到的树形结构的节点。 3. 编写Mapper.xml配置文件 在Mapper.xml配置文件中,编写递归查询语句,通过查询语句实现树形结构的查询。具体实现方式如下: - 定义一个select语句,用于查询指定节点的所有子节点。 - 在select语句中,使用union all关键字连接多个子查询语句,实现递归查询。 - 在子查询语句中,使用with recursive关键字定义递归查询语句。 4. 调用Mapper接口 在Java代码中,调用Mapper接口的方法,获取查询到的树形结构的节点。可以通过递归遍历节点,实现树形结构的展示。 下面是一个示例代码,供参考: ``` // 定义实体类 public class TreeNode { private Integer id; private Integer parentId; private String name; // getter和setter方法 } // 定义Mapper接口 public interface TreeNodeMapper { List<TreeNode> selectTreeNodes(Integer parentId); } // 编写Mapper.xml配置文件 <select id="selectTreeNodes" parameterType="java.lang.Integer" resultType="TreeNode"> with recursive cte(id, parent_id, name) as ( select id, parent_id, name from tree_node where parent_id = #{parentId} union all select tn.id, tn.parent_id, tn.name from tree_node tn inner join cte on tn.parent_id = cte.id ) select * from cte; </select> // 调用Mapper接口 @Autowired private TreeNodeMapper treeNodeMapper; public List<TreeNode> getTreeNodes(Integer parentId) { return treeNodeMapper.selectTreeNodes(parentId); } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值