学习地址🔗
一、简介
💬概述:SQL映射文件是MyBatis的核心文件,它指导MyBatis对数据库进行CRUD操作,有着重要意义
🔑SQL映射文件中的重要标签
标签名 | 解释 |
---|
cache | 命名空间的二级缓存配置 | cache-ref | 其他命名空间缓存配置的引用 | resultMap | 自定义结果集映射 | sql | 抽取可重用的语句块 | insert | 映射插入(INSERT)语句 | update | 映射更新(UPDATE)语句 | delete | 映射删除(DELETE)语句 | select | 映射查询(SELECT)语句 |
💡 还有一个parameterMap标签,但已经废弃
🔑根标签<mapper>
- 概述:SQL映射文件的根标签是
<mapper> ——映射,表示映射文件中每一条SQL语句都需要与对应的持久层接口方法建立映射关系,不然没有意义 - 唯一属性——
namespace :名称空间,<mapper> 的唯一属性,也是必须要设置的属性,因为它是SQL映射文件的唯一标识,属性值为对应的持久层接口的全类名,将映射文件与持久层接口建立映射关系
二、insert、update和delete标签
2.1 关于增删改
💬概述:在SQl映射文件中使用<insert> 、<update> 和<delete> 三个SQL语句标签可以实现增删改操作
🔑增删改标签的主要属性
属性名 | 解释 |
---|
id | 标签的唯一标识,对应持久层接口的方法名,必须添加 | parameterType | 参数类型,对应该SQL语句标签对应的持久层接口方法中的形参类型,只适用于单个形参,可以不添加 | resultType | 返回值类型,对应该SQL语句标签对应的持久层接口方法中的形参类型,可以不添加 |
🔑实现增删改操作的注意事项
- 在持久层接口中创建对应增删改方法时,方法返回值类型可以是void、boolean、int、long以及它们对应的包装类
- 实现增删改操作时,需要修改数据库表数据,因此需要对当前事务进行提交,而MyBatis中通过
SqlSessionFactory.openSession() 获取sqlSession 对象时,默认不会自动提交,实现提交有两种方式 ① 设置自动提交:在openSession() 方法中,带上true参数,即openSession(true) ,此时获取的sqlSession 对象在操作完增删改后就回自动提交事务,不用手动提交 ② 手动提交:使用openSession() 方法直接获取sqlSession 对象,在实现增删改操作之后,再通过sqlSession.commit() 方法手动提交事务
🔑测试
-
持久层接口方法 public interface EmployeeDao {
void addEmp(Employee newEmployee);
void updateEmpInfo(Employee updatedEmp);
void deleteEmpById(Integer empId);
}
-
对应映射文件中的SQL语句 <?xml version="1.0" encoding="utf8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.key.mybatis.dao.EmployeeDao">
<insert id="addEmp" parameterType="com.key.mybatis.entity.Employee">
insert into `employee`(`emp_name`, `gender`, `email`, `dept_id`)
values(#{empName}, #{gender}, #{email}, 1);
</insert>
<update id="updateEmpInfo" parameterType="com.key.mybatis.entity.Employee">
update `employee`
set `emp_name` = #{empName}, `gender` = #{gender}, `email` = #{email}
where emp_id = #{empId};
</update>
<delete id="deleteEmpById" parameterType="int">
delete
from `employee`
where emp_id = #{empId};
</delete>
</mapper>
-
测试类 public class EmployeeTest {
@Test
public void testInsert() {
SqlSession sqlSession = MyBatisUtil.getSqlSession();
EmployeeDao mapper = sqlSession.getMapper(EmployeeDao.class);
Employee newEmp = new Employee(null, "周星驰",
"男", "zhouxingchi@qq.com");
mapper.addEmp(newEmp);
sqlSession.commit();
sqlSession.close();
}
@Test
public void testUpdate() {
SqlSession sqlSession = MyBatisUtil.getSqlSession();
EmployeeDao mapper = sqlSession.getMapper(EmployeeDao.class);
Employee updatedEmp = new Employee(2, "周润发",
"男", "zhourunfa@gmail.com");
mapper.updateEmpInfo(updatedEmp);
sqlSession.commit();
sqlSession.close();
}
@Test
public void testDelete() {
SqlSession sqlSession = MyBatisUtil.getSqlSession();
EmployeeDao mapper = sqlSession.getMapper(EmployeeDao.class);
mapper.deleteEmpById(4);
sqlSession.commit();
sqlSession.close();
}
}
2.2 获取自增主键的值
💬概述:在实现添加操作时,如果数据库表中的主键字段是自增的,则在创建对应的JavaBean时,会将主键成员变量设置null后再插入到数据表中,但插入后JavaBean中的主键成员变量还是null,此时就需要在<insert> 标签中设置相关属性获取自增主键的值
🔑关于获取自增主键值的几个相关属性
属性名 | 属性值 | 解释 |
---|
useGeneratedKeys | true/false,默认是false | 设置使用主键策略,即获取自增主键值;设置为true表示使用主键策略,必须添加 | keyProperty | JavaBean中自增主键对应的成员变量名 | 将获取的自增主键值赋值给JavaBean中对应的成员变量,必须添加 | keyColumn | 数据库表中对应的自增主键字段名 | 标识出数据库表中对应的自增主键字段,可以不添加 |
💡 MyBatis底层中获取自增主键的值使用的也是原生JDBC中statement 对象,即通过statement.getGeneratedKeys() 方法获取
🔑获取自增主键值方式:直接在<insert> 标签中添加useGeneratedKeys="true" 和keyProperty="empId" 两个属性即可
<insert id="addEmp" parameterType="com.key.mybatis.entity.Employee"
useGeneratedKeys="true" keyProperty="empId" keyColumn="emp_id">
insert into `employee`(`emp_name`, `gender`, `email`, `dept_id`)
values(#{empName}, #{gender}, #{email}, 1);
</insert>
三、参数处理
3.1 获取不同形式的参数
💡 在持久层接口的方法中可以设置多个多种不同类型的形参,因此在SQL映射文件中获取不同参数时有不同的处理方式
3.1.1 获取单个参数
🔑传入的形参是普通类型:对于单个普通类型的形参,如String name ,MyBatis不会对其做特殊处理,因此在映射文件中通过#{Key} 获取对应参数值时,#{Key} 中的键Key可以是任意字符串,不一定与方法形参名一致
🔑测试
Employee getEmpById(Integer empId);
<select id="getEmpById" resultType="com.key.mybatis.entity.Employee" parameterType="int">
select *
from `employee`
where emp_id = #{empId};
</select>
3.1.2 获取多个参数
🔑传入的形参都是普通类型:持久层接口中传入多个普通类型的形参,MyBatis会将所有形参封装成一个Map 集合,Map 集合中的键Key就是#{Key} 中的Key,值Value就是参数值,因此通过#{Key} 获取每一个参数值时,#{Key} 中的键Key就不能是任意的,键Key有下列两种情况
-
没有对每一个形参进行命名
-
键的选取:只能使用MyBatis为每一个形参设置的默认键 -
默认键:[arg0…argN-1] 或 [param1…paramN](N表示有N个形参) -
测试
<select id="getEmpByIdAndName" resultType="com.key.mybatis.entity.Employee">
select *
from `employee`
where `emp_id` = #{arg0} and
`emp_name` = #{param2};
</select>
-
对每一个形参进行命名?
-
键的选取:可以使用默认的[param1…paramN](不能使用[arg0…argN-1]),也可以使用自定义的参数名 -
对形参进行命名的方式:在持久层接口对应方法中的每一个形参上添加@Param("自定义参数名") 注解,属性值就是为对应参数自定义的名称(一般命名为与形参名一样) -
测试
Employee getEmpByIdAndName(@Param("empId") Integer empId, String empName);
<select id="getEmpByIdAndName" resultType="com.key.mybatis.entity.Employee">
select *
from `employee`
where `emp_id` = #{empId} and
`emp_name` = #{param2};
</select>
🔑传入单个特殊类型形参
🔑传入形参中既有普通类型又有对象类型:在持久层接口方法中可以添加不同类型的形参,当添加一个普通类型形参和一个对象类型形参,在SQL标签中要获取的参数需要从对象类型的形参中获取,此时MyBatis同样会将两个形参封装成一个Map 集合
-
集合的键Key有下列情况
- 对于普通类型的形参:普通类型的形参适用于MyBatis设置的默认的键Key——[arg0…argN-1] 或 [param1…paramN];如果不使用默认的键Key,同样可以对每一个普通类型形参进行命名,此时键Key既可以时默认的(不能使用[arg0…argN-1]),也可以是自定的参数名
- 对于对象类型的形参:需要获取的参数得从对象形参的属性中获取,因此键Key必须是【对象.属性名】的形式,而【对象.属性名】中的对象相当于一个普通类型的形参,既可以是默认的[param1…paramN](不能使用[arg0…argN-1]),也可以使用自己命名的参数名(命名方式同普通类形参)
-
测试
Employee getEmpByNameAndUserId(String empName, User user);
<select id="getEmpByNameAndUserId" resultType="com.key.mybatis.entity.Employee">
select *
from `employee`
where `emp_name` = #{param1} and
`emp_id` = #{arg1.userid};
</select>
3.2 #{Key}
3.2.1 #{}🆚${}
🔑共同点
- 都可以获取由持久层接口方法传过来的参数值
- 获取参数值的方式一样,都是在
{} 中添加参数值对应的键Key即可
🔑区别
获取参数两种方式 | 在SQL语句中设置参数值的方式 | 适用场景 | 适用场景例子 |
---|
#{Key} | #{Key} 通过预编译的方式将参数值设置到SQL语句中,而SQL字符串中对应参数值的位置用占位符? 来拼接,而不是直接将参数值拼接到SQL中,相当于JDBC中的PreparedStatement 对象,可以防止SQL注入 | 原生JDBC支持使用占位符? 的地方,一般都使用#{Key} | 在WHERE 语句中获取参数值 | ${Key} | ${Key} 是直接将获取的参数值拼接到SQL语句中,不进行预编译,因此会有SQL注入的安全问题 | 不能使用#{Key} 获取参数的情况,即JDBC不支持占位符地方就可以使用${Key} 获取参数值 | ①获取数据库表名;②获取排序规则的ASE 和DESE ; |
3.2.2 #{}中设置参数规则
💬概述:#{} 中除了可以添加键Key来获取对应参数值之外,还可以设置获取参数时的其他规则
🔑可以设置的主要规则
- javaType:设置JavaBean的属性类型
- jdbcType:设置数据库表中字段的类型
- mode:设置存储过程
- resultMap:设置结果集
- jdbcName:设置数据库表字段名
- typeHandler:设置类型处理器
四、select标签
4.1 select标签的主要属性
属性名 | 解释 | 是否必须设置 |
---|
id | <select> 标签的唯一标识,对应持久层接口方法名,与方法进行绑定 | 是 | resultType | 设置返回值类型 | 是 | parameterType | 设置参数类型(仅适用单个形参) | 否 | resultMap | 设置结果集中数据库表的字段名和JavaBea中属性名之间的映射规则,属性值对应<resultMap> 的唯一标识id,不能与resultType 一起使用 | 否 |
4.2 关于返回值类型
4.2.1 返回值类型为List集合
💬概述:在持久层接口方法中,可以设置返回值类型为List<T> ,此时<select> 标签中的resultType 的属性值为集合中泛型T 对应的全类名,而不是集合List 的全类名
🔑测试
List<Employee> getAllEmployees();
<select id="getAllEmployees" resultType="com.key.mybatis.entity.Employee">
select *
from `employee`;
</select>
4.2.2 返回值类型为Map集合
💬概述:持久层接口方法的返回值类型可以设置为Map<K,V> ,此时<select> 标签中的resultType 的属性值就为Map 类型的全类名,但MyBatis已经对常用的Java类型设置过别名,因此属性名直接写类型小写map 即可
🔑对于查询一条与多条记录,返回Map集合时的封装规则不同
-
查询返回一条记录:返回一条记录时,相当于返回一个结果集对应的JavaBean,此时返回值Map<K,V> 中的键值对分别对应JavaBean的属性名和属性值,一般泛型设置为Map<String,Object>
Map<String, Object> getEmpReturnMap(Integer empId);
<select id="getEmpReturnMap" resultType="map">
select *
from `employee`
where `emp_id` = #{empId};
</select>
-
查询返回多条记录:返回多条记录时,相当于返回多个JavaBean,此时需要将每一个JavaBean作为Map 集合的值Value来返回,而键Key需要自定义为JavaBean的某一个属性名(如Map<String, Employee> ),将该属性与JavaBean一一对应起来
-
自定义键Key的方式:在持久层接口对应的方法上添加@MapKey("JavaBean中某一个属性名") 注解,注解中添加的属性值就是自定义的JavaBean的某一个属性名 -
测试
@MapKey("empId")
Map<Integer, Employee> getAllEesReturnMap();
<select id="getAllEesReturnMap" resultType="map">
select *
from `employee`;
</select>
4.3 嵌套结果集查询
4.3.1 resultMap属性和resultMap标签
🔑resultMap属性
- 作用:
<select> 标签的属性,用来标识结果集所使用的映射规则,而具体的映射规则需要使用<resultMap> 标签来设置 - 属性值:
resultMap 的属性值对应<resultMap> 标签的唯一标识id,表示将<resultMap> 标签中设置的映射规则应用到<select> 标签的结果集中
🔑<resultMap> 标签
-
作用:设置查询结果集与JavaBean的映射规则
? 关于结果集的映射规则
- 使用
<resultMap> 设置的映射规则是作用于查询出来的结果集(一张不存在的表)与某一个JavaBean之间,不是将数据库中某张表与某一个JavaBean建立映射关系 - 查询出来的结果集与该JavaBean可以是没有关系的,也可以是相对应的,如在不取别名的情况下查询某一个员工的信息,则查询结果集中每一列与员工表每一列都相等,因此员工表对应的JavaBean就与结果集相对应
? 解决表字段名与JavaBean字段名不一致的方法
① 在SQL语句中为每一个字段设置别名,别名与JavaBean的属性名保持一致(比较麻烦) ② 在全局配置文件中通过<setting> 标签开启驼峰命名映射(只能将带下划线的_ 表字段名转成对应的使用驼峰命名JavaBean属性名) ③ 使用<resultMap> 标签自定义映射规则,适用于任何情况
-
<resultMap> 标签的两个属性 ① id:标签的唯一标识,属性值对应<select> 标签的resultMap 属性值 ② type:标识映射规则中的JavaBean,属性值为JavaBean的全类名 -
主要的子标签
标签名 | 作用 | 主要属性 | 是否必须添加 |
---|
<id> | 设置主键列名对应JavaBean属性的映射规则 | ①column:对应结果集中的主键列名; ②property:对应JavaBean中的属性名 | 否 | <result> | 设置普通列名与对应JavaBean属性的映射规则 | 同上 | 否 |
? 关于两个子标签
<id> 和<result> 的作用其实是一样的,只是使用<id> 标签时,MyBatis底层会对相应的列做特殊处理- 两个子标签都不是必须要添加的,但不添加时就不能保证结果集与JavaBean的映射关系
-
测试
<resultMap id="myEmp" type="com.key.mybatis.entity.Employee">
<id column="emp_id" property="empId"/>
<result column="emp_name" property="empName"/>
<result column="gender" property="gender"/>
<result column="email" property="email"/>
</resultMap>
<select id="getEmpById" resultMap="myEmp">
select *
from `employee`
where `emp_id` = #{emp};
</select>
4.3.2 级联查询
🔑创建既有级联关系的表及其对应的JavaBean
-
创建部门表department,员工表中添加部门id字段,作为外键约束
drop table if exists `department`;
create table `department` (
`dept_id` int unsigned not null unique auto_increment comment '部门id - 主键',
`dept_name` varchar(20) not null comment '部门名字',
primary key(`dept_id`)
) engine = innodb default character = utf8 comment = '部门表';
alter table `employee`
add column `dept_id` int unsigned not null comment '员工所在部门id - 外键';
alter table `employee`
add constraint `fk_emp_dept`
foreign key(`dept_id`) references `department`(`dept_id`);
-
创建部门表对应JavaBean——Department public class Department {
private Integer deptId;
private String deptName;
}
-
在Employee 类中添加对象类型属性(级联属性)——Department myDept (注意添加的不是部门的id,是部门对象) public class Employee {
private Integer empId;
private String empName;
private String gender;
private String email;
private Department myDept;
}
🔑设置级联查询的结果集映射规则:在级联查询中,需要使用<resultMap> 标签设置级联查询结果集的映射规则(封装规则),保证级联查询的结果,在子标签<result> 中通过【对象.属性】的形式可以对级联属性进行赋值,此时【对象.属性】中的【对象】为JavaBean(员工类)中的对象类型属性名(部门属性)
<resultMap id="cascadeQuery" type="com.key.mybatis.entity.Employee">
<id column="emp_id" property="empId"/>
<result column="dept_id" property="myDept.deptId"/>
<result column="dept_name" property="myDept.deptName"/>
</resultMap>
? 为什么不使用resultType :级联查询时,在<select> 中只能使用resultMap 属性设置结果集,不能简单使用resultType 设置返回值类型,因为resultType 属性只能设置查询结果集的类型,不能设置结果集中的对象类型属性,因此调用接口方法返回的对象中,其对象类型的属性值肯定是null
🔑测试:根据员工id查询某一位员工的全部信息及其所在部门的id和部门名(连接查询)
-
SQL语句 select e.`emp_id`, e.`emp_name`, e.`gender`, e.`email`, e.`dept_id`, d.`dept_name`
from `employee` e, `department` d
where e.`dept_id` = d.`dept_id` and
e.`emp_id` =
-
员工持久层接口中的查询方法 Employee getEmpAndDeptById(Integer empId);
-
员工映射文件
<resultMap id="cascadeQuery" type="com.key.mybatis.entity.Employee">
<id column="emp_id" property="empId"/>
<result column="emp_name" property="empName"/>
<result column="gender" property="gender"/>
<result column="email" property="email"/>
<result column="dept_id" property="myDept.deptId"/>
<result column="dept_name" property="myDept.deptName"/>
</resultMap>
<select id="getEmpAndDeptById" resultMap="cascadeQuery">
select e.`emp_id`, e.`emp_name`, e.`gender`, e.`email`, e.`dept_id`, d.`dept_name`
from `employee` e, `department` d
where e.`dept_id` = d.`dept_id` and
e.`emp_id` = #{empId};
</select>
-
测试方法 @Test
public void testCascadeQuery() {
SqlSession sqlSession = MyBatisUtil.getSqlSession();
EmployeeDao mapper = sqlSession.getMapper(EmployeeDao.class);
Employee emp = mapper.getEmpAndDeptById(2);
System.out.println("员工对象 --> " + emp);
System.out.println("员工部门 --> " + emp.getMyDept());
sqlSession.close();
}
-
打印结果
4.3.3 association标签
💬概述:<resultMap> 还有一个重要的子标签——<association> ,它的使用与<resultMap> 类似
🔑作用:可以设置结果集与被关联的JavaBean(部门类)的映射规则
🔑主要属性
① property:属性值为<resultMap> 中设置的JavaBean(员工类)中的对象类型的属性名(部门属性),必须设置 ② javaType:属性值为对象类型属性的类型全类名(部门类),即被关联的JavaBean全类名,必须设置
🔑<association> 的子标签:<association> 中的子标签中也有<id> 和<result> ,用于设置结果集中的列名与被关联JavaBean的属性名的映射规则,用法与<resultMap> 的一样
🔑使用1.0——简单的级联查询:在<resultMap> 标签中添加<association> ,然后添加以上两个属性和子标签
<resultMap id="cascadeQuery" type="com.key.mybatis.entity.Employee">
<id column="emp_id" property="empId"/>
<association property="myDept" javaType="com.key.mybatis.entity.Department">
<id column="dept_id" property="deptId"/>
<result column="dept_name" property="deptName"/>
</association>
</resultMap>
? 关于主键映射规则设置
- 使用
<association> 设置级联属性映射规则时,也能通过<id> 标签设置主键,而外部的父标签<resultMap> 中也有一个<id> 设置主键,两个<id> 中的column 属性值不能一样 - 如果员工表和部门表的主键字段名一样,都是id,则在查询时需要通过取别名的方式区分两个主键,保证两个
<id> 标签中的column 属性值不一样
🔑使用2.0——分步查询
🔑使用3.0——延迟加载
-
?什么是延迟加载:延迟加载也叫懒加载、按需加载,即在分步查询的基础上,按需要获取另一个结果集对象
💡 还是以上面为例,如果开启了延迟加载,则当我们只获取员工对象中的基本信息(姓名、性别等),不获取其部门信息时,MyBatis就只会加载第一条SQL语句(在控制台只有一条SQL语句被打印),只有当我们获取部门信息时,MyBatis才会加载第二条SQL语句,获取出对应的部门信息
-
开启延迟加载:在MyBatis全局配置文件中的<setting> 标签中设置下列两个参数
参数 | 设置参数值 | 解释 |
---|
lazyLoadingEnabled | true | 开启全局的延迟加载,默认是参数值是false | aggressiveLazyLoading | false | 侵入延迟加载,默认参数值是true |
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
-
测试1.0——只获取员工的姓名
-
测试2.0——直接打印员工对象emp,获取员工全部信息
-
测试3.0——获取员工信息及其部门信息
4.3.4 collection标签
💬概述:<resultMap> 另一个实现嵌套结果集的子标签——<collection> ,用法与<association> 和<resultMap> 类似
🔑作用:设置结果集与集合类型属性之间的映射规则,集合中的元素类型(即泛型)是被关联的JavaBean类型
🔑主要属性
① property:属性值为<resultMap> 中设置的JavaBean(部门类)中集合类型属性名,必须设置 ② ofType:属性值为集合类型属性中,集合元素类型(员工类)的全类名,即被关联JavaBean全类名,必须设置
🔑<collection> 的子标签:<collection> 中的子标签中也有<id> 和<result> ,用于设置结果集与集合元素类型(员工类)的属性之间的映射规则,用法与<association> 和<resultMap> 一样
🔑使用1.0——简单的级联查询
🔑使用2.0——分步查询:<collection> 实现分步查询与<association> 类似
-
实现分步查询的重要属性
属性名 | 属性值 | 解释 | 是否必须添加 |
---|
property | <resultMap> 中设置的JavaBean(部门类)中的集合类型的属性名(员工集合) | 与级联查询的使用方式一样,都是用来标识级联属性 | 是 | select | 被关联JavaBean的持久层接口的方法,需要在方法前写上方法所在类的全类名 | 标识被关联JavaBean的方法,通过该方法获取的集合(员工集合)赋值给property 所标识的级联属性(集合类型属性) | 是 | column | 结果集中某一列的列名 | 标识查询出来的结果集中某一列的列名,select 中标识的方法会根据该结果集列的值,来查询对应的集合,最后再赋值给property 的级联属性 | 是 |
-
测试
-
员工持久层接口方法
List<Employee> getEesByDeptId(Integer deptId);
-
员工映射文件
<select id="getEesByDeptId" resultType="com.key.mybatis.entity.Employee">
select *
from `employee`
where `dept_id` = #{deptId};
</select>
-
部门持久层接口方法
Department getDeptAndEesByIdStep(Integer deptId);
-
部门映射文件
<resultMap id="collQueryStep" type="com.key.mybatis.entity.Department">
<id column="dept_id" property="deptId"/>
<result column="dept_name" property="deptName"/>
<collection property="empList"
select="com.key.mybatis.dao.EmployeeDao.getEesByDeptId"
column="dept_id"/>
</resultMap>
<select id="getDeptAndEesByIdStep" resultMap="collQueryStep">
select *
from `department`
where `dept_id` = #{deptId};
</select>
-
测试方法 @Test
public void testCollQueryStep() {
SqlSession sqlSession = MyBatisUtil.getSqlSession();
DepartmentDao mapper = sqlSession.getMapper(DepartmentDao.class);
Department dept = mapper.getDeptAndEesByIdStep(3);
System.out.println("部门信息 --> " + dept);
System.out.println("部门下的所有员工信息");
dept.getEmpList().forEach(System.out :: println);
sqlSession.close();
}
-
打印结果
🔑使用3.0——延迟加载:同上👆
4.3.5 association标签🆚collection标签
🔑共同点
- 都是
<resultMap> 的子标签 - 都用于在嵌套结果集查询中设置级联属性的映射规则
- 都能实现分步查询
- 延迟加载对它们的分步查询影响一样
🔑区别
<association> 标签用于设置对象类型的级联属性,而<collection> 标签用于设置集合类型的级联属性<association> 中使用javaType 标识对象属性的类型,而<collection> 中使用ofType 标识集合元素的类型
|