导读
不要以为MyBatis更方便,如果只是用一些简单1+1=2,MyBatis会很简单,但当业务实体之间存在复杂的关联、继承关系时,MyBatis的结果集映射会就比较繁琐了。
技术潮流往往一波一波的:初中级开发者往往被裹夹其中,随波逐流,但这些人往往嗓门最大。十多年前,Hibernate如火如荼时,初中级开发者高呼:有了Hibernate,就不再需要JDBC、iBatis(后更名为MyBatis)了;现在,又换了另一波初中级开发者高呼:Hibernate已死,MyBatis才强大!
正如十多年前,我呼吁大家可以花点时间关注JDBC本身、iBatis一样,从那时候起,我就一直重复:Hibernate只是对JDBC的封装,如果不能精通JDBC,盲目使用Hibernate会带来致命的性能问题:
如果这些问题不能好好地理顺,盲目地依靠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的结果集映射就比较繁琐了。