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是一個不錯的選擇。