MyBatis迷信者,清醒点!

  • 2019 年 10 月 6 日
  • 筆記

导读

不要以为MyBatis更方便,如果只是用一些简单1+1=2,MyBatis会很简单,但当业务实体之间存在复杂的关联、继承关系时,MyBatis的结果集映射会就比较繁琐了。

技术潮流往往一波一波的:初中级开发者往往被裹夹其中,随波逐流,但这些人往往嗓门最大。十多年前,Hibernate如火如荼时,初中级开发者高呼:有了Hibernate,就不再需要JDBC、iBatis(后更名为MyBatis)了;现在,又换了另一波初中级开发者高呼:Hibernate已死,MyBatis才强大!

正如十多年前,我呼吁大家可以花点时间关注JDBC本身、iBatis一样,从那时候起,我就一直重复:Hibernate只是对JDBC的封装,如果不能精通JDBC,盲目使用Hibernate会带来致命的性能问题:

  • 关联关系的1+N的性能陷阱?
  • 延迟加载的性能差异?何时应该使用延迟加载?何时应该关闭延迟加载?
  • 更新之前执行数据检查如何影响性能?
  • 如何控制Hibernate生成高效的SQL?
  • 二级缓存、查询缓存如何优化?

如果这些问题不能好好地理顺,盲目地依靠Hibernate去执行持久化操作,肯定会在项目中引入严重的性能陷阱。

这些原来以为Hibernate上手简单的初中级开发者,当他用熟之后往往才会发现Hibernate“很难驯服”,此时他们就会陷入对Hibernate的恐惧,转投MyBatis的怀抱。

现在,有些开发者对MyBatis的掌握,完全停留在简单的ResultSet映射上,对MyBatis的复杂一点的用法尚不熟悉,至于MyBatis底层运行机制、原理更是一无所知——这些人之所以推崇MyBatis,同样是因为MyBatis上手快。

但问题是:在编程世界里,哪个Hello world不简单呢?就像算术中1+1=2当然简单了,但你应该知道还有1+2=3,还有乘法、还有除法——

因此,对于那些只能简单使用MyBatis、连MyBatis官方文档都没能认真撸一遍的开发者而言,本文并不适合!因为本文要介绍的场景比MyBatis的官方文档的示例要复杂一些。

现在,我希望花点时间来对比一下MyBatis与Hibernate的在“关联查询”、“多态查询”上的的差异,希望让广大一知半解的初中级开发者清醒一点。

业务场景

本文用的实例包括4个实体类,这些实体类之间不仅存在继承关系,也存在复杂的关联关系。

本示例中一共包括Person、Employee、Manager、Customer四个实体类,其中Person持久化类还包含一个Address组件属性。

上面4个持久化类之间的继承关系是:Person派生出了Employee和Customer,而Employee又派生出了Manager。

上面4个实体之间的关联关系是:Employee和Manager之间存在双向的N-1关联关系,Employee和Customer之间存在双向的1-N关联关系。

图1显示了这4个实体之间的关系。

图1 4个实体之间的关联、继承关系

上面4个实体中,Person实体包含了一个Address复合属性,Address类比较简单,它就是一个普通的JavaBean。该类的代码如下:

