Mybatis(五)面试官:Mybatis官网这句话你怎么理解?如果这个世界总是这么简单就好了

259 阅读8分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

在实际开发中我们一般情况下,结果映射有以下三种:

map作为结果映射

<select id="selectUsers" resultType="map">
  select id, username, password
  from User
  where id = #{id}
</select>

resultType作为结果映射

<select id="selectUsers" resultType="User">
  select
    user_id             as "id",
    user_name           as "userName",
    hashed_password     as "hashedPassword"
  from User
  where id = #{id}
</select>
<select id="selectUsers" resultType="com.evader.mapper.User">
  select id, username, password
  from User
  where id = #{id}
</select>

resultMap作为结果映射

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

小结:实际开发中一般不采用第一种方式,将返回的结果映射到map的key上。简单的结果通常是映射到POJO上,复杂的一般使用自己定义的JavaBean。使用resultType时注意点就是需要用as起别名将数据库的列名与JavaBean的字段名对应上。使用resultMap的SQL语句中也可以使用as起别名,只要将别名配置到标签的column属性上即可。

一, 结果映射

有很多子标签,实话说,平时我用不到这么多。

  • resultMap
    • constructor:用于实例化类时,注入结果到构造方法中
      • idArg:用于标记主键,提高性能
      • arg:被注入到构造方法中的参数
    • id:表中的主键,提高整体性能
    • result:注入到字段或JavaBean上的某个字段
    • association:注入到某个对象类型
      • 嵌套结果查询:该标签下可以嵌套标签,或是对其他结果映射的引用
    • collection:注入到一个集合类型
      • 嵌套结果查询:该标签下可以嵌套标签,或是对其他结果映射的引用
    • discriminator:使用结果值来决定使用哪个resultMap
      • case:基于某些值的结果映射
        • 嵌套结果映射:该标签下可以是对其他结果映射的引用

二, id&result

一个简单的resultMap

<resultMap id="BaseResultMap" type="com.evader.generator.pojo.Goods">
    <id column="goods_id" jdbcType="INTEGER" property="goodsId" />
    <result column="goods_name" jdbcType="VARCHAR" property="goodsName" />
    <result column="goods_type_id" jdbcType="INTEGER" property="goodsTypeId" />
    <result column="goods_sales" jdbcType="INTEGER" property="goodsSales" />
    <result column="shop_id" jdbcType="INTEGER" property="shopId" />
    <result column="pic_url" jdbcType="VARCHAR" property="picUrl" />
  </resultMap>

这是最常见的子标签,其中是映射表里的主键,一般在第一个位置。是用来映射表中其他属性。当JavaBean是自己构建或表没有主键时那么就不存在这个标签。

三,构造方法

在实际开发中我们声明的JavaBean通常都是作为结果映射的类。一般是不会定义构造函数的,在Java中默认会提供一个无参的构造函数。你好奇给你返回的对象是如何生成的吗?实际是Mybatis调用默认的无参构造函数创建的对象。(我盲猜的)如果你提供了有参构造函数,就像这样:

public class User {
   public User(Integer id, String username, int age) {
     //TODO
  }
}

这个时候User类就不再有默认的无参构造了,所以需要告诉Mybatis用那个构造函数创建对象。这个机制在Mybatis中使用的是标签。

<constructor>
   <idArg column="id" javaType="int"/>
   <arg column="username" javaType="String"/>
   <arg column="age" javaType="_int"/>
</constructor>

这里的类似于标签下的,类似于。需要注意的是,写标签时尽量顺序与构造方法参数的顺序一致,尽管Mybatis在3.4.3做了说明,可以随意放置参数的位置。

三,对象类型映射

3. 1 关联(association)

标签的作用是映射类中的对象类型,比如这篇博客的有一个帅气的作者,这个时候Blog类中就应该有一个属性是User类型的。对于简单类型之前的文章已经提到过,Mybatis是会自动去推断的。如果是对象类型,那么Mybatis是无法推断的,所以需要你告诉Mybatis博客与作者如何关联。Mybatis的关联方式有两种:

  1. 嵌套查询:通过查询两个sql,以其中一个sql的运行结果来加载另一个sql中的对象属性
  2. 嵌套结果映射:使用一个sql语句,将嵌套的结果映射来处理连接结果的重复子集

