Mybatis之XML 映射器详解

9 篇文章 0 订阅
7 篇文章 0 订阅

Mybatis系列文章
Mybatis之配置详解
Mybatis之XML 映射器详解
Mybatis之动态 SQL详解
Mybatis之缓存详解
Mybatis之映射器注解详解
Mybatis之Mybatis Generator vs IDEA插件 自动生成代码

前言

知识储备要求:对Mybatis框架有一定基础,了解Mybatis框架。
简述:SQL 映射文件几个顶级元素(按照应被定义的顺序列出):
cache – 该命名空间的缓存配置。
cache-ref – 引用其它命名空间的缓存配置。
resultMap– 描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素。
parameterMap – 老式风格的参数映射。此元素已被废弃,并可能在将来被移除!请使用行内参数映射。文档中不会介绍此元素。
sql – 可被其它语句引用的可重用语句块。
insert– 映射插入语句。
update – 映射更新语句。
delete – 映射删除语句。
select– 映射查询语句。

参数

  • MyBatis 参数建议明确指定jdbcType类型,javaType可以省略

    #{property,javaType=int,jdbcType=NUMERIC}
    

    MyBati可以根据参数对象的类型确定 javaType,除非该对象是一个 HashMap。这个时候,你需要显式指定 javaType 来确保正确的类型处理器(TypeHandler)被使用。
    提示 JDBC 要求,如果一个列允许使用 null 值,并且会使用值为 null 的参数,就必须要指定 JDBC 类型(jdbcType)。参数传递null时会报错。

  • @Param可以给传入的参数指定名称。

  • 传递单参数时,非数组和集合类,可以通过#{参数名}或者#{0}获取。传递数组时foreach标签里用array,List用collection或者list。Set只能用collection。

  • 传递多参数时,即使不加@Param注解,也可以根据参数名来取值,也可以使用#{param1}、#{param2…}这种方式取值,示例如下:

    GradeParam selectByNameAndLevel(String name, GradeLevel level);
    
    <select id="selectByNameAndLevel" resultType="GradeParam">
      select
      <include refid="columns" />
      from grade_param
      where name = #{param1,jdbcType=VARCHAR} and level = #{param2,jdbcType=TINYINT}
    </select>
    
  • 自定义类型处理方式,可以指定一个特殊的类型处理器类(或别名),比如:

    #{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
    
  • 对于数值类型,还可以设置 numericScale 指定小数点后保留的位数。

    #{height,javaType=double,jdbcType=NUMERIC,numericScale=2}
    

    思考:验证numericScale指定小数是否四舍五入。结果:指定小数位后并没有效果。

  • mode 属性允许你指定 IN,OUT 或 INOUT 参数。如果参数的 mode 为 OUT 或 INOUT,将会修改参数对象的属性值,以便作为输出参数返回。

    #{department, mode=OUT, jdbcType=CURSOR, javaType=ResultSet, resultMap=departmentResultMap}
    

    补充:验证存储过程OUT参数。结果:需要将参数封装成map或者DTO传入,否则获取不到。

    Student callProcedure(Map<String, Object> map);
    
    <select id="callProcedure" resultMap="StudentResultMap" statementType="CALLABLE">
        call student_procedure(#{id,mode=IN,jdbcType=BIGINT}, #{enterScore,mode=OUT,jdbcType=DECIMAL})
    </select>
    

    存储过程如下:

    	CREATE PROCEDURE `student_procedure`(IN inputId bigint, OUT enterScore decimal(8,2))
    BEGIN
      select enter_score into enterScore from student where id = inputId;
    	select id,
        name,
        age,
        sex,
        school_id,
        enter_score,
        create_time,
        update_time from student where id = inputId;
    END
    

    如果 mode 为 OUT(或 INOUT),而且 jdbcType 为 CURSOR(也就是 Oracle 的 REFCURSOR),你必须指定一个 resultMap 引用来将结果集 ResultMap 映射到参数的类型上。要注意这里的 javaType 属性是可选的,如果留空并且 jdbcType 是 CURSOR,它会被自动地被设为 ResultMap。
    相关示例参考:https://www.cnblogs.com/feixian/p/6182439.html

  • 使用 #{} 参数语法,MyBatis 会创建 PreparedStatement 参数占位符,并通过占位符安全地设置参数(就像使用 ? 一样)。 这样做更安全,更迅速。使用${}参数语法,直接在 SQL 语句中直接插入一个不转义的字符串。 示例如下:

    ORDER BY ${columnName}
    

    或者

    @Select("select * from user where ${column} = #{value}")
    User findByColumn(@Param("column") String column, @Param("value") String value);
    

    提示 用这种方式接受用户的输入,并用作语句参数是不安全的,会导致潜在的 SQL 注入攻击。因此,要么不允许用户输入这些字段,要么自行转义并检验这些参数。

select

一个简单查询的 select 元素是非常简单的。比如:

<select id="selectPerson" parameterType="int" resultType="hashmap">
  SELECT * FROM PERSON WHERE ID = #{id,jdbcType=BIGINT}
</select>

这个语句名为 selectPerson,接受一个 int(或 Integer)类型的参数,并返回一个 HashMap 类型的对象,其中的键是列名,值便是结果行中的对应值。
select 元素允许你配置很多属性来配置每条语句的行为细节。

<select
  id="selectPerson"
  parameterType="int"
  parameterMap="deprecated"
  resultType="hashmap"
  resultMap="personResultMap"
  flushCache="false"
  useCache="true"
  timeout="10"
  fetchSize="256"
  statementType="PREPARED"
  resultSetType="FORWARD_ONLY">

Select 元素的属性

属性描述
id在命名空间中唯一的标识符,可以被用来引用这条语句。
parameterType将会传入这条语句的参数的类全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器(TypeHandler)推断出具体传入语句的参数,默认值为未设置。
parameterMap用于引用外部 parameterMap 的属性,目前已被废弃。请使用行内参数映射和 parameterType 属性。
resultType期望从这条语句中返回结果的类全限定名或别名。 注意,如果返回的是集合,那应该设置为集合包含的类型,而不是集合本身的类型。 resultType 和 resultMap 之间只能同时使用一个。
resultMap对外部 resultMap 的命名引用。结果映射是 MyBatis 最强大的特性,如果你对其理解透彻,许多复杂的映射问题都能迎刃而解。 resultType 和 resultMap 之间只能同时使用一个。
flushCache将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false。
useCache将其设置为 true 后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对 select 元素为 true。
timeout这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(依赖数据库驱动)。
fetchSize这是一个给驱动的建议值,尝试让驱动程序每次批量返回的结果行数等于这个设置值。 默认值为未设置(依赖驱动)。
statementType可选 STATEMENTPREPAREDCALLABLE。这会让 MyBatis 分别使用 StatementPreparedStatementCallableStatement,默认值:PREPARED。
resultSetTypeFORWARD_ONLY,SCROLL_SENSITIVE, SCROLL_INSENSITIVE 或 DEFAULT(等价于 unset) 中的一个,默认值为unset(依赖数据库驱动)。
databaseId如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有不带 databaseId匹配当前 databaseId的语句;如果带和不带的语句都有,则不带的会被忽略。
resultOrdered这个设置仅针对嵌套结果 select 语句:如果为 true,将会假设包含了嵌套结果集或是分组,当返回一个主结果行时,就不会产生对前面结果集的引用。 这就使得在获取嵌套结果集的时候不至于内存不够用。默认值:false
resultSets这个设置仅适用于多结果集的情况。它将列出语句执行后返回的结果集并赋予每个结果集一个名称,多个名称之间以逗号分隔。

补充:查询入参慎用Mybatis自带RowBounds分页方式,这种方式是在内存中分页。PageHelper分页插件整合RowBounds是物理分页,而非内存分页。

insert, update 和 delete

Insert, Update, Delete 元素的属性

属性描述
id在命名空间中唯一的标识符,可以被用来引用这条语句。
parameterType将会传入这条语句的参数的类全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器(TypeHandler)推断出具体传入语句的参数,默认值为未设置。
parameterMap用于引用外部 parameterMap 的属性,目前已被废弃。请使用行内参数映射和 parameterType 属性。
flushCache将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:(对 insert、update 和 delete 语句)true。
timeout这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(依赖数据库驱动)。
statementType可选 STATEMENTPREPAREDCALLABLE。这会让 MyBatis 分别使用 StatementPreparedStatementCallableStatement,默认值:PREPARED
useGeneratedKeys(仅适用于 insert 和 update)这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键(比如:像 MySQL 和 SQL Server 这样的关系型数据库管理系统的自动递增字段),默认值:false。
keyProperty(仅适用于 insert 和 update)指定能够唯一识别对象的属性,MyBatis 会使用 getGeneratedKeys 的返回值或 insert 语句的 selectKey 子元素设置它的值,默认值:未设置。如果生成列不止一个,可以用逗号分隔多个属性名称。
keyColumn(仅适用于 insert 和 update)设置生成键值在表中的列名,在某些数据库(像 PostgreSQL)中,当主键列不是表中的第一列的时候,是必须设置的。如果生成列不止一个,可以用逗号分隔多个属性名称。
databaseId如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有不带 databaseId匹配当前 databaseId 的语句;如果带和不带的语句都有,则不带的会被忽略。

下面是 insert,update 和 delete 语句的示例:

<insert id="insertOne" useGeneratedKeys="true" keyProperty="id" parameterType="Student">
    insert into student(name, age, sex, school_id, enter_score)
    value (#{name,jdbcType=VARCHAR}, #{age,jdbcType=INTEGER}, #{sex,jdbcType=VARCHAR},
    #{schoolId,jdbcType=BIGINT}, #{enterScore,jdbcType=NUMERIC,numericScale=2})
</insert>
<update id="updateAuthor">
  update Author set username = #{username}, password = #{password}, email = #{email}, bio = #{bio} where id = #{id}
</update>
<delete id="deleteAuthor">
  delete from Author where id = #{id}
</delete>

如果你的数据库支持自动生成主键的字段(比如 MySQL 和 SQL Server),那么你可以设置 useGeneratedKeys=”true”,然后再把keyProperty设置为目标属性就 OK 了。
对于不支持自动生成主键列的数据库和可能不支持自动生成主键的 JDBC 驱动,MyBatis 有另外一种方通过 selectKey 元素来生成主键。

<insert id="insertOne" parameterType="GradeParam">
  insert into grade_param(name, manager_name, level)
  value(#{name,jdbcType=VARCHAR}, #{managerName,jdbcType=VARCHAR}, #{level,jdbcType=TINYINT})
  <selectKey keyProperty="id" keyColumn="id" resultType="long" order="AFTER">
      select last_insert_id()
  </selectKey>
</insert>

补充:DDL操作(如alter)直接使用update标签即可。

selectKey 元素的属性

属性描述
keyPropertyselectKey 语句结果应该被设置到的目标属性。如果生成列不止一个,可以用逗号分隔多个属性名称。
keyColumn返回结果集中生成列属性的列名。如果生成列不止一个,可以用逗号分隔多个属性名称。
resultType结果的类型。通常 MyBatis 可以推断出来,但是为了更加准确,写上也不会有什么问题。
order可以设置为BEFOREAFTER。如果设置为 BEFORE,那么它首先会生成主键,设置 keyProperty 再执行插入语句。如果设置为 AFTER,那么先执行插入语句,然后是 selectKey 中的语句
statementType和前面一样,MyBatis 支持 STATEMENTPREPAREDCALLABLE 类型的映射语句,分别代表 Statement, PreparedStatementCallableStatement类型。

sql

这个元素可以用来定义可重用的 SQL 代码片段,以便在其它语句中使用。 参数可以静态地(在加载的时候)确定下来,并且可以在不同的 include 元素中定义不同的参数值。比如:

<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>

这个 SQL 片段可以在其它语句中使用,例如:

<select id="selectUsers" resultType="map">
  select
    <include refid="userColumns"><property name="alias" value="t1"/></include>,
    <include refid="userColumns"><property name="alias" value="t2"/></include>
  from some_table t1
    cross join some_table t2
</select>

也可以在 include 元素的 refid 属性或内部语句中使用属性值,例如:

<sql id="sometable">
  ${prefix}Table
</sql>

<sql id="someinclude">
  from
    <include refid="${include_target}"/>
</sql>

<select id="select" resultType="map">
  select
    field1, field2, field3
  <include refid="someinclude">
    <property name="prefix" value="Some"/>
    <property name="include_target" value="sometable"/>
  </include>
</select>

补充:

  1. 问题1:mapper接口传过来的参数能否通过#{}传入到<sql/>中
    答案:能
    示例如下:
    List<School> listByInclude2(String name, String address);
    
        <sql id="name_and_address">
        name = #{name,jdbcType=VARCHAR} and address = #{address,jdbcType=VARCHAR}
    	</sql>
        <select id="listByInclude2" resultType="School">
        select * from school where
        <include refid="name_and_address" />
    </select>
    
  2. 问题2:<include/>的property value值里是否支持#{}
    答案:支持
    (1) 可转义,转义后不会被识别为占位符进行替换
    (2) value值传入中仅支持${}不支持#{}
    List<School> listByInclude1(String name);
    
        <select id="listByInclude1" resultType="School">
    	    select * from school where
    	    <include refid="name_equals">
    	        <property name="schoolName" value="#{name,jdbcType=VARCHAR}"/>
    	    </include>
    	</select>
    

结果映射

  1. resultType 属性指定,再=根据属性名来映射列到 JavaBean 的属性上。

    <!-- mybatis-config.xml 中 -->
    <typeAlias type="com.someapp.model.User" alias="User"/>
    
    <!-- SQL 映射 XML 中 -->
    <select id="selectUsers" resultType="User">
      select id, username, hashedPassword
      from some_table
      where id = #{id}
    </select>
    
  2. resultMap 解决列名不匹配的一种方式。

    <resultMap id="userResultMap" type="User">
      <id property="id" column="user_id" />
      <result property="username" column="user_name"/>
      <result property="password" column="hashed_password"/>
    </resultMap>
    
    <select id="selectUsers" resultMap="userResultMap">
      select user_id, user_name, hashed_password
      from some_table
      where id = #{id}
    </select>
    

高级结果映射

<!-- 非常复杂的结果映射 -->
<resultMap id="detailedBlogResultMap" type="Blog">
  <constructor>
    <idArg column="blog_id" javaType="int"/>
  </constructor>
  <result property="title" column="blog_title"/>
  <association property="author" javaType="Author">
    <id property="id" column="author_id"/>
    <result property="username" column="author_username"/>
    <result property="password" column="author_password"/>
    <result property="email" column="author_email"/>
    <result property="bio" column="author_bio"/>
    <result property="favouriteSection" column="author_favourite_section"/>
  </association>
  <collection property="posts" ofType="Post">
    <id property="id" column="post_id"/>
    <result property="subject" column="post_subject"/>
    <association property="author" javaType="Author"/>
    <collection property="comments" ofType="Comment">
      <id property="id" column="comment_id"/>
    </collection>
    <collection property="tags" ofType="Tag" >
      <id property="id" column="tag_id"/>
    </collection>
    <discriminator javaType="int" column="draft">
      <case value="1" resultType="DraftPost"/>
    </discriminator>
  </collection>
</resultMap>

结果映射(resultMap)

  • constructor - 用于在实例化类时,注入结果到构造方法中
    • idArg - ID 参数;标记出作为 ID 的结果可以帮助提高整体性能
    • arg - 将被注入到构造方法的一个普通结果
  • id – 一个 ID 结果;标记出作为 ID 的结果可以帮助提高整体性能
  • result – 注入到字段或 JavaBean 属性的普通结果
  • association – 一个复杂类型的关联;许多结果将包装成这种类型
    • 嵌套结果映射 – 关联可以是 resultMap 元素,或是对其它结果映射的引用
  • collection – 一个复杂类型的集合
    • 嵌套结果映射 – 集合可以是 resultMap 元素,或是对其它结果映射的引用
  • discriminator – 使用结果值来决定使用哪个 resultMap
    • case – 基于某些值的结果映射
      • 嵌套结果映射 – case 也是一个结果映射,因此具有相同的结构和元素;或者引用其它的结果映射

ResultMap 的属性列表

属性描述
id当前命名空间中的一个唯一标识,用于标识一个结果映射。
type类的完全限定名, 或者一个类型别名。
autoMapping如果设置这个属性,MyBatis 将会为本结果映射开启或者关闭自动映射。 这个属性会覆盖全局的属性 autoMappingBehavior。默认值:未设置。补充:当有嵌套结果集时,父结果的autoMapping无法继承全局的属性 autoMappingBehavior,必须明确指定。

id & result

<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>

id 和 result 元素都将一个列的值映射到一个简单数据类型(String, int, double, Date 等)的属性或字段,id 元素对应的属性会被标记为对象的标识符,在比较对象实例时使用。 这样可以提高整体的性能,尤其是进行缓存和嵌套结果映射(也就是连接映射)的时候。

Id 和 Result 的属性

属性描述
property映射到列结果的字段或属性。 无论是哪一种情形,你都可以使用常见的点式分隔形式进行复杂属性导航。 比如,映射一些简单的东西:“username”,或者映射到一些复杂的东西上:“address.street.number”。
column数据库中的列名,或者是列的别名。一般情况下,这和传递给 resultSet.getString(columnName) 方法的参数一样。
javaType一个 Java 类的全限定名,或一个类型别名。如果你映射到的是 HashMap,那么你应该明确地指定 javaType 来保证行为与期望的相一致。
jdbcTypeJDBC 类型,所支持的 JDBC 类型参见这个表格之后的“支持的 JDBC 类型”。
typeHandler我们在前面讨论过默认的类型处理器。使用这个属性,你可以覆盖默认的类型处理器。 这个属性值是一个类型处理器实现类的全限定名,或者是类型别名。

支持的 JDBC 类型
为了以后可能的使用场景,MyBatis 通过内置的 jdbcType 枚举类型支持下面的 JDBC 类型。

BITFLOATCHARTIMESTAMPOTHERUNDEFINED
TINYINTREALVARCHARBINARYBLOBNVARCHAR
SMALLINTDOUBLELONGVARCHARVARBINARYCLOBNCHAR**
INTEGERNUMERICDATELONGVARBINARYBOOLEANNCLOB
BIGINTDECIMALTIMENULLCURSORARRAY

构造方法

有些情况下你想使用不可变类,构造方法很适合使用不可变类。 构造方法注入允许你在初始化时为类设置属性的值,而不用暴露出公有方法。MyBatis 也支持私有属性和私有 JavaBean 属性来完成注入,但有一些人更青睐于通过构造方法进行注入。 constructor 元素就是为此而生的。

  1. 按顺序指出columnjavaType
    <resultMap id="ConstructorResultMap" type="School">
        <constructor>
            <idArg column="id" javaType="long" />
            <arg column="name" javaType="string" />
            <arg column="address" javaType="string" />
        </constructor>
    </resultMap>
    
    <select id="selectByIdWithConstructor" resultMap="ConstructorResultMap">
        select id, name, address, build_date, description, create_time, update_time from school where id = #{id,jdbcType=BIGINT}
    </select>
    
  2. 版本 3.4.3开始,可以在指定参数名称的前提下,以任意顺序编写 arg 元素。 为了通过名称来引用构造方法参数,你可以添加@Param注解,或者使用'-parameters'编译选项并启用 useActualParamName选项(默认开启)来编译项目。
 <resultMap id="ConstructorResultMap" type="School">
    <constructor>
        <idArg column="id" name="id" />
        <arg column="name" name="name" />
        <arg column="address" name="address" />
    </constructor>
</resultMap>

arg和idArg属性

属性描述
column数据库中的列名,或者是列的别名。一般情况下,这和传递给 resultSet.getString(columnName) 方法的参数一样。
javaType一个 Java 类的完全限定名,或一个类型别名。如果你映射到的是 HashMap,那么你应该明确地指定 javaType 来保证行为与期望的相一致。
jdbcTypeJDBC 类型,所支持的 JDBC 类型参见这个表格之前的“支持的 JDBC 类型”。
typeHandler使用这个属性,你可以覆盖默认的类型处理器。 这个属性值是一个类型处理器实现类的完全限定名,或者是类型别名。
select用于加载复杂类型属性的映射语句的 ID,它会从 column 属性中指定的列检索数据,作为参数传递给此 select 语句。
resultMap结果映射的 ID,可以将嵌套的结果集映射到一个合适的对象树中为了将结果集正确地映射到嵌套的对象树中,MyBatis 允许你 “串联”结果映射,以便解决嵌套结果集的问题。
name构造方法形参的名字。

关联 association与collection

association元素处理一对一的关系。 比如,学生就读于一所学校,学生与学校就是一一对应的关系。
collection元素处理一对多的关系。 比如,学校可供多名学生就读,学校与学生就是一对多的关系。
association与collection属性

属性描述
property映射到列结果的字段或属性。
javaType/ofType一个 Java 类的完全限定名,或一个类型别名。
jdbcTypeJDBC 类型,所支持的 JDBC 类型参见这个表格之前的“支持的 JDBC 类型”。
typeHandler我们在前面讨论过默认的类型处理器。使用这个属性,你可以覆盖默认的类型处理器。 这个属性值是一个类型处理器实现类的完全限定名,或者是类型别名。
column数据库中的列名,或者是列的别名。一般情况下,注意:在使用复合主键的时候,你可以使用 column="{prop1=col1,prop2=col2}"这样的语法来指定多个传递给嵌套 Select 查询语句的列名。这会使得 prop1 和 prop2 作为参数对象,被设置为对应嵌套 Select 语句的参数。
select用于加载复杂类型属性的映射语句的 ID,它会从 column 属性指定的列中检索数据,作为参数传递给目标 select 语句。
fetchType可选的。有效值为 lazyeager。 指定属性后,将在映射中忽略全局配置参数 lazyLoadingEnabled,使用属性的值。
resultMap结果映射的 ID,可以将此关联的嵌套结果集映射到一个合适的对象树中。
columnPrefix当连接多个表时,你可能会不得不使用列别名来避免在 ResultSet 中产生重复的列名。指定 columnPrefix 列名前缀允许你将带有这些前缀的列映射到一个外部的结果映射中。
notNullColumn默认情况下,在至少一个被映射到属性的列不为空时,子对象才会被创建。 你可以在这个属性上指定非空的列来改变默认行为,指定后,Mybatis 将只在这些列非空时才创建一个子对象。可以使用逗号分隔来指定多个列。默认值:未设置。
autoMapping如果设置这个属性,MyBatis 将会为本结果映射开启或者关闭自动映射。 这个属性会覆盖全局的属性 autoMappingBehavior。默认值:未设置。
foreignColumn指定外键对应的列名,指定的列将与父类型中 column 的给出的列进行匹配。
resultSet指定用于加载复杂类型的结果集名字。

columnPrefix的使用示例

<resultMap id="StudentWithColumnPrefixResultMap" type="com.nestor.mybatisdemo.dto.StudentDTO" autoMapping="true">
    <association property="school" javaType="School" resultMap="com.nestor.mybatisdemo.mapper.SchoolMapper.SchoolResultMap" columnPrefix="s_"/>
</resultMap>

<!--列名不能重复否则会发生非预期映射-->
<select id="selectStudentWithColumnPrefix" resultMap="StudentWithColumnPrefixResultMap" databaseId="mysql">
    select s1.id, s1.name, s1.age, s1.sex, s1.enter_score, s1.create_time, s1.update_time,
    s2.id as s_id, s2.name as s_name, s2.address as s_address, s2.build_date as s_build_date,
    s2.description as s_description, s2.create_time as s_create_time, s2.update_time as s_update_time
    from student s1
    inner join school s2 on s1.school_id = s2.id
</select>

关联的不同之处是,你需要告诉 MyBatis 如何加载关联。MyBatis 有三种不同的方式加载关联:

嵌套结果映射

在查询语句中进行关联join操作,结果集使用嵌套的结果映射来处理连接结果的重复子集。尽量查出父子结果集的主键以避免相同列值的一组数据被处理为一条导致非预期结果。

<resultMap id="detailedBlogResultMap" type="Blog">
  <constructor>
    <idArg column="blog_id" javaType="int"/>
  </constructor>
  <result property="title" column="blog_title"/>
  <association property="author" javaType="Author">
    <id property="id" column="author_id"/>
    <result property="username" column="author_username"/>
    <result property="password" column="author_password"/>
    <result property="email" column="author_email"/>
    <result property="bio" column="author_bio"/>
    <result property="favouriteSection" column="author_favourite_section"/>
  </association>
</resultMap>
嵌套 Select 查询

以下两个 select 查询语句:一个用来加载学生(Student),另外一个用来加载学校(School),而且学生的结果映射指定使用 selectById语句加载学校属性。

<!--SchoolMapper.xml-->
<resultMap id="SchoolResultMap" type="School">
    <id column="id" property="id" />
    <result column="name" property="name" />
    <result column="address" property="address" />
    <result column="build_date" property="buildDate" />
    <result column="description" property="description" />
    <result column="create_time" property="createTime" />
    <result column="update_time" property="updateTime" />
</resultMap>

<select id="selectById" resultMap="SchoolResultMap">
    select id, name, address, build_date, description, create_time, update_time from school where id = #{id,jdbcType=BIGINT}
</select>

<!--StudentMapper.xml-->
<resultMap id="StudentWithSchoolResultMap" type="com.nestor.mybatisdemo.dto.StudentDTO">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <result property="age" column="age"/>
    <result property="sex" column="sex"/>
    <result property="enterScore" column="enter_score"/>
    <result property="createTime" column="create_time"/>
    <result property="updateTime" column="update_time"/>
    <association property="school" javaType="School" column="school_id" select="com.nestor.mybatisdemo.mapper.SchoolMapper.selectById" />
</resultMap>

<!--嵌套关联查询-->
<select id="selectStudentWithSchool" resultMap="StudentWithSchoolResultMap">
  select id, name, age, sex, enter_score, create_time, update_time, school_id from student
</select>

注:association标签中需指定column,来告诉select如何去查询子集。
这种方式虽然很简单,但在大型数据集或大型数据表上性能不佳。这个问题被称为“N+1 查询问题”。 概括地讲,N+1 查询问题是这样子的:你执行了一个单独的 SQL 语句来获取结果的一个列表(就是“+1”)。对列表返回的每条记录,你执行一个 select 查询语句来为每条记录加载详细信息(就是“N”)。

MyBatis 能够对这样的查询进行延迟加载,通过设置全局配置lazyLoadingEnabledaggressiveLazyLoading或者指定fetchType属性可以优化,具体性能优化程度还要看具体场景,这里不推荐这样的写法。

多结果集查询

版本 3.2.3 开始,MyBatis 提供了另一种解决 N+1 查询问题的方法。
某些数据库允许存储过程返回多个结果集,或一次性执行多个语句,每个语句返回一个结果集。 我们可以利用这个特性,在不使用连接的情况下,只访问数据库一次就能获得相关数据。

<resultMap id="SchoolWithMoreResultSetResultMap" type="com.nestor.mybatisdemo.dto.SchoolDTO">
    <id column="id" property="id" />
    <result column="name" property="name" />
    <result column="address" property="address" />
    <result column="build_date" property="buildDate" />
    <result column="description" property="description" />
    <result column="create_time" property="createTime" />
    <result column="update_time" property="updateTime" />
    <collection property="students" ofType="Student" resultSet="students" column="id" foreignColumn="school_id" />
</resultMap>

<select id="selectSchoolWithMoreResultSet" resultSets="school,students" resultMap="SchoolWithMoreResultSetResultMap" statementType="CALLABLE">
    {call school_procedure(#{id,jdbcType=BIGINT})}
</select>

存储过程如下:

CREATE PROCEDURE `school_procedure`(IN inputId bigint)
begin
	select * from school where id = inputId;
  select * from student where school_id = inputId;
end

在例子中,存储过程执行下面的查询并返回两个结果集。第一个结果集会返回学校(School)的结果,第二个则返回学生(Student)的结果。

在映射语句中,必须通过resultSets 属性为每个结果集指定一个名字,多个名字使用逗号隔开

collection标签中需要明确指出resultSetcolumnforeignColumn来确定学校结果集与学生结果集之间的关联关系,column代表父类型传入的关联列,foreignColumn代表子类型的关联列。

反例如下方式多语句是不支持的,仅支持单语句

<!--不支持报错-->
<select id="moreSelect" resultSets="school,students" resultMap="SchoolWithMoreResultSetResultMap">
  	select * from school where id = #{id,jdbcType=BIGINT};
    select * from student where school_id = #{id,jdbcType=BIGINT};
</select>

鉴别器

鉴别器(discriminator)元素就是被设计来应对一个数据库查询可能会返回多个不同的结果集情况,另外也能处理其它情况,例如类的继承层次结构。 鉴别器的概念很好理解——它很像 Java 语言中的 switch 语句

一个鉴别器的定义需要指定 columnjavaType 属性。column 指定了 MyBatis 查询被比较值的地方。 javaType 确保使用正确的相等测试(虽然很多情况下字符串的相等测试都可以工作)。

<resultMap id="StudentWithDiscriminatorResultMap" type="Student">
    <discriminator javaType="string" column="sex">
        <case value="M" resultType="com.nestor.mybatisdemo.dto.ScienceStudent" />
        <case value="F" resultType="com.nestor.mybatisdemo.dto.LiberalArtStudent" />
    </discriminator>
</resultMap>

<!--鉴别器-->
<select id="selectStudentWithDiscriminator" resultMap="StudentWithDiscriminatorResultMap" databaseId="mysql">
    select id, name, age, sex, enter_score, create_time, update_time, school_id from student
</select>

在这个示例中,MyBatis 会从结果集中得到每条记录,然后比较它的 sex 值。 如果它匹配任意一个鉴别器的 case,就会使用这个 case 指定的结果映射。 这个过程是互斥的,也就是说,剩余的结果映射将被忽略。 如果不能匹配任何一个 case,MyBatis 就只会使用鉴别器块外定义的结果映射。结果集中部分属性值自动映射所以没有明确写出。

自动映射

在简单的场景下,MyBatis 可以为你自动映射查询结果。但如果遇到复杂的场景,你需要构建一个结果映射。 接下来你将看到,你可以混合使用这两种策略。让我们深入了解一下自动映射是怎样工作的。

当自动映射查询结果时,MyBatis 会获取结果中返回的列名并在 Java 类中查找相同名字的属性(忽略大小写)。 这意味着如果发现了 ID 列和 id 属性,MyBatis 会将列 ID 的值赋给 id 属性。

通常数据库列使用大写字母组成的单词命名,单词间用下划线分隔;而 Java 属性一般遵循驼峰命名法约定。为了在这两种命名方式之间启用自动映射,需要将 mapUnderscoreToCamelCase 设置为true

甚至在提供了结果映射后,自动映射也能工作。在这种情况下,对于每一个结果映射,在 ResultSet 出现的列,如果没有设置手动映射,将被自动映射。在自动映射处理完毕后,再处理手动映射。 在下面的例子中,id 和 userName 列将被自动映射,hashed_password 列将根据配置进行映射。

<select id="selectUsers" resultMap="userResultMap">
  select
    user_id             as "id",
    user_name           as "userName",
    hashed_password
  from some_table
  where id = #{id}
</select>
<resultMap id="userResultMap" type="User">
  <result property="password" column="hashed_password"/>
</resultMap>

有三种自动映射等级:
NONE - 禁用自动映射。仅对手动映射的属性进行映射。
PARTIAL - 对除在内部定义了嵌套结果映射(也就是连接的属性)以外的属性进行映射
FULL - 自动映射所有属性。
默认值是 PARTIAL,这是有原因的。当对连接查询的结果使用FULL时,连接查询会在同一行中获取多个不同实体的数据,因此可能导致非预期的映射。 下面的例子将展示这种风险:

<select id="selectBlog" resultMap="blogResult">
  select
    B.id,
    B.title,
    A.username,
  from Blog B left outer join Author A on B.author_id = A.id
  where B.id = #{id}
</select>
<resultMap id="blogResult" type="Blog">
  <association property="author" resultMap="authorResult"/>
</resultMap>

<resultMap id="authorResult" type="Author">
  <result property="username" column="author_username"/>
</resultMap>

在该结果映射中,Blog 和 Author 均将被自动映射。但是注意 Author 有一个 id 属性,在 ResultSet 中也有一个名为 id 的列,所以 Author 的 id 将填入 Blog 的 id,这可不是你期望的行为。 所以,要谨慎使用 FULL

无论设置的自动映射等级是哪种,你都可以通过在结果映射上设置 autoMapping 属性来为指定的结果映射设置启用/禁用自动映射。

<resultMap id="userResultMap" type="User" autoMapping="false">
  <result property="password" column="hashed_password"/>
</resultMap>

github工程地址https://github.com/nestorbian/spring-boot-examples/tree/master/mybatis-demo

Mybatis系列文章
Mybatis之配置详解
Mybatis之XML 映射器详解
Mybatis之动态 SQL详解
Mybatis之缓存详解
Mybatis之映射器注解详解
Mybatis之Mybatis Generator vs IDEA插件 自动生成代码

参考:
Mybatis3官网

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值