public class Address  {      // 定义代表该Address详细信息的成员变量      private String detail;      // 定义代表该Address邮编信息的成员变量      private String zip;      // 定义代表该Address国家信息的成员变量      private String country;      // 无参数的构造器      public Address()      {      }      // 初始化全部成员变量的构造器      public Address(String detail , String zip , String country)      {          this.detail = detail;          this.zip = zip;          this.country = country;      }      // 省略所有的setter和getter方法      ...  }

至于本例用到Person、Customer、Employee、Manager这四个类,基本可通过图1的UML图来写出代码,此处不再给出。

本例用到的数据库脚本如下:

create database mybatis;    use mybatis;    create table person_inf (    person_id int primary key auto_increment,    address_country varchar(255),    address_detail varchar(255),    address_zip varchar(255),    name varchar(255),    gender char(1) NOT NULL,    comments varchar(255),    salary double,    title varchar(255),    department varchar(255),    employee_id int(11),    manager_id int(11),    person_type varchar(31) NOT NULL,    foreign key (manager_id) references person_inf (person_id),    foreign key (employee_id) references person_inf (person_id)  );    insert into person_inf values  (1, '中国', '天河', '434333', 'crazyit.org', '男', NULL, NULL, NULL, NULL, NULL, NULL, '普通人');  insert into person_inf values  (2, '美国', '加州', '523034', 'Grace', '女', NULL, 12000, '项目经理', '研发部', NULL, NULL, '经理');  insert into person_inf values  (3, '中国', '广州', '523034', '老朱', '男', NULL, 4500, '项目组长', NULL, NULL, 2, '员工');  insert into person_inf values  (4, '中国', '广州', '523034', '张美丽', '女', NULL, 5500, '项目分析', NULL, NULL, 2, '员工');  insert into person_inf values  (5, '中国', '湖南', '233034', '小贺', '男', '喜欢购物', NULL, NULL, NULL, 2, NULL, '顾客');  

本例需要执行的业务查询如下:

// 加载id为4的Employee  Employee emp2 = (Employee) personMapper.selectPersons(4);  System.out.println(emp2.getName());  System.out.println(emp2.getGender());  System.out.println(emp2.getSalary());  System.out.println(emp2.getTitle());  System.out.println(emp2.getAddress().getDetail());  // 获取emp2关联Manager  Manager mgr3 = emp2.getManager();  System.out.println(mgr3.getName());  System.out.println(mgr3.getGender());  System.out.println(mgr3.getSalary());  System.out.println(mgr3.getTitle());  System.out.println(mgr3.getDepartment());  System.out.println(mgr3.getAddress().getDetail());  // 获取mgr3关联的所有Employee  System.out.println(mgr3.getEmployees());  mgr3.getEmployees().forEach(e -> System.out.println(e.getManager().getName()));  // 获取mgr3关联的所有Customer  System.out.println(mgr3.getCustomers());  mgr3.getCustomers().forEach(c -> System.out.println(c.getName()));

从上面代码可以看到,程序既需要利用几个实体之间的关联关系,还要利用实体之间的继承关系。

Hibernate的解决方案

Hibernate默认采用一张表来保存整个继承树的所有记录,因此开发者只要为这些实体定义合适的关联、继承映射即可。

下面是Person类的注解。

@Entity  // 定义辨别者列的列名为person_type,列类型为字符串  @DiscriminatorColumn(name="person_type" ,      discriminatorType=DiscriminatorType.STRING)  // 指定Person实体对应的记录在辨别者列的值为"普通人"  @DiscriminatorValue("普通人")  @Table(name="person_inf")  public class Person  {      ...  }  

上面@DiscriminatorColumn注解为person_inf表定义了一个person_type列,该列作为辨别者列,用于区分每行记录对应哪个类的实例。

接下来@DiscriminatorValue("普通人")指定Person实体在辨别者列中保存”普通人“(此处也可使用整数)。

Employee只要通过@DiscriminatorValue指定辨别者列的值即可

@DiscriminatorValue("员工")  public class Employee extends Person  {      ...      // 定义和该员工保持关联的Customer关联实体      @OneToMany(cascade=CascadeType.ALL          , mappedBy="employee" , targetEntity=Customer.class)      private Set<Customer> customers          = new HashSet<>();      // 定义和该员工保持关联的Manager关联实体      @ManyToOne(cascade=CascadeType.ALL          ,targetEntity=Manager.class)      @JoinColumn(name="manager_id", nullable=true)      private Manager manager;      ...  }

上面程序还使用@OneToMany、@ManyToOne映射了Employee与Customer、Manager之间的关联关系。

剩下的Manager、Customer两个实体的代码基本与此相似,只要为它们增加@DiscriminatorValue修饰,并指定相应的value属性即可,并通过@OneToMany、@ManyToOne映射关联关系即可。

MyBatis的解决方案

记住

MyBatis并不是真正的ORM框架,它只是一个ResultSet映射框架,它的作用就是将JDBC查询的ResultSet映射成实体

由于MyBatis只是一个ResultSet映射框架,因此开发者需要自己编写复杂的SQL语句,这要求开发者必须有扎实的SQL功能。

简单来说一句话:那些只能写些简单的查询、多表查询的开发者几乎没法真正使用MyBatis。

由于MyBatis只是ResultSet映射,因此首先需要一条关联查询语句,这条语句是为了将Customer关联的Employee、Employee关联的Manager查询出来。下面是这条查询语句。

<select id="selectPersons" resultMap="personResult">      select p.*,      emp.person_id emp_person_id,      emp.address_country emp_address_country,      emp.address_detail emp_address_detail,      emp.address_zip emp_address_zip,      emp.name emp_name,      emp.gender emp_gender,      emp.salary emp_salary,      emp.title emp_title,      emp.department emp_department,      emp.employee_id emp_employee_id,      emp.manager_id emp_manager_id,      emp.person_type emp_person_type,      mgr.person_id mgr_person_id,      mgr.address_country mgr_address_country,      mgr.address_detail mgr_address_detail,      mgr.address_zip mgr_address_zip,      mgr.name mgr_name,      mgr.gender mgr_gender,      mgr.salary mgr_salary,      mgr.title mgr_title,      mgr.department mgr_department,      mgr.employee_id mgr_employee_id,      mgr.manager_id mgr_manager_id,      mgr.person_type mgr_person_type      from person_inf p      left join person_inf mgr      on p.manager_id = mgr.person_id      left join person_inf emp      on p.employee_id = emp.person_id      where p.person_id = #{id}    </select>

上面查询时,必须将Customer关联的Employee、Employee关联的Manager查询出来,否则就会导致N+1的性能陷阱。

注意

Hibernate用不好同样有N+1性能陷阱

接下来需要为上面的select定义映射关系,上面resultMap="personResult"属性指定了使用personResult执行映射,该映射定义如下。

