1. 怎么进行一对一或一对多映射?

首先准备 2 张表、4 个实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class GroupDO {
    private Integer id;
    private String groupName;
}

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserDO {
    private Integer id;
    private String username;
    private String password;
    private Integer groupId;
    private Sex sex;
}

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Test1 {
    private Integer id;
    private String groupName;
    private UserDO userDO;
}

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Test2 {
    private Integer id;
    private String groupName;
    private List<UserDO> users;


@Mapper
public interface UserMapper {
//    UserDO getUserById(Integer id);
    void insertUser(UserDO userDO);
    UserDO getUserById(Integer id);
    Test1 getTest1();
    Test2 getTest2();
}
}

一对一

对于getTest1方法,如果直接执行select * from t_group tg,t_user tu where tg.id = tu.group_id limit 1会发现其中 UserDO 为空,所以需要使用association 单独进行映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<resultMap id="test1ResultMap" type="com.danaizio.entity.Test1">
    <id property="id" column="id"/>
    <result property="groupName" column="group_name"/>
    <association property="userDO" resultMap="userResultMap"/>
</resultMap>

<resultMap id="userResultMap" type="com.danaizio.entity.UserDO">
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    <result property="password" column="password"/>
    <result property="sex" column="sex" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
    <result property="groupId" column="group_id"/>
</resultMap>

<select id="getTest1" resultMap="test1ResultMap">
    select * from t_group tg,t_user tu where tg.id = tu.group_id limit 1
</select>

一对多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<resultMap id="Test2ResultMap" type="com.danaizio.entity.Test2">
    <id property="id" column="group_id"/>
    <result property="groupName" column="group_name"/>
    <collection property="users" ofType="com.danaizio.entity.UserDO">
        <id property="id" column="user_id"/>
        <result property="username" column="username"/>
        <result property="password" column="password"/>
        <result property="groupId" column="group_id"/>
        <result property="sex" column="sex" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
    </collection>
</resultMap>

<select id="getTest2" resultMap="Test2ResultMap">
    SELECT
        g.id AS group_id, g.group_name,
        u.id AS user_id, u.username, u.password, u.group_id, u.sex
    FROM
        t_group g
            LEFT JOIN
        t_user u ON g.id = u.group_id
</select>

使用collection进行一对多映射,其中的 ofType 表示实体类中集合的元素类型

注意

由于两张表都有 id 字段,如果不用别名进行区分,会产生错误的查询结果,并且使用了别名的同时也要修改 resultMap 的 column 属性,它本来是对应的数据库字段名,使用了别名后要与查询语句中别名相同

2. 能映射枚举类型吗

定义一个枚举(同时参看上面 UserDO 的定义)

1
2
3
4
public enum Sex {
    MALE,
    FEMALE;
}

MyBatis 从一开始就自带了两个枚举的类型处理器 EnumTypeHandler EnumOrdinalTypeHandler,这两个枚举类型处理器可以用于最简单情况下的枚举类型。

  • EnumTypeHandler

    这个类型处理器是 MyBatis 中默认的枚举类型处理器,他的作用是将枚举的名字和枚举类型对应起来。对于 Sex 枚举来说,存数据库时会使用 “MALE” 或者 “FEMALE” 字符串存储,从数据库取值时,会将字符串转换为对应的枚举

  • EnumOrdinalTypeHandler

    这是另一个枚举类型处理器,他的作用是将枚举的索引和枚举类型对应起来。对于 YesNoEnum 枚举来说,存数据库时会使用枚举对应的顺序 0(MALE) 或者 1(FEMALE) 存储,从数据库取值时,会将整型顺序号(int)转换为对应的枚举。

具体使用

