在 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 能够高效地处理一对多的映射关系,并且在多次查询同一个父对象时避免重复创建实例。