MyBatis-21MyBatis高级结果映射【一对多映射(2种方式)】
概述
MyBatis-20MyBatis高级结果映射【一对一映射(4种方式)】中我们介绍了4种方式实现一对一映射,本篇博文,一对多映射只有两种配置方式,都是使用collection标签进行的。
collection集合的嵌套结果映射
和association类似,集合的嵌套结果映射就是通过一次SQL查询将所有的结果查询出来,然后通过配置的结果映射,将数据映射到不同的对象中取。 在一对多的关系中,主表的一条数据会对应关联表中的多条数据,因此一般查询时会查询出多个结果,按照一对多的数据结果存储数据的时候,最终的结果会小于等于查询的总记录数。
在RBAC权限系统中,一个用户用于多个角色(在使用association是设定的特例,现在一个用户只能有一个角色),每个角色又是多个权限的集合,所以要渐进式的去实现一个SQL,查询出所有用户和用户拥有的角色,以及角色所包含的所有权限信息的两层嵌套结果。
SysUse实体类改造
为了能够存储一对多的数据,先对SysUser类进行修改
增加
public class SysUser{
// 原有属性, setter getter保持不变
/**
* 用户角色: 一个用户拥有多个角色 , 一对多
*/
private List<SysRole> roleList;
public List<SysRole> getRoleList() {
return roleList;
}
public void setRoleList(List<SysRole> roleList) {
this.roleList = roleList;
}
}
UserMapper接口增加接口方法
/**
*
*
* @Title: selectAllUserAndRoles
*
* @Description:获取所有用户及对应的角色
*
* @return
*
* @return: List<SysUser>
*/
List<SysUser> selectAllUserAndRoles();
UserMapper.xml
<!-- 简化的配置 -->
<resultMap id="userRoleListMap" extends="userMap"
type="com.artisan.mybatis.xml.domain.SysUser" >
<!-- sysRole相关的属性 property 对应实体类List<SysRole>属性名-->
<collection property="roleList" columnPrefix="sysRole_"
resultMap="roleMap">
</collection>
</resultMap>
<select id="selectAllUserAndRoles" resultMap="userRoleListMap">
SELECT
u.id,
u.user_name ,
u.user_password ,
u.user_email ,
u.user_info ,
u.create_time ,
u.head_img ,
r.id sysRole_id,
r.role_name sysRole_role_name,
r.enabled sysRole_enabled,
r.create_by sysRole_create_by,
r.create_time sysRole_create_time
FROM
sys_user u
INNER JOIN sys_user_role ur ON u.id = ur.user_id
INNER JOIN sys_role r ON ur.role_id = r.id
</select>
和一对一映射相比,一对多的userRoleListMap,就是把association改成collection, 然后将property设置为roleList,其他的属性保持不变。
collection用于配置一对多的关系,对应的属性必须是对象中的集合类型,因此这里是roleList。 另外resultMap只是为了配置数据库字段和实体属性的映射关系,因此其他都一样。 同时能存储一对多的数据结构肯定也能存储一对一的关系,所以一对一是一对多的一种特例。 collection支持的属性以及属性的作用和association完全相同。
为了简化配置,我们通过继承userMap来使用sys_user的映射关系,同时我们在UserMapper.xml中配置了roleMap的映射关系(更加合适的问题应该在RoleMapper.xml中,如果在RoleMapper.xml中,引用的时候一定要加上命名空间),因此直接饮用roleMap ,经过这两个方式的简化,最终的userRoleListMap如上
总结下:一对多配置变化的地方是 association变为collection, property由role变为了roleList
单元测试
@Test
public void selectAllUserAndRolesTest() {
logger.info("selectAllUserAndRolesTest");
// 获取SqlSession
SqlSession sqlSession = getSqlSession();
try {
// 获取UserMapper接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 调用selectAll,查询全部用户
List<SysUser> userList = userMapper.selectAllUserAndRoles();
// 结果不为空
Assert.assertNotNull(userList);
// 结果大于0
Assert.assertTrue(userList.size() > 0);
logger.info("userList总数为:" + userList.size());
for (SysUser sysUser : userList) {
logger.info("用户名:" + sysUser.getUserName());
for (SysRole sysRole : sysUser.getRoleList()) {
logger.info("\t角色名:" + sysRole.getRoleName());
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
sqlSession.close();
logger.info("sqlSession close successfully ");
}
}
我们在 Assert.assertNotNull(userList); 加上个断点,debug看下数据
点进去一个看下
从上图可以看到一个用于已经拥有两个角色,实现了一对多的查询。
接下来看下日志
2018-05-02 02:02:26,338 INFO [main] (BaseMapperTest.java:26) - sessionFactory bulit successfully
2018-05-02 02:02:26,343 INFO [main] (BaseMapperTest.java:29) - reader close successfully
2018-05-02 02:02:26,346 INFO [main] (UserMapperTest.java:1133) - selectAllUserAndRolesTest
2018-05-02 02:02:26,415 DEBUG [main] (BaseJdbcLogger.java:145) - ==> Preparing: SELECT u.id, u.user_name , u.user_password , u.user_email , u.user_info , u.create_time , u.head_img , r.id sysRole_id, r.role_name sysRole_role_name, r.enabled sysRole_enabled, r.create_by sysRole_create_by, r.create_time sysRole_create_time FROM sys_user u INNER JOIN sys_user_role ur ON u.id = ur.user_id INNER JOIN sys_role r ON ur.role_id = r.id
2018-05-02 02:02:26,538 DEBUG [main] (BaseJdbcLogger.java:145) - ==> Parameters:
2018-05-02 02:02:26,586 TRACE [main] (BaseJdbcLogger.java:151) - <== Columns: id, user_name, user_password, user_email, user_info, create_time, head_img, sysRole_id, sysRole_role_name, sysRole_enabled, sysRole_create_by, sysRole_create_time
2018-05-02 02:02:26,587 TRACE [main] (BaseJdbcLogger.java:151) - <== Row: 1, admin, 123456, aaa@qq.com.com, <<BLOB>>, 2018-04-13 21:12:47.0, <<BLOB>>, 1, 管理员, 1, 1, 2018-04-13 21:12:46.0
2018-05-02 02:02:26,598 TRACE [main] (BaseJdbcLogger.java:151) - <== Row: 1, admin, 123456, aaa@qq.com.com, <<BLOB>>, 2018-04-13 21:12:47.0, <<BLOB>>, 2, 普通用户, 1, 1, 2018-04-13 21:12:46.0
2018-05-02 02:02:26,600 TRACE [main] (BaseJdbcLogger.java:151) - <== Row: 1001, artisan, 123456, aaa@qq.com.com, <<BLOB>>, 2018-04-13 21:12:47.0, <<BLOB>>, 2, 普通用户, 1, 1, 2018-04-13 21:12:46.0
2018-05-02 02:02:26,602 DEBUG [main] (BaseJdbcLogger.java:145) - <== Total: 3
2018-05-02 02:02:26,604 INFO [main] (UserMapperTest.java:1146) - userList总数为:2
2018-05-02 02:02:26,604 INFO [main] (UserMapperTest.java:1148) - 用户名:admin
2018-05-02 02:02:26,604 INFO [main] (UserMapperTest.java:1150) - 角色名:管理员
2018-05-02 02:02:26,605 INFO [main] (UserMapperTest.java:1150) - 角色名:普通用户
2018-05-02 02:02:26,605 INFO [main] (UserMapperTest.java:1148) - 用户名:artisan
2018-05-02 02:02:26,605 INFO [main] (UserMapperTest.java:1150) - 角色名:普通用户
2018-05-02 02:02:26,608 INFO [main] (UserMapperTest.java:1157) - sqlSession close successfully
MyBatis的处理规则
通过日志可以清楚地看到,SQL执行的结果数有3条,用户输出的数量确实2条,也就是说本来查询出的3条结果经过MyBatis对collection数据的处理后,变成了2条。
从日志中,我们知道第一个用户拥有两个角色,所以转换为一对多的数据结构后就变成了两套结果,那么 MyBatis又是怎么知道要处理成这样的结果呢?
先来看MyBatis是如何要知道合并admin的两条数据的,为什么不把test这条数据也合并进去呢?
MyBatis在处理结果的时候,会判断结果是否相同,如果是相同的结果,则只会保留第一个结果。 所以这个问题的关键点就是MyBatis是如何判断结果是否相同。 最简单的情况就是在映射配置中至少有一个id标签
<id property="id" column="id" />
我们对id的理解一般是,它配置的字段为表的主键(联合主键时可以配置多个id标签),因为MyBatis的resultMap只用于配置结果如何映射,并不知道这个表具体如何。 id的唯一作用就是在嵌套的映射配置中判断数据是否相同。 .当配置id标签时,MyBatis只需要逐条比较所有数据中id标签的字段值是否相同即可。 在配置嵌套结果查询时,配置id标签提高处理效率。
这样一来,上面的查询就不难理解了,因为前两套数据的userMap部分的id相同,所以他们属于同一个用户,因子这条数据会合并到同一个用户中。
为了更加清楚的理解id的作用,我们队userMap的映射进行如下修改。
<resultMap id="userMap"
type="com.artisan.mybatis.xml.domain.SysUser">
<id property="userPassword" column="userPassword" />
<result property="userName" column="user_name" />
<result property="userPassword" column="user_password" />
<result property="userEmail" column="user_email" />
<result property="userInfo" column="user_info" />
<result property="headImg" column="head_img" jdbcType="BLOB" />
<result property="createTime" column="create_time" jdbcType="TIMESTAMP" />
</resultMap>
在测试数据中,用户的密码均为 123456
如果把密码最为id,按照上面的逻辑,3条数据就会合并为1条数据,修改后,再次执行单元测试。
2018-05-02 12:24:27,161 INFO [main] (BaseMapperTest.java:26) - sessionFactory bulit successfully
2018-05-02 12:24:27,161 INFO [main] (BaseMapperTest.java:29) - reader close successfully
2018-05-02 12:24:27,173 INFO [main] (UserMapperTest.java:1133) - selectAllUserAndRolesTest
2018-05-02 12:24:27,253 DEBUG [main] (BaseJdbcLogger.java:145) - ==> Preparing: SELECT u.id, u.user_name , u.user_password , u.user_email , u.user_info , u.create_time , u.head_img , r.id sysRole_id, r.role_name sysRole_role_name, r.enabled sysRole_enabled, r.create_by sysRole_create_by, r.create_time sysRole_create_time FROM sys_user u INNER JOIN sys_user_role ur ON u.id = ur.user_id INNER JOIN sys_role r ON ur.role_id = r.id
2018-05-02 12:24:27,383 DEBUG [main] (BaseJdbcLogger.java:145) - ==> Parameters:
2018-05-02 12:24:27,433 TRACE [main] (BaseJdbcLogger.java:151) - <== Columns: id, user_name, user_password, user_email, user_info, create_time, head_img, sysRole_id, sysRole_role_name, sysRole_enabled, sysRole_create_by, sysRole_create_time
2018-05-02 12:24:27,433 TRACE [main] (BaseJdbcLogger.java:151) - <== Row: 1, admin, 123456, aaa@qq.com.com, <<BLOB>>, 2018-04-13 21:12:47.0, <<BLOB>>, 1, 管理员, 1, 1, 2018-04-13 21:12:46.0
2018-05-02 12:24:27,443 TRACE [main] (BaseJdbcLogger.java:151) - <== Row: 1, admin, 123456, aaa@qq.com.com, <<BLOB>>, 2018-04-13 21:12:47.0, <<BLOB>>, 2, 普通用户, 1, 1, 2018-04-13 21:12:46.0
2018-05-02 12:24:27,443 TRACE [main] (BaseJdbcLogger.java:151) - <== Row: 1001, artisan, 123456, aaa@qq.com.com, <<BLOB>>, 2018-04-13 21:12:47.0, <<BLOB>>, 2, 普通用户, 1, 1, 2018-04-13 21:12:46.0
2018-05-02 12:24:27,443 DEBUG [main] (BaseJdbcLogger.java:145) - <== Total: 3
2018-05-02 12:24:27,443 INFO [main] (UserMapperTest.java:1146) - userList总数为:1
2018-05-02 12:24:27,443 INFO [main] (UserMapperTest.java:1148) - 用户名:admin
2018-05-02 12:24:27,443 INFO [main] (UserMapperTest.java:1150) - 角色名:管理员
2018-05-02 12:24:27,443 INFO [main] (UserMapperTest.java:1150) - 角色名:普通用户
2018-05-02 12:24:27,443 INFO [main] (UserMapperTest.java:1157) - sqlSession close successfully
userList总数为:1
用户名:admin
角色名:管理员
角色名:普通用户
用户信心保留的是第一条数据的信心,因此用户名是admin . 角色为什么不是3个呢? 因为“普通用户”这个角色重复了,所以也只保留第一个出现的“普通用户”。 因为MyBatis会对嵌套查询的每一级对象都进行属性比较。 MyBatis会首先比较顶层的对象,如果SysUser相同,就继续比较SysRole部分,如果SysRole不同,就会增加一个sysRole,两个SysROle相同就保留前一个。 假设SysRole还有下一级,仍然按照该规则去比较。
通过上述这个例子应该明白了id的作用了,需要注意的是,很肯能出现一种没有配置id的情况。 当没有配置id的时候,MyBatis就会把resultMap中配置的说哟字段进行比较,如果所有字段的值都相同就合并,只要有一个字段值不同,就不合并。
在嵌套结果配置id属性时,如果查询中没有查询id属性配置的列,就会导致id对应的值为null.这种情况下,所有的id都相同,因此会使嵌套的集合中只有一条数据。 所以在配置id列时,查询语句中必须包含该列。
可以对userMap再次改造,将id标签改为result标签,执行结果是一样的,由于MyBatis要对所有字段字段进行比较,因此当字段数为M时,如果查询结果有N条,就需要进行M*N,相比配置id时的N次比较,效率差很多。 所以尽量配置id标签.
<result property="id" column="id"/>
两层嵌套
在RBAC权限系统中,除了一个用户对应多个角色外,每个角色还会对应多个权限,在上个例子的基础上我们增加一级,获取角色对应的所有权限。
PrivilegeMap.xml增加映射
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<!-- 当Mapper接口和XML文件关联的时候, namespace的值就需要配置成接口的全限定名称 -->
<mapper namespace="com.artisan.mybatis.xml.mapper.PrivilegeMapper">
<resultMap id="privilegeMap" type="com.artisan.mybatis.xml.domain.SysPrivilege">
<id property="id" column="id" />
<result property="privilegeName" column="privilege_name" />
<result property="privilegeUrl" column="privilege_url" />
</resultMap>
</mapper>
SysRole实体类改造
增加
/**
* 一对多,权限集合
*/
List<SysPrivilege> privilegeList;
public List<SysPrivilege> getPrivilegeList() {
return privilegeList;
}
public void setPrivilegeList(List<SysPrivilege> privilegeList) {
this.privilegeList = privilegeList;
}
RoleMapper.xml文件中增加如下resultMap
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<!-- 当Mapper接口和XML文件关联的时候, namespace的值就需要配置成接口的全限定名称 -->
<mapper namespace="com.artisan.mybatis.xml.mapper.RoleMapper">
<resultMap id="rolePrivilegeListMap"
type="com.artisan.mybatis.xml.domain.SysRole"
extends="com.artisan.mybatis.xml.mapper.UserMapper.roleMap">
<collection property="privilegeList" columnPrefix="privilege_"
resultMap="com.artisan.mybatis.xml.mapper.PrivilegeMapper.privilegeMap" />
</resultMap>
</mapper>
我们创建了角色权限映射,继承了roleMap,嵌套了privilegeList属性,直接使用PrivilegeMapper.xml中的privilegeMap。
UserMapper.xml改造
<resultMap id="userRoleAndPrivilegeListMap" extends="userMap"
type="com.artisan.mybatis.xml.domain.SysUser" >
<collection property="roleList" columnPrefix="sysRole_"
resultMap="com.artisan.mybatis.xml.mapper.RoleMapper.rolePrivilegeListMap">
</collection>
</resultMap>
到这里我们就配置好了一个两层嵌套的映射,为了得到权限信息,还需要修改SQL进行关联
<select id="selectAllUserAndRolesAndPrivileges" resultMap="userRoleAndPrivilegeListMap">
select
u.id,
u.user_name,
u.user_password,
u.user_email,
u.user_info,
u.head_img,
u.create_time,
r.id sysRole_id,
r.role_name sysRole_role_name,
r.enabled sysRole_enabled,
r.create_by sysRole_create_by,
r.create_time sysRole_create_time,
p.id sysRole_privilege_id,
p.privilege_name sysRole_privilege_privilege_name,
p.privilege_url sysRole_privilege_privilege_url
from sys_user u
inner join sys_user_role ur on u.id = ur.user_id
inner join sys_role r on ur.role_id = r.id
inner join sys_role_privilege rp on rp.role_id = r.id
inner join sys_privilege p on p.id = rp.privilege_id
</select>
这里需要特别注意sys_privilege表中的别名。因为sys_privilege嵌套在rolePrivilegeListMap中,前缀名是 privilege_
而rolePrivilegeListMap的前缀是sysRole_
所以rolePrivilegeListMap中的privilegeMap的前缀就变测过了 sysRole_privilege_
在嵌套中,这个前缀需要叠加,一定不要写错,所以SQL如下
单元测试
@Test
public void selectAllUserAndRolesAndPrivilegesTest() {
logger.info("selectAllUserAndRolesAndPrivilegesTest");
// 获取SqlSession
SqlSession sqlSession = getSqlSession();
try {
// 获取UserMapper接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 调用selectAll,查询全部用户
List<SysUser> userList = userMapper.selectAllUserAndRolesAndPrivileges();
// 结果不为空
Assert.assertNotNull(userList);
// 结果大于0
Assert.assertTrue(userList.size() > 0);
logger.info("userList总数为:" + userList.size());
for (SysUser sysUser : userList) {
logger.info("用户名:" + sysUser.getUserName());
for (SysRole sysRole : sysUser.getRoleList()) {
logger.info("\t角色名:" + sysRole.getRoleName());
for (SysPrivilege sysPrivilege : sysRole.getPrivilegeList()) {
logger.info("\t\t权限名:" + sysPrivilege.getPrivilegeName());
}
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
sqlSession.close();
logger.info("sqlSession close successfully ");
}
}
Assert.assertNotNull(userList); 加个断点,debug看下数据
日志
2018-05-02 20:10:08,202 INFO [main] (BaseMapperTest.java:26) - sessionFactory bulit successfully
2018-05-02 20:10:08,207 INFO [main] (BaseMapperTest.java:29) - reader close successfully
2018-05-02 20:10:08,211 INFO [main] (UserMapperTest.java:1164) - selectAllUserAndRolesAndPrivilegesTest
2018-05-02 20:10:08,287 DEBUG [main] (BaseJdbcLogger.java:145) - ==> Preparing: select u.id, u.user_name, u.user_password, u.user_email, u.user_info, u.head_img, u.create_time, r.id sysRole_id, r.role_name sysRole_role_name, r.enabled sysRole_enabled, r.create_by sysRole_create_by, r.create_time sysRole_create_time, p.id sysRole_privilege_id, p.privilege_name sysRole_privilege_privilege_name, p.privilege_url sysRole_privilege_privilege_url from sys_user u inner join sys_user_role ur on u.id = ur.user_id inner join sys_role r on ur.role_id = r.id inner join sys_role_privilege rp on rp.role_id = r.id inner join sys_privilege p on p.id = rp.privilege_id
2018-05-02 20:10:08,411 DEBUG [main] (BaseJdbcLogger.java:145) - ==> Parameters:
2018-05-02 20:10:08,448 TRACE [main] (BaseJdbcLogger.java:151) - <== Columns: id, user_name, user_password, user_email, user_info, head_img, create_time, sysRole_id, sysRole_role_name, sysRole_enabled, sysRole_create_by, sysRole_create_time, sysRole_privilege_id, sysRole_privilege_privilege_name, sysRole_privilege_privilege_url
2018-05-02 20:10:08,449 TRACE [main] (BaseJdbcLogger.java:151) - <== Row: 1, admin, 123456, aaa@qq.com.com, <<BLOB>>, <<BLOB>>, 2018-04-13 21:12:47.0, 1, 管理员, 1, 1, 2018-04-13 21:12:46.0, 1, 用户管理, /users
2018-05-02 20:10:08,466 TRACE [main] (BaseJdbcLogger.java:151) - <== Row: 1, admin, 123456, aaa@qq.com.com, <<BLOB>>, <<BLOB>>, 2018-04-13 21:12:47.0, 1, 管理员, 1, 1, 2018-04-13 21:12:46.0, 3, 系统日志, /logs
2018-05-02 20:10:08,468 TRACE [main] (BaseJdbcLogger.java:151) - <== Row: 1, admin, 123456, aaa@qq.com.com, <<BLOB>>, <<BLOB>>, 2018-04-13 21:12:47.0, 1, 管理员, 1, 1, 2018-04-13 21:12:46.0, 2, 角色管理, /roles
2018-05-02 20:10:08,469 TRACE [main] (BaseJdbcLogger.java:151) - <== Row: 1, admin, 123456, aaa@qq.com.com, <<BLOB>>, <<BLOB>>, 2018-04-13 21:12:47.0, 2, 普通用户, 1, 1, 2018-04-13 21:12:46.0, 4, 人员维护, /persons
2018-05-02 20:10:08,473 TRACE [main] (BaseJdbcLogger.java:151) - <== Row: 1, admin, 123456, aaa@qq.com.com, <<BLOB>>, <<BLOB>>, 2018-04-13 21:12:47.0, 2, 普通用户, 1, 1, 2018-04-13 21:12:46.0, 5, 单位维护, /companies
2018-05-02 20:10:08,475 TRACE [main] (BaseJdbcLogger.java:151) - <== Row: 1001, artisan, 123456, aaa@qq.com.com, <<BLOB>>, <<BLOB>>, 2018-04-13 21:12:47.0, 2, 普通用户, 1, 1, 2018-04-13 21:12:46.0, 4, 人员维护, /persons
2018-05-02 20:10:08,477 TRACE [main] (BaseJdbcLogger.java:151) - <== Row: 1001, artisan, 123456, aaa@qq.com.com, <<BLOB>>, <<BLOB>>, 2018-04-13 21:12:47.0, 2, 普通用户, 1, 1, 2018-04-13 21:12:46.0, 5, 单位维护, /companies
2018-05-02 20:10:08,478 DEBUG [main] (BaseJdbcLogger.java:145) - <== Total: 7
2018-05-02 20:10:08,479 INFO [main] (UserMapperTest.java:1177) - userList总数为:2
2018-05-02 20:10:08,479 INFO [main] (UserMapperTest.java:1179) - 用户名:admin
2018-05-02 20:10:08,482 INFO [main] (UserMapperTest.java:1181) - 角色名:管理员
2018-05-02 20:10:08,482 INFO [main] (UserMapperTest.java:1183) - 权限名:用户管理
2018-05-02 20:10:08,482 INFO [main] (UserMapperTest.java:1183) - 权限名:系统日志
2018-05-02 20:10:08,482 INFO [main] (UserMapperTest.java:1183) - 权限名:角色管理
2018-05-02 20:10:08,483 INFO [main] (UserMapperTest.java:1181) - 角色名:普通用户
2018-05-02 20:10:08,483 INFO [main] (UserMapperTest.java:1183) - 权限名:人员维护
2018-05-02 20:10:08,483 INFO [main] (UserMapperTest.java:1183) - 权限名:单位维护
2018-05-02 20:10:08,483 INFO [main] (UserMapperTest.java:1179) - 用户名:artisan
2018-05-02 20:10:08,483 INFO [main] (UserMapperTest.java:1181) - 角色名:普通用户
2018-05-02 20:10:08,484 INFO [main] (UserMapperTest.java:1183) - 权限名:人员维护
2018-05-02 20:10:08,484 INFO [main] (UserMapperTest.java:1183) - 权限名:单位维护
2018-05-02 20:10:08,485 INFO [main] (UserMapperTest.java:1191) - sqlSession close successfully
collection集合的嵌套查询
同association关联的嵌套查询这种方式类似,collection也会执行额外的SQL查询。 后续单开篇介绍。