该标签的属性如下:

propertyJavaBean的属性名。简单写可写成username,复杂的可以使用点分法写成user.username
javaType当前字段的全限定类名/别名,如果映射到JavaBean上,Mybatis会自动推断,如果使用HashMap作为映射对象,那么就应该明确javaType的类型
jdbcTypeMybatis并不对这个属性做类型处理,开发者只需要在可能执行插入、更新和删除的且允许空值的列上指定 JDBC 类型
typeHandler类型处理器
column对应表中的列名,如果是复合主键,可以写成column="{prop1=col1,prop2=col2}",这样就可以传递多个参数给指定的select查询语句
select用于加载其他sql语句的属性,一般用在映射对象属性的映射语句的ID。它会从column属性中获取查询出来的数据,然后将此数据传递给目标select语句,如果是复合主键,可以写成column="{prop1=col1,prop2=col2}",这样就可以传递多个参数给指定的select查询语句
fetchType有效值为 lazy 和 eager。 指定属性后,将在映射中忽略全局配置参数 lazyLoadingEnabled,使用属性的值。
resultMap与select属性差不多,是select的替代方案,它可以引入一个其他的,实现的嵌套。也称"串联"结果映射
columnPrefix当一个类中有两个属性是同一种类型切该属性是对象类型时,可以使用它,复用参考下方的例子
notNullColumn默认情况下,在至少一个被映射到属性的列不为空时,子对象才会被创建。 你可以在这个属性上指定非空的列来改变默认行为,指定后,Mybatis 将只在这些列非空时才创建一个子对象。可以使用逗号分隔来指定多个列。
autoMapping如果设置这个属性,MyBatis 将会为本结果映射开启或者关闭自动映射。 这个属性会覆盖全局的属性 autoMappingBehavior。注意,本属性对外部的结果映射无效,所以不能搭配 select 或 resultMap 元素使用。

3.2 关联的嵌套查询

示例:

// 该类中种有个属性是User对象
@Data
public class BlogDto {
    private Integer blogId;
    private String blogName;
    private User user;
}
<resultMap id="blogResult" type="com.evader.pojo.vo.BlogDto">
    <result property="blogId" column="blog_id"/>
    <result property="blogName" column="blog_name"/>
    <association property="user" column="user_id" javaType="user" select="selectUser"/>
</resultMap>

<select id="selectBlog" resultMap="blogResult">
    select * from Blog where blog_id = #{id}
</select>

<select id="selectUser" resultType="user">
    select * from User where id = #{id}
</select>

这个示例包含两个查询语句,一个是查询博客(Blog)表的,一个是查询作者(User)表的。这个示例的执行流程是先执行查询博客的语句,这时就能查到blog_id,blog_name,user_id三个字段,由于关联标签的存在,此时会把user_id这个属性当成参数传递给selectUser这个查询语句,selectUser查询到作者后将作者映射到中。 ​

这种方式虽然容易理解,但在数据量打的时候性能很差。这就是常说的"N+1 查询问题"。假设上述案例没有where条件。那么其中+1就是单独使用一条sql来获取博客。N就是博客列表中所有user_id都执行了一个select查询语句。 虽然Mybatis能够实现延迟加载,但是架不住数据量多与遍历,所以有了另一种解决方式。

3.3 关联的嵌套结果映射

示例:使用实现嵌套查询

<resultMap id="blogResultSecond" type="com.evader.pojo.vo.BlogDto">
  <result property="blogId" column="blog_id"></result>
  <result property="blogName" column="blog_name"/>
  <association property="user" column="user_id" javaType="User" resultMap="selectUser"/>
</resultMap>

<resultMap id="selectUser" type="User">
  <id property="id" column="id"/>
  <result property="name" column="name"/>
  <result property="password" column="password"/>
</resultMap>

<select id="selectBlogSecond" resultMap="blogResultSecond">
  select *
  from Blog b
  left join USER u
  on b.user_id = u.id
  where blog_id = #{id}
</select>