  <resultMap id="personResult" type="Person" autoMapping="true">      <result property="id" column="person_id" />      <association property="address" javaType="address">        <result property="detail" column="address_detail" />        <result property="zip" column="address_zip" />        <result property="country" column="address_country" />      </association>      <!-- 定义辨别者列 -->      <discriminator javaType="string" column="person_type">        <case value="员工" resultMap="employeeResult"/>        <case value="顾客" resultMap="customerResult"/>        <case value="经理" resultMap="managerResult"/>      </discriminator>    </resultMap>  

为了完成Employee、Manager、Customer的映射,上面定义辨别者列,并针对不同的值定义了各自的映射。

例如employeeResult映射对应的映射如下:

  <resultMap id="employeeResult" type="Employee" extends="personResult"      autoMapping="true">      <association property="manager" javaType="manager"        columnPrefix="mgr_" resultMap="managerResult">      </association>      <collection property="customers" javaType="ArrayList"        column="person_id" ofType="customer" fetchType="lazy"        select="selectCustomersByEmployee">      </collection>      <!-- 定义辨别者列 -->      <discriminator javaType="string" column="person_type">        <case value="经理" resultMap="managerResult"/>      </discriminator>    </resultMap>

注意上面映射Employee时,由于它有多个关联的Customer实体,上面程序必须再次定义selectCustomersByEmployee来查询他的关联实体,因此还需要定义如下查询:

  <select id="selectCustomersByEmployee" resultMap="customerResult">      select p.*,      emp.person_id emp_person_id,      emp.address_country emp_address_country,      emp.address_detail emp_address_detail,      emp.address_zip emp_address_zip,      emp.name emp_name,      emp.gender emp_gender,      emp.salary emp_salary,      emp.title emp_title,      emp.department emp_department,      emp.employee_id emp_employee_id,      emp.manager_id emp_manager_id,      emp.person_type emp_person_type,      mgr.person_id mgr_person_id,      mgr.address_country mgr_address_country,      mgr.address_detail mgr_address_detail,      mgr.address_zip mgr_address_zip,      mgr.name mgr_name,      mgr.gender mgr_gender,      mgr.salary mgr_salary,      mgr.title mgr_title,      mgr.department mgr_department,      mgr.employee_id mgr_employee_id,      mgr.manager_id mgr_manager_id,      mgr.person_type mgr_person_type      from person_inf p      left join person_inf mgr      on p.manager_id = mgr.person_id      left join person_inf emp      on p.employee_id = emp.person_id      where p.person_type='顾客' and p.employee_id = #{id}    </select>

类似的,针对Manager的映射managerResult同样有多个关联的Employee实体,因此同样需要为之定义collection,如下的代码所示:

  <resultMap id="managerResult" type="Manager" extends="employeeResult"      autoMapping="true">      <collection property="employees" javaType="ArrayList"        column="person_id" ofType="employee" fetchType="lazy"        select="selectEmployeesByManager">      </collection>    </resultMap>

上面collection元素定义了Manager关联的多个Employee实体,该实体又需要额外的selectEmployeesByManager进行查询,因此还需要为selectEmployeesByManager定义查询 ,如下代码所示。

<select id="selectEmployeesByManager" resultMap="employeeResult">      select p.*,      emp.person_id emp_person_id,      emp.address_country emp_address_country,      emp.address_detail emp_address_detail,      emp.address_zip emp_address_zip,      emp.name emp_name,      emp.gender emp_gender,      emp.salary emp_salary,      emp.title emp_title,      emp.department emp_department,      emp.employee_id emp_employee_id,      emp.manager_id emp_manager_id,      emp.person_type emp_person_type,      mgr.person_id mgr_person_id,      mgr.address_country mgr_address_country,      mgr.address_detail mgr_address_detail,      mgr.address_zip mgr_address_zip,      mgr.name mgr_name,      mgr.gender mgr_gender,      mgr.salary mgr_salary,      mgr.title mgr_title,      mgr.department mgr_department,      mgr.employee_id mgr_employee_id,      mgr.manager_id mgr_manager_id,      mgr.person_type mgr_person_type      from person_inf p      left join person_inf mgr      on p.manager_id = mgr.person_id      left join person_inf emp      on p.employee_id = emp.person_id      where p.person_type='员工' and p.manager_id = #{id}    </select>

看到这些映射了吗?你晕了吗?

最后的结论

不要以为MyBatis更方便,如果只是用一些简单1+1=2,MyBatis会很简单,但当业务实体之间存在复杂的关联、继承关系时,MyBatis的结果集映射就比较繁琐了。

当然,本文并不是想告诉你,MyBatis一无是处。记住:MyBatis只是见到那的ResultSet映射,如果程序并不需要关注实体之间的关联、继承关系,用MyBatis是一个不错的选择。