JAVA持久层工具最佳实践探讨-查询篇

  在java领域,常见的持久层框架有许多。他们或多或少都有各自的优点和缺陷。  
  
          正如"软件工程没有银弹"。寄希望于单一的技术解决所有问题是不可行的。我们可以通过不同工具的组合使用,来面对不同场景下的各种问题。本文将讨论我所用过的一些持久层框架和库,来讨论各种场景和需求下解决方案。
          声明:由于本人对各个框架、工具的使用程度不同,不能保证对每一种工具给出的例子和点评都趋于完美。希望大家多多包涵,共同探讨。如果有更好的方案,可以在评论区留言。

        我们主要面对的问题可以用CRUD进行归类,我们将分别对查询、更新、删除、新增这四个方面对不同的工具进行探讨。此外如果有时间还会讨论一些常见的问题,比如分页,审计,多租户,逻辑删除等等。

接下来,我将使用讨论各个框架和库,并且对方案进行评估,评估的标准主要在于以下几个方面:可维护性、编码难度、调试难度

        首先,我们列举和介绍一些java持久层常用的工具、框架、或者库。Mybatis,Mybatis-plus ,JPA,Spring-Data,Hibernate,Query-Dsl。除此之外,java领域还有一些其他的持久层工具,比如JOOQ之类,不过不在本文的讨论范围内。

        我们大致可以把它们分为两个派系,Mybatis体系和JPA体系。

        Mybatis通过在XML中使用标签编写动态查询,其核心是一个字符串模板引擎的功能。Mybatis的学习成本很低,很容易上手,但是功能较少,不足以支撑整个DAO层。

        Mybatis-plus在其基础上更加(JPA)化,使得单表查询,以及单表动态条件查询的编写更加简化,所需的代码量大幅减少。

        JPA是java持久层规范,主要由一系列预定义的接口、注解规范组成。Hibernate作为基本上等同于jpa的标准实现,其原理跟Mybatis有本质的不同。Hibernate通过构建查询条件,组织语法树,然后发出sql。

        Spring-Data-JPA 则是基于jpa的进一步封装,主要提供了repository的抽象,可以通过方法名推导出sql。

        QueryDsl则是一个查询构建器,通过一系列api来构建查询逻辑,支持JPA、原生SQL、MongoDB等。


为了避免争议,在开始前,我们先对一些名词作出一些仅限于本文中的定义和解释:


复杂查询:在本文中指的是,需要连接多张表,根据不同条件进行动态查询。根据不同的条件,连接的表可能不同,连接表所用的条件可能不同(join on xxx),所查询的字段可能不同,where条件可能不同。
        比如以下伪sql,根据问号部分的条件,可能会有很多的组合。
        select ?,? from ?  join ? on ?, where ?  offset ? limit ?
除此之外,使用多个子查询等功能,也会使得sql变得复杂化。


接下来,我们根据不同的需求场景,对不同工具进行分析

第一板块:查询

特例:查询需要使用数据库特有的功能、或者使用存储过程。

        这种情况的本质是,你需要编写一段字符串,然后发出查询,仅此而已。这种情况下可能推荐Mybatis,主要原因是Java8对于字符串的支持不好。如果你使用更新的Java版本,也可以直接把多行字符串写在代码里,然后使用Spring-JDBC进行查询。

静态查询

指的是查询逻辑固定的情况。产生的sql逻辑不会因为条件而发生变化。

1、根据主键进行单表查询。

这种需求下,各种工具都有良好的支持,其中Mybatis-Plus的baseMapper提供了直接的实现,Spring-Data-JPA通过Repository也提供了直接的实现。

2、根据单表中字段条件进行单表查询。

这种情况下,Mybatis比较麻烦,需要手写sql。Mybatis-Plus提供QueryWrapper用于构建查询条件。Spring-Data-JPA可以通过方法名直接推导出sql,也比较方便。

3、不带额外条件的连表查询。

Mybatis需要手写join条件。而对于JPA体系,表或者对象之间的关联关系已经在实体模型中指定,正常情况下连表不需要指定join条件只要查找出一方,另一方对象也会被直接查询出来。

