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的結果集映射就比較繁瑣了。