前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >MyBatis迷信者,清醒点!

MyBatis迷信者,清醒点!

作者头像
Java技术江湖
发布2019-09-24 11:55:58
8360
发布2019-09-24 11:55:58
举报
文章被收录于专栏:微信公众号【Java技术江湖】

导读

不要以为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。该类的代码如下:

代码语言:javascript
复制
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图来写出代码,此处不再给出。

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

代码语言:javascript
复制
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, '顾客');

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

代码语言:javascript
复制
// 加载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类的注解。

代码语言:javascript
复制
@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指定辨别者列的值即可

代码语言:javascript
复制
@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查询出来。下面是这条查询语句。

代码语言:javascript
复制
<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执行映射,该映射定义如下。

代码语言:javascript
复制
  <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映射对应的映射如下:

代码语言:javascript
复制
  <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来查询他的关联实体,因此还需要定义如下查询:

代码语言:javascript
复制
  <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,如下的代码所示:

代码语言:javascript
复制
  <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定义查询 ,如下代码所示。

代码语言:javascript
复制
<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是一个不错的选择。
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-07-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java技术江湖 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档