在连表时,如果对fetch的字段有要求,JPA使用的是声明式的写法,可能需要指定EntityGraph,会引入额外的学习成本。但是编码时候的代码量会少很多,可读性也不错。除此之外,JPA在关联时涉及到懒加载,还需要控制N+1查询还是left join的形式查询集合,以及BatchSize,要学习的点比较多。

4、带额外条件的连表查询。(指的是除了join条件外,还有各种where条件的查询。)

Mybatis需要手写所有sql,包括查询字段,要连接的表,连表条件,以及过滤条件等等。Hibernate的方式可以通过HQL,但是HQL并不会比SQL优雅多少,还有额外的学习负担。Spring-Data-JPA支持通过方法名判断多个表的多个条件。比如:findStudentsByTeacherId。这种方式在少量条件的多表查询中很简洁,但是也不适合连接很多表的情况,会造成方法名很长,而且很难读,表达能力也不够强。

因此我认为,在条件不是很复杂,连接的表比较少的情况下,Spring-Data-JPA有绝对优势,编码时间只要Mybatis的几十分之一,但是面对多张表以及复杂的连接、查询条件,Mybatis的纯sql可能会更胜一筹。

分析结论:

一: 通过对比可以分析出,Mybatis对于单表,以及轻量的连接查询几乎没有额外的支持。在实际生产环境中,大多依赖于代码生成技术来生成单表查询的代码。这种通过代码生成解决大量模板化的查询有几个问题:

        1 难以维护字段:当你修改了一个字段,或者说表修改了一个列,这个时候,你需要修改各种java类,然后要在各个mapper文件中找到这个字段进行修改,如果找漏了,就GG。

        2 code gen: 你可以选择先修改数据库,然后重新运行代码生成。但是这种情况下你需要处理生成的代码与手动修改过的代码的合并问题。针对这种问题,如果有更好的维护方案,欢迎在评论区提出。

因此推荐使用Mybatis-Plus进行单表查询, mapper文件中不要维护大量的模板sql。

二:而在JPA体系,Spring-Data-JPA几乎可以解决所有的静态轻量查询逻辑,有最高的开发效率。但是面对需要在多个表之间,判断很多条件的情况,可能并不合适。

相信对于简单查询,每个人都有自己的舒适圈,也不是本文主要的讨论点。接下来,我们将讨论如何处理复杂的动态查询。

动态查询

由于Mybatis对于各种动态查询的处理方法比较统一,我将先分析Mybatis的方案。

Mybatis可以通过IF标签来组织动态查询逻辑,还可以通过choose when等标签实现一些更加灵活的判断。比如

 <if test="id != null">  AND id = #{id}  </if>

使用Mybatis的各种标签进行组合,可以构建非常复杂的动态查询,但是使用各种标签进行逻辑判断有一些弊端:

1 魔法字符串

判断条件没有编译期检查,也很难得到IDE的语法提示,有巨大的编码失误的风险。这也是Mybatis最大的问题之一。

2 可读性差:

XML的可读性绝不会比JAVA代码高。其可读性很大程度上依赖于排版格式。

3 难以调试:

你无法通过debug调试这些XML文件。当你面对成百上千行充斥着各种标签的mapper,就会感受到痛苦了。

当然 用mybatis编写复杂sql也有一定的好处,如果忘记未来你还可能要维护它,在编写的时候还是比较舒适的,你可以畅游在复杂的表关系与字段条件中。并且如果需要DBA审查问题,看mapper文件可以基本还原出sq的结构。

从文章的一开始就提到过,Mybatis的核心之一是字符串模板引擎,当你理解了这个本质,就能理解在编码时,它能解决什么问题,以及需要面对的弊端。

Mybatis可以很好的面对这种场景:你对sql有着不错的掌握,而且不想花费很高的学习成本,那么Mybatis会是一种很好的选择。使用Mybatis可以很大程度上减少团队对技术的要求。只要会写sql,就可以工作了。

此外,如果你真的需要经常别写非常长且复杂的sql,那么Mybatis可能是不二之选,可以帮你很好的管理和组织那些sql语句。如果你愿意在项目中使用两个以上的工具作为持久层查询的解决方案,Mybatis适合用于解决复杂的问题,而简单的问题,可以交给其他工具解决。