1
2
3
4
5
<insert id="insertUser" parameterType="com.danaizio.entity.UserDO">
    insert into t_user(username, password, sex, group_id) values (#{username}, #{password}, #{sex,typeHandler=org.apache.ibatis.type.EnumOrdinalTypeHandler}, #{groupId})
</insert>

<result property="sex" column="sex" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>

可以在#{}中指定,也可以在标签中指定

  • Mybatis 不单可以映射枚举类,Mybatis 可以映射任何对象到表的一列上。映射方式为自定义一个 TypeHandler,实现 TypeHandler 的 setParameter()和 getResult()接口方法。

  • TypeHandler 有两个作用,一是完成从 javaType 至 jdbcType 的转换,二是完成 jdbcType 至 javaType 的转换,体现为 setParameter()和 getResult()两个方法,分别代表设置 sql 问号占位符参数和获取列查询结果。

3. 延迟加载

Mybatis 支持 association 关联对象和 collection 关联集合对象的延迟加载,association 指的就是一对一,collection 指的就是一对多查询。在 Mybatis 配置文件中,可以配置是否启用延迟加载 lazyLoadingEnabled=true|false。

1
2
3
4
5
6
7
8
9
<resultMap id="departmentResultMap" type="com.example.Department">
    <id property="deptId" column="dept_id"/>
    <result property="deptName" column="dept_name"/>
    <collection property="employees" ofType="com.example.Employee" lazyLoad="true">
        <select id="selectEmployeesByDeptId" resultType="com.example.Employee">
            SELECT * FROM employee WHERE dept_id = #{deptId}
        </select>
    </collection>
</resultMap>

工作流程:

  1. 初始化加载:当第一次查询 Department 数据时,MyBatis 只会加载 Department 的基本信息,不会立即加载关联的 Employee 集合。
  2. 属性访问触发:当代码尝试访问 Department 对象的 employees 属性时,此时 MyBatis 的懒加载机制会被触发。
  3. 动态代理创建:MyBatis 使用 CGLIB 创建 Department 对象的代理,当代理对象的 getEmployees() 方法被调用时,会检查当前 employees 是否为空。
  4. 发送查询:如果 employees 为空,MyBatis 将执行 select 语句中定义的 SQL 查询,从数据库中获取与当前 Department 关联的所有 Employee 数据。
  5. 设置关联对象:查询结果返回后,MyBatis 会将 Employee 对象集合设置到 Department 的 employees 属性中。
  6. 完成调用:最后,代理对象的 getEmployees() 方法返回 employees 集合,完成对属性的访问。

    注意

    这里查询返回的 Department 其实是个代理对象,当调用 Department 的 getEmployees()并且结果为空的话就会被拦截而去执行预先定义好的 sql

4. 工作原理

  1. 先读取 mybatis.config 和其他 xxxMapper.xml 文件生成了一个配置类 Configuration

  2. 通过这个配置类构建 SqlSessionFactory,SqlSessionFactory 只是一个接口,构建出来的实际上是它的实现类的实例,一般我们用的都是它的实现类 DefaultSqlSessionFactory。

  3. 通过 SqlSessionFactory 生成 Sqlseession 执行 sql

  4. 执行 sql 中,需要四大组件配合,

    • Executor(执行器)
      SqlSession 只是一个门面,相当于客服,真正干活的是是 Executor,它提供了相应的查询和更新方法,以及事务方法。

    • StatementHandler(数据库会话器)
      以最常用的 PreparedStatementHandler 看一下它的 query 方法,其实在上面的 prepareStatement 已经对参数进行了预编译处理,到了这里,就直接执行 sql,使用 ResultHandler 处理返回结果。

    • ParameterHandler (参数处理器)
      PreparedStatementHandler 里对 sql 进行了预编译处理(类似#{}用?占位

      1
      2
      3
      public void parameterize(Statement statement) throws SQLException {
          this.parameterHandler.setParameters((PreparedStatement)statement);
      }

      这里用的就是 ParameterHandler,setParameters 的作用就是设置预编译 SQL 语句的参数(负责将 Java 对象的参数值设置到预编译 SQL 语句的占位符中)。里面还会用到 typeHandler 类型处理器,对类型进行处理。

    • ResultSetHandler(结果处理器)

      我们前面也看到了,最后的结果要通过 ResultSetHandler 来进行处理,handleResultSets 这个方法就是用来包装结果集的。Mybatis 为我们提供了一个 DefaultResultSetHandler,通常都是用这个实现类去进行结果的处理的。它会使用 typeHandle 处理类型,然后用 ObjectFactory 提供的规则组装对象,返回给调用者。

图解:
出处:面渣逆袭
出处:面渣逆袭

总结:

  1. 读取 MyBatis 配置文件——mybatis-config.xml 、加载映射文件——映射文件即 SQL 映射文件,文件中配置了操作数据库的 SQL 语句。最后生成一个配置对象。
  2. 构造会话工厂:通过 MyBatis 的环境等配置信息构建会话工厂 SqlSessionFactory。
  3. 创建会话对象:由会话工厂创建 SqlSession 对象,该对象中包含了执行 SQL 语句的所有方法。
  4. Executor 执行器:MyBatis 底层定义了一个 Executor 接口来操作数据库,它将根据 SqlSession 传递的参数动态地生成需要执行的 SQL 语句,同时负责查询缓存的维护。
  5. StatementHandler:数据库会话器,串联起参数映射的处理和运行结果映射的处理。
  6. 参数处理:对输入参数的类型进行处理,并预编译。
  7. 结果处理:对返回结果的类型进行处理,根据对象映射规则,返回相应的对象。

5. 插件

想要实现一个插件就要实现Myabtis的Interceptor 接口
MyBatis 仅可以编写针对 ParameterHandler、 ResultSetHandler、 StatementHandler、 Executor 这 4 种接口的插件,MyBatis 使用 JDK 的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这 4 种接口对象的方法时,就会进入拦截方法,具体就是 InvocationHandler 的 invoke() 方法,当然,只会拦截那些你指定需要拦截的方法。

其中:

1
2
3
4
5
@Intercepts({@Signature(
        type = Executor.class,  //确定要拦截的对象
        method = "update",        //确定要拦截的方法
        args = {MappedStatement.class,Object.class}   //拦截方法的参数
)})

最后再将这个插件注册到mybatis配置文件中

1
2
3
4
5
<plugins>
    <plugin interceptor="xxx.MyPlugin">
       <property name="dbType",value="mysql"/>
    </plugin>
</plugins>