这种方式采用嵌套的结果映射,并且将嵌套的独立出来,方便复用。如果你的映射文件中不需要复用时,你可以写成这样:

<resultMap id="blogResultPlus" type="com.evader.pojo.vo.BlogDto">
  <result property="blogId" column="blog_id"/>
  <result property="blogName" column="blog_name"/>
  <association property="user" javaType="User">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <result property="password" column="password"/>
  </association>
</resultMap>

关联的多结果集

拓展:如果一个博客有多个作者的情况下,如何映射结果集呢?

@Data
public class BlogDtoPlus {
    private Integer blogId;
    private String blogName;
    private User user;
    private User anotherUser;
}
<resultMap id="selectBlogDtoPlus" type="com.evader.pojo.vo.BlogDtoPlus">
  <result property="blogId" column="blog_id"/>
  <result property="blogName" column="blog_name"/>
  <association property="user" resultMap="selectUser"/>
  <association property="anotherUser" resultMap="selectUser" columnPrefix="another_"/>
</resultMap>

<select id="selectBlogDtoPlus" resultMap="selectBlogDtoPlus">
  select
    B.blog_id,
    B.blog_name,
    A.id,
    A.name,
    A.password,
    C.id as another_id,
    C.name as another_name,
    C.password as another_password
  from Blog B
  left join User A on b.user_id = a.id
  left join User C on b.another_user_id = c.id
  where blog_id = #{id}
</select>

既然有两个User那么就使用两个同样的关联标签即可,需要在其中一个标签上加上columnPrefix属性,用于区分两个User对象的具体字段。属性的值就是统一的前缀了,看具体的sql就明了。

四 集合

4.1 集合

标签与标签差不多是一样的,后者是用来映射对象属性的,前者是用来映射集合属性的的。与对象属性一样,Mybatis有两种解决方法:

  1. 嵌套查询:通过查询两个sql,以其中一个sql的运行结果来加载另一个sql
  2. 嵌套结果映射:使用一个sql语句,将嵌套的结果映射来处理连接结果的重复子集

4.2 集合的嵌套Select查询

集合的嵌套查询与对象的嵌套查询的设计思想是差不多的,与其相比集合的嵌套多了两个注意点,第一个是ofType属性,这个属性的值是集合中存储的对象类型。另一个是javaType,它的值需要配成ArrayList。Mybatis有推断类型的功能,所以javaType这个属性是可以不写的,就像下面这样:

public class UserDto {
    private Integer id;
    private String name;
    private String password;
    private List<Blog> blogList;
}
<resultMap id="selectUserDto" type="UserDto">
  <result property="id" column="id"/>
  <result property="name" column="name"/>
  <result property="password" column="password"/>
  <collection property="blogList" column="id" ofType="Blog" select="selectBlog"/>
</resultMap>

<select id="selectUserDTo" resultMap="selectUserDto">
  select * from User where id = #{id}
</select>

<select id="selectBlog" parameterType="integer" resultType="com.evader.pojo.Blog">
  select blog_id as blogId,blog_name as blogName
  ,user_id as userId,another_user_id as anotherUserId from Blog where user_id = #{id}
</select>

4.3 集合的嵌套结果映射

集合的嵌套结果映射与对象的映射是差不多的,与嵌套select查询的区别在于它用一条sql就查出来所有的数据,并且全部映射到一个结果集里面,就像下面这样:

<resultMap id="selectUserDToSecond" type="UserDto">
  <result property="id" column="id"/>
  <result property="name" column="name"/>
  <result property="password" column="password"/>
  <collection property="blogList" ofType="blog" >
    <id property="blogId" column="blog_id"/>
    <result property="blogName" column="blog_name"/>
    <result property="userId" column="user_id"/>
  </collection>
</resultMap>


<select id="selectUserDToSecond" resultMap="selectUserDToSecond">
  select u.id,u.name,u.password,b.blog_id,b.blog_name,b.user_id,b.another_user_id
  from User u left join Blog b on u.id = b.user_id
  where id = #{id}
</select>

如果你想提取可重用的部分,那么你可以参考对象的方式,使用嵌套的方式来写。这里就不做示例了。