接下来讨论其它工具

由于业务的需求,时常需要根据不同的条件发出不同的sql进行查询,我将把动态查询的需求分为几类。

1 根据非空条件查询

即根据对象不同属性是否是null 来判断要不要把这个属性加入到查询条件。该类查询的逻辑可以用伪代码描述:

User user//有一个User对象,可能是前端传过来的,也可能是自己构建的

if(user.id!=null){

user_table.id = {user.id}

}

if(user.name!=null){

user_table.name = {username}

}

这种情况下 Mybatis可以通过QueryWrapper构建查询条件。而Spring-Data-Jpa可以通过ExampleExecutor,直接传入一个Example对象进行查询。


2 根据不同条件进行动态查询。

假设有这么一个需求 给定一个User对象,和一个条件ge。如果User的age属性不为空 且ge为真,则查询年龄大于等于age的用户,如果ge为假 则查询年龄小于age的用户。

User user//有一个User对象,可能是前端传过来的,也可能是自己构建的

boolean ge;//某个参数

if(user.age!=null){

        if(gt){

                user_table.age >= {user.id}

        }else{

                user_table.age < {user.id}

        }

}

这种条件下,Mybatis-Plus的仍然可以用QueryWrapper组合查询条件。而在JPA这边,则需要使用Criteria api构建查询条件了。

Criteria api的设计,从易用性和符合直觉上来说,我觉得不如Mybatis-Plus的QueryWrapper直观。但是其功能强大,可以处理很复杂的连表查询、以及子查询等等。 其最大的问题是,学习成本较高,可读性较差。 

Criteria api建议不要出现在项目的普通DAO中,可以作为一个动态查询的基础设施,通过反射来根据传入的对象来构造不同的条件。


3 根据条件查找不同的表,以及使用不同的连表条件。

这种场景,其实可以考虑把一些条件在构建查询之前判断完,针对要查询的表不同,进行分类,然后再调用不同的具体的查询语句。

这种场景下,Criteria api可以提供比较完善的支持,但是会出现很难读懂的代码,需要有一些注释加以说明。此外Mybatis可以大显身手,依靠其字符串拼接的核心功能,可以实现任意的条件组合。但是Mybatis的XML标签也难以回避可维护性差的问题。

此外,如果你足够头铁,这个时候可以选择StringBuilder进行纯粹的字符串拼接,不过可读性、可维护性以及安全性都不如Mybatis,不做推荐,唯一的好处是容易debug,容易分析出具体的条件分支。如果使用这种写法,建议不要在拼接sql的时候直接拼接参数的值,建议先拼接占位符,最后再用PreparedStatement填充,避免sql注入。

复杂查询终极解决方案:QUERY-DSL

在复杂查询的场景下,无论是Mybatis,还是JPA 都需要面对复杂逻辑带来的可维护性差的问题。无论是大几百行的xml标签和sql、hql、还是通过Criteria api构建的java代码,都难以阅读。

在这种场景下,QueryDsl可能最佳的解决方案,其使用符合直觉的api构建sql。其编写的代码最接近sql,并且可调试,可阅读。

其依靠元模型提供类型安全的查询,并且使用中缀表达式,在JAVA领域,很难找到更完美的方案。

优点一  中缀写法:

作为比较,我们通过Mybatis-Plus和Query-Dsl编写同样的逻辑:

Mybatis-Plus

LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Employee::getName,"张三");
List<Employee> list = userService.list(queryWrapper);

Query-Dsl(使用JPA版本) 

QEmployee employee = QEmployee.employee;
JPAQueryFactory factory = new JPAQueryFactory(entityManager);
 List<Employee> list = factory
         .select(employee)
         .from(employee)
         .where(employee.name.eq("张三"))
         .fetch();

可以注意到,其中最大的区别在于 QueryDsl依靠元模型,可以使用中缀表达式,而Mybatis-Plus、Criteria api都需要编写前缀表达式模式的调用:
 

Mybatis-Plus:

queryWrapper.eq(Employee::getName,"张三")


Query-Dsl:

employee.name.eq("张三")

这在可读性上有着天与地的区别。就像你在写算数表达式的时候,会写(a+b),而不是写(+ab)

