myabtis 一对多实现原理(自连接)
在 MyBatis 中进行 一对多映射 时,确实需要通过 resultMap 来映射父对象和子对象的关系。你提到的“为了避免父对象重复插入”,实际上是指 如何避免在查询结果中父对象重复创建。为了实现这一点,MyBatis 提供了 collection 标签,可以在映射父对象时,把多个子对象聚合到一个父对象中,从而避免重复创建父对象实例。
1. 场景和数据模型:
假设有如下数据模型:
admin_menu 表:
| mid | menuname | pid |
|---|---|---|
| 1 | 菜单管理 | 0 |
| 2 | 菜单管理 | 1 |
| 3 | 菜单管理 | 1 |
| 4 | 用户管理 | 0 |
mid是菜单的唯一标识。pid是父菜单的标识,pid = 0表示该菜单没有父菜单。
2. 目标:
我们希望通过 MyBatis 映射以下一对多关系:
- 每个父菜单可以有多个子菜单。
- 子菜单的
pid列指向父菜单的mid列。 - 查询时将父菜单和其所有子菜单一起返回。
3. SQL 查询:
首先,我们需要编写一个 SQL 查询,通过联表查询获取父菜单和子菜单。
SELECT am1.mid AS parent_mid, am1.menuname AS parent_menuname, am2.mid AS sub_mid, am2.menuname AS sub_menuname FROM admin_menu am1 LEFT JOIN admin_menu am2 ON am1.mid = am2.pid WHERE am1.menuname = '菜单管理'; 4. MyBatis 映射:
我们可以使用 resultMap 来定义如何将查询结果映射到 Java 对象。首先,我们定义父对象和子对象的 resultMap。
4.1 定义 Menu 类
假设我们有一个 Menu 类表示菜单。
public class Menu { private Integer mid; private String menuname; private List<Menu> subMenus; // 子菜单列表 // Getters and Setters public Integer getMid() { return mid; } public void setMid(Integer mid) { this.mid = mid; } public String getMenuname() { return menuname; } public void setMenuname(String menuname) { this.menuname = menuname; } public List<Menu> getSubMenus() { return subMenus; } public void setSubMenus(List<Menu> subMenus) { this.subMenus = subMenus; } } 4.2 定义 resultMap
接下来,在 MyBatis 的 XML 映射文件中,我们定义 resultMap 来处理父对象和子对象的映射。
<resultMap id="MenuMap" type="com.example.Menu"> <!-- 映射父对象字段 --> <id property="mid" column="parent_mid"/> <result property="menuname" column="parent_menuname"/> <!-- 映射子菜单集合 --> <collection property="subMenus" ofType="com.example.Menu"> <id property="mid" column="sub_mid"/> <result property="menuname" column="sub_menuname"/> </collection> </resultMap> <select id="selectMenuWithSubMenu" resultMap="MenuMap"> SELECT am1.mid AS parent_mid, am1.menuname AS parent_menuname, am2.mid AS sub_mid, am2.menuname AS sub_menuname FROM admin_menu am1 LEFT JOIN admin_menu am2 ON am1.mid = am2.pid WHERE am1.menuname = '菜单管理'; </select> 4.3 解释
id:将查询结果中的parent_mid映射到父对象的mid属性,parent_menuname映射到menuname。collection:表示一对多的关系,subMenus属性是一个List<Menu>,将查询结果中的sub_mid和sub_menuname映射到子菜单对象的属性。ofType="com.example.Menu":指定子菜单的类型。
5. 查询结果
假设执行查询后,我们得到如下的 SQL 查询结果:
| parent_mid | parent_menuname | sub_mid | sub_menuname |
|---|---|---|---|
| 1 | 菜单管理 | 2 | 菜单管理 |
| 1 | 菜单管理 | 3 | 菜单管理 |
6. MyBatis 如何处理结果
MyBatis 会将查询结果映射为 Java 对象,结果结构如下:
Menu parentMenu = new Menu(); parentMenu.setMid(1); parentMenu.setMenuname("菜单管理"); List<Menu> subMenus = new ArrayList<>(); Menu subMenu1 = new Menu(); subMenu1.setMid(2); subMenu1.setMenuname("菜单管理"); subMenus.add(subMenu1); Menu subMenu2 = new Menu(); subMenu2.setMid(3); subMenu2.setMenuname("菜单管理"); subMenus.add(subMenu2); parentMenu.setSubMenus(subMenus); 7. 避免父对象重复
- MyBatis 会合并重复的父对象,即使 SQL 查询的结果集有重复的父对象,MyBatis 会根据父对象的
mid字段,自动将多个相同的父对象合并成一个父对象,并将所有的子菜单放到父对象的subMenus集合中。 - 这意味着,即使 SQL 查询返回了两行包含相同父对象的记录,MyBatis 也不会重复创建父对象实例,而是将所有子菜单添加到同一个父菜单对象的
subMenus集合中。
8. 总结:
在 MyBatis 中,一对多映射是通过 collection 标签实现的,collection 会将 SQL 查询结果中的子对象聚合到父对象的集合属性中。MyBatis 会根据查询结果中的父对象 mid 来判断是否已经创建过父对象实例,从而避免重复创建。这样,多个子对象会共享一个父对象实例,避免了父对象的重复插入。
通过这种方式,MyBatis 可以有效地处理一对多关系,并且确保不会因为 SQL 查询的重复记录而重复创建父对象。
您说:
ChatGPT 说:
MyBatis 实现 一对多关系映射 时,能够避免父对象的重复实例化并将子对象合并到父对象的集合属性中的机制,主要依赖于 MyBatis 的 ResultMap 以及 collection 标签的特性。具体来说,MyBatis 会利用 一级缓存 和 对象的标识 来判断父对象是否已创建,从而避免重复创建父对象实例。
1. MyBatis 的一级缓存与合并机制
MyBatis 的一级缓存默认是开启的,这意味着在一个 SqlSession 中,同一条父对象会缓存到一级缓存中。当查询返回多个包含相同父对象的记录时,MyBatis 会检查一级缓存中是否已经存在这个父对象实例,如果存在,则直接复用该实例,而不是创建新的父对象。
1.1 缓存原理
-
一级缓存:MyBatis 的一级缓存是基于
SqlSession的,即在一个SqlSession的生命周期内,如果多次查询相同的对象,MyBatis 会使用缓存的对象,而不会每次都查询数据库。- 查找父对象时,MyBatis 会先检查缓存中是否已有这个父对象。如果有,直接返回缓存中的父对象实例。
- 如果父对象没有被缓存(即第一次查询时),MyBatis 会从数据库查询该对象,并将其加入缓存。
-
缓存与
collection标签结合:- 当查询结果返回多个父对象时,MyBatis 会利用
mid(或其他唯一标识)来判断该父对象是否已经存在于缓存中。 - 如果已经存在,MyBatis 会直接将新的子对象添加到父对象的
subMenus集合中,而不会创建新的父对象实例。
- 当查询结果返回多个父对象时,MyBatis 会利用
1.2 Example
考虑下面的查询:
SELECT am1.mid AS parent_mid, am1.menuname AS parent_menuname, am2.mid AS sub_mid, am2.menuname AS sub_menuname FROM admin_menu am1 LEFT JOIN admin_menu am2 ON am1.mid = am2.pid WHERE am1.menuname = '菜单管理'; 假设查询结果如下:
| parent_mid | parent_menuname | sub_mid | sub_menuname |
|---|---|---|---|
| 1 | 菜单管理 | 2 | 菜单管理 |
| 1 | 菜单管理 | 3 | 菜单管理 |
在这种情况下,即使 SQL 查询返回了两行包含相同父对象 parent_mid = 1 的记录,MyBatis 会执行以下步骤:
2. 步骤解析:
- 第一次查询:
- MyBatis 查询到第 1 行记录,
parent_mid = 1,parent_menuname = 菜单管理。 - MyBatis 创建一个新的父对象
Menu,并将其mid设置为 1,menuname设置为 "菜单管理"。 - 然后,MyBatis 查询到第 1 行的子对象
sub_mid = 2和sub_menuname = 菜单管理,并将子对象添加到父对象的subMenus集合中。
- MyBatis 查询到第 1 行记录,
- 第二次查询(重复父对象):
- MyBatis 查询到第 2 行记录,
parent_mid = 1,parent_menuname = 菜单管理。 - MyBatis 会检测到父对象
mid = 1已经存在于缓存中(一级缓存),因此不会再次创建新的父对象。 - MyBatis 会将新的子对象
sub_mid = 3和sub_menuname = 菜单管理添加到已经存在的父对象的subMenus集合中。
- MyBatis 查询到第 2 行记录,
- 最终结果:
- 最终,MyBatis 会返回一个
Menu对象,mid = 1,menuname = 菜单管理,subMenus集合中包含两个子菜单对象,sub_mid = 2和sub_mid = 3。
- 最终,MyBatis 会返回一个
3. ResultMap 和 collection 的实现原理:
MyBatis 会通过 resultMap 和 collection 来处理这些对象的映射。collection 标签会告诉 MyBatis 把查询结果中的多个子对象聚合成父对象的一个集合属性。具体实现原理如下:
3.1 父对象映射
<resultMap id="MenuMap" type="com.example.Menu"> <id property="mid" column="parent_mid"/> <result property="menuname" column="parent_menuname"/> </resultMap> id和result标签会分别映射父对象的标识符和其他属性。
3.2 子对象映射
<collection property="subMenus" ofType="com.example.Menu"> <id property="mid" column="sub_mid"/> <result property="menuname" column="sub_menuname"/> </collection> collection标签会把多个子对象通过subMenus属性聚合到父对象中,并且通过ofType指定子对象的类型(如Menu)。
3.3 合并逻辑:
- 在执行查询时,MyBatis 会逐行读取 SQL 查询的结果,并根据
resultMap来映射到对象。如果查询结果中的父对象字段相同,MyBatis 会通过mid判断是否是同一个父对象。如果是同一个父对象,就不会创建新的父对象实例,而是直接将当前行的子对象添加到父对象的subMenus集合中。
4. 避免父对象重复实例化的关键:
-
通过唯一标识符(
mid)来合并父对象:每次查询时,MyBatis 都会检查当前父对象的唯一标识符(如mid)。如果当前父对象已在缓存中,MyBatis 会直接复用缓存中的对象实例。 -
一级缓存的作用:一级缓存确保了在一次
SqlSession的生命周期内,重复查询同一个父对象时只会创建一次父对象实例。
5. MyBatis 处理一对多关系的总结:
- MyBatis 会根据
resultMap和collection标签将查询结果映射为 Java 对象。 - 当查询结果中有重复的父对象时,MyBatis 会通过父对象的唯一标识符(如
mid)来避免重复创建父对象实例。 - 子对象会被添加到父对象的集合属性(如
subMenus)中,从而实现一对多关系的映射。 - 一级缓存 是避免重复创建父对象的关键机制,它在同一个
SqlSession中缓存了查询到的父对象,避免了重复创建。
通过这种机制,MyBatis 能够高效地处理一对多的映射关系,并且在多次查询同一个父对象时避免重复创建实例。

浙公网安备 33010602011771号