优点二:类型安全

先说MybatisPlus,MybatisPlus也在一定程度上提供了类型安全的功能。

queryWrapper.eq(Employee::getName,"张三");

这里的Employee::getName, 就是类型安全的,确保这个类存在这个字段 而不是魔法字符串

queryWrapper.eq("name","张三");

这里实际上是使用了java的反射功能,通过SerializedLambda接收lambda时,通过反射可以读出这个Lambda的方法名,比如这里的"getName",然后通过java Bean的标准命名格式,就可以解析出Employee的name这个成员属性。

这实际上是通过一个巧妙的技巧,解决了java中无法用一个简单的表达式来获取Field的缺陷。

比如 在java中,想要表达一个类的方法,可以使用Employee::getName,但是无法通过Employee::Name来表示类的一个Field。

而在其他语言中,比如kotlin,则可以直接通过这种写法获得一个KProperty,我觉得这是java的一个能力缺失,降低了元编程的能力。


而Query-Dsl为了解决这个问题,使用元模型类来描述一个类的所有属性。并且提供了查询条件构建的能力,而且结合ide,有可连续的代码提示。

这里的

employee.name

返回了一个StringPath,代表一个String类型的路径。

优点三:可读性强

MybatisPlus在多表连接时几乎无能为力(通过某些插件可以提供支持),而Query-DSL可以很好的构建复杂查询。

我们先构建一个OneToMany的关系

@Entity
@Data
public  class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;

    String name;
    
    @ManyToOne
    Boss boss;
}
@Entity
@Data
@Table
public class Boss {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;

    String name;
    
    @OneToMany
    Set<Employee> employees;
}

接下来,我们查询“员工的名字以 '李','王' 开头的老板”。

先看一下原生sql

select boss.name from boss

join employee

on boss.id=employee.boss_id

where employee.name like '李%'

or employee.name like '王%'

QEmployee employee = QEmployee.employee;
QBoss boss = QBoss.boss;
JPAQueryFactory factory = new JPAQueryFactory(entityManager);
List<String> list = factory
        .select(boss.name)
        .from(boss)
        .join(employee)
        .on(boss.id.eq(employee.boss.id))
        .where(employee.name.startsWith("李").or(employee.name.startsWith("王")))
        .fetch();

可以看到 QueryDsl在面对多表连接的复杂查询条件下,也有很好的表现能力与可读性。

ORM无关性:

Qurey Dsl不依赖于orm框架,也可以直接构建sql语句。

QueryDsl的弊端

queryDsl虽然能够应付各种查询场景,但是依赖元模型是他唯一的一个弊端。元模型的生成是一个注解处理器技术,跟Lombok是类似的,在构建期间,生成java代码或者class字节码。所以元模型类的管理就成为一个问题。

不过这种代码生成,跟Mybatis常用的代码生成有本质的区别,元模型并不需要被人为的修改,如果类进行了改动,只需要重新生成,不需要手动将新旧文件进行合并。

这里提供一个通用的方法解代码生成的问题,那就是把生成的代码纳入git管理。

元模型生成的是.java文件 而不是.class字节码文件,意味着你可以把输出路径从generated-sources改到源代码中(建议放在与main/java同级的另一个文件夹,不要和源码混在一起)。

这种方案不仅解决元模型生成的问题,其他依赖于代码生成技术的库、工具同样适用。

同时,你可以通过配置maven clean插件,在clean阶段删除掉整个生成的文件夹,然后在构建阶段重新生成,避免出现错误。


综上所述,我个人推荐两个组合用来实现查询需求:

Mybatis+Mybatis-Plus:其中Mybatis负责编写复杂查询,Mybatis-Plus编写单表查询,以及单表动态条件查询。

但是注意,QueryWrapper尽量不要编写在Service中。把所有查询条件,作为参数传入到mapper,在mapper里面根据查询条件做拼接。service层最好不要出现持久层的实现类。当然,这是理想化的情况。

Spring-Data-JPA+QueryDsl  :其中Spring-Data-JPA负责简单查询,还可以使用Example来处理简单的动态查询,QueryDsl用来编写复杂查询。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值