Ebean:一款被低估的ORM框架

ORM框架为什么不香?

对ORM框架的偏见

看了一些MyBaties与Hibernate进行对比的文章。可能是因为一些Hibernate历史原因,国内对于Hibernate普遍存在偏见,我摘抄了几点:

  1. hibernate是全自动,而mybatis是半自动

hibernate完全可以通过对象关系模型实现对数据库的操作,拥有完整的JavaBean对象与数据库的映射结构来自动生成sql。而mybatis仅有基本的字段映射,对象数据以及对象实际关系仍然需要通过手写sql来实现和管理。

  1. sql直接优化上,mybatis要比hibernate方便很多

由于mybatis的sql都是写在xml里,因此优化sql比hibernate方便很多。而hibernate的sql很多都是自动生成的,无法直接维护sql

  1. 应用场景

MyBatis 适合需求多变的互联网项目,例如电商项目、金融类型、旅游类、售票类项目等。 Hibernate 适合需求明确、业务固定的项目,例如 OA 项目、ERP 项目和 CRM 项目等。

也不知道是不是因为这些对Hibernate的偏见,导致大家对ORM框架也普遍存在偏见。

现状是不论大小公司,国内清一色地使用MyBaties。有时,我都不敢说,我喜欢使用ORM框架。

本文并不是一篇为Hibernate洗地的文章,而是介绍另一款比较小众的ORM框架:Ebean。

领域问题分析

介绍Ebean之前,我们需要弄清楚一个问题:为什么会有MyBaties和ORM这些框架?对于这个问题,我们无从下手,那么,我们将问题倒置:如果没有这些框架,会怎么样?

问题倒置的好处是我们立马就有了可下手的方向。我们找到了不使用框架的情况下,Java代码与数据库进行交互的代码:

public static void viewTable(Connection con) throws SQLException {
    String query = "select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES";
    try (Statement stmt = con.createStatement()) {
      ResultSet rs = stmt.executeQuery(query);
      while (rs.next()) {
        String coffeeName = rs.getString("COF_NAME");
        int supplierID = rs.getInt("SUP_ID");
        float price = rs.getFloat("PRICE");
        int sales = rs.getInt("SALES");
        int total = rs.getInt("TOTAL");
      }
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
    }
  }

这样的代码存在什么问题呢?

  1. 代码不易于维护:你需要知道每个字段在数据库中的类型,才知道该调用ResultSet的哪个方法;
  2. 代码重复:像COF_NAME这样的字段名,在整个代码仓库可能会飘落得到处都是;
  3. 不安全:手工拼装SQL带来的安全问题,不须多言。

以上三个问题,我们称之为ORM领域核心问题。

从目前市面上的解决方案来看,解决这些核心问题的方案,至少需要包含以下三个能力:

  1. 自动映射:在数据库与Java对象之间自动进行字段类型映射,而不是手工进行映射;
  2. 自动生成SQL:根据Java API自动生成SQL,而不是手写;
  3. 自动执行:自动执行,而不是手工直接操作JDBC接口。

MyBaties如何解决核心问题

MyBaties通过Mapper实现自动映射、自动执行。但是并没有实现自动生成SQL,也正是它称之为Mapper的原因。

public interface PersonMapper {

    @Insert("Insert into person(name) values (#{name})")
    public Integer save(Person person);

    @Select(
      "Select personId, name from Person where personId=#{personId}")
    @Results(value = {
      @Result(property = "personId", column = "personId"),
      @Result(property="name", column = "name"),
      @Result(property = "addresses", javaType = List.class,
        column = "personId", many=@Many(select = "getAddresses"))
    })
    public Person getPersonById(Integer personId);

    // ...
}

我个人很好奇,为什么MyBatise没有使用JPA规范,而是自己又创造一种注解。

MyBaties另一种通过XML的配置方式配置的,本文就不介绍了。想想当年,Spring也是使用XML进行配置Bean的,现在好像已经没有人这么干了。

说到底,MyBaties也是一个ORM框架。MyBatis-Plus插件的流行程度正好证明了这一点。所以,大家没有必要对ORM框架抱有偏见。:P

JPA小传

在介绍Ebean前,我们回顾一下JPA的历史。

JPA全称:Java Persistent API(Java持久化API)。它只是规范,并不是具体技术,其中Hibernate应该是最出名的实现之一了。Ebean也是具体实现之一。

值得注意的是我们应该可以认定这个规范没有限制我们只能用它将数据持久化到数据库(思路要打开)。即使,我们绝大多数时候,只用它持久化数据到数据库中。

它的版本历史如下:

  • 2006.5.11: JPA1.0作为JSR220规范的一部分发布。Ebean同年11月发布Bate测试版本;
  • 2009年:JPA2.0发布;
  • 2013年和2017年:JPA2.1和JPA2.2分别发布;
  • 2019年:JPA更名为Jakarta Persistence。
  • 2020年和2022年:Jakarta Persistence3.0和3.1版本分别发布。

此部分内容来自: https://handwiki.org/wiki/Java_Persistence_API https://en.wikipedia.org/wiki/Jakarta_Persistence

Ebean:一款被低估的ORM框架

Ebean最早于2006年11月13日发布了Bate测试版本。然后v1.0.0版本,在2008年11月24日,由它的作者Rob Bygrave发布到了SourceForge

后来Ebean迁到了Github。目前最新版本是2023年11月22日。从Github组织来看,Ebean的主要维护人只有:Rob Bygrave。18年的坚持,不得不佩服作者的毅力。

但这也成为我认为Ebean目前最大的问题:如果作者突然有个什么三长两短?社区应该如何应对。即使,它目前有将近100个contributor。

个人在2011左右接触到Play Framework的时候,了解到Ebean。Play框架使用Ebean作为它的JPA实现。当时就被它优秀的设计所吸引。

但是真正让我使用的是:它的设计非常符合我的DDD口味,同时鼓励充血模型的实体。

Ebean是如何实现自动映射的

在上文中,我们已经介绍了ORM领域核心问题:自动映射。这是JPA规范要解决的最重要的问题之一。Ebean实现了JPA规范定义的注解。

用户在字段上加上JPA的注解,然后在真正需要映射的时候,Ebean自动进行映射。以下是定义一个实体Person,它对应的表名是people。实体字段与数据库字段也有相应的映射:

@Entity
@Table(name="people") 
public class Person { 
    @Id
    @GeneratedValue
    private int id; 
    @Column(name="first_name", length=10)
    private String firstName; 
    @Column(name="last_name", length=10)
    private String lastName; 

在实体类上中定义Java类与数据库之间的映射关系,最大的好处就是DDL语句和数据库迁移SQL可以被工具自动生成。

假如你在Person类中增加一个email的字段,Ebean的的DDL特性就可以为你生成相应的建表语句。而Ebean的Migration工具就为你生成相应的alert语句。当然,这些语句的执行时机,还是由用户控制。

JPA本身提供了大量注解,Ebean还扩展了一些有用的注解:

  1. @DbJson注解,自动将对象转成JSON进行存储。如果你所使用的数据库支持JSON模式,你会非常喜欢这个注解;有了这个注解,你可能就不需要值对象注解了;
  2. @WhenCreated注解:自动设置对象的创建时间;
  3. @WhenModified注解:自动设置对象的修改时间;
  4. @DbMap注解:自动将Map结构,构建数据库映射到不同的数据类型,如果是Postgre就映射到HSTORE,其它数据库则映射到VARCHAR。
  5. @SoftDelete 软删除注解:当调用实体的delete方法时,只是软删除。只需要在实体中增加一个字段:
  @SoftDelete
  boolean deleted;

更多相关信息:https://ebean.io/docs/mapping/

Ebean是如何自动生成SQL并执行的

以下我们通过一个实例来展示Ebean相关的能力。

// Database是Ebean与数据库进行交互的主要接口
@Autowired
Database database;
@Test
public void crud() {

  Person customer = new Person();
  customer.setFirstName("Jack");
  customer.setLastName("J");

  // 这是为了让大家对Ebean的database类有一个感性认知
  database.save(customer);
  // 实际应用中,我通常是在Person类中定义一个save方法,并在内容调用database.save(this)。
  // 最终就是实现这样的调用效果:customer.save()

  // 批量执行存储。你猜这里应该是生成一条语句,还是多条语句?
  database.saveAll(customerList);


  // 根据ID查询对象。Ebean生成相应的select-where语句并执行
  Customer customerA = database.find(Customer.class, 1);
  // 当然你也可以只查询其中一个字段的值,Ebean将生成并执行:
  // select first_name from people where id=1;
  database.find(Customer.class).select("first_name").where().idEq(1).findSingleAttribute();

  customerA.setFirstName("Jane");
  // Ebean会识别出customerA要做的是修改,而不是创建新的记录。所以,生成alert语句并执行。
  database.save(customerA);

  // 当然,少不了大家关心的能否执行原生SQL
  String sql = "select id, first_name from customer where first_name like ?";
  Customer customer = database.findNative(Customer.class, sql)
      .setParameter("Jo%")
      .findOne();

  // 另,有时,我们会想念DTO,则可以这么写:
  List<CustomerDto> beans = database.findDto(CustomerDto.class, 
      "select id, first_name from customer where first_name = :name")
  .setParameter("name", "Rob")
  .findList();
  // CustomerDto是需要提前定义好的。


  // 删除记录
  database.delete(customer);

}

至此,已经把Ebean的解决方案介绍完成,由于篇幅有限,还请感兴趣的同学到官网学习。

Ebean的实体类增强技术

在Ebean的官网或者一些网上的文章,你会发现只要实体类继承了Ebean的BaseModel类,都会自动多出save方法以及其它方法。这Lombok与类似,只要加一个@Setter注解,类中就自动出多了相应的setter方法。

又或者,你会看到:

Person contact = new QContact()
    .firstName.equalTo("rob")
    .findOne();

QContact类是由Ebean生成的(在实体类前加一个Q字母代表查询类),方便用户使用链式调用来查询自己想要数据。而不是需要像database.find(Customer.class).select("first_name")这样手工写字段名。

发生以上的魔法是因为Ebean使用了增强(Enhancement)技术。这项技术必须嵌入到我们的IDE和构建工具中,否则相关代码的编译都不可能通过。

Enhancement技术虽然让我们少写代码,但是我们也要认清这门技术所带来的成本:它使我们的开发环境强依赖相应的插件。比如Maven必须要安装它的插件才能构建通过、IDE必须安装插件才能正常写代码。

幸运的是,我们可以选择不使用它的编译时生成的代码。IDE也就不需要安装相应的插件了。

我个人宁愿自己在实体中手写save方法,也不使用这项技术生成。另一个重要的考虑因素是:我不希望领域实体类依赖于具体实现技术。

然而,运行时,还是必须加上agent,即-javaagent:<路径>/ebean-agent.jar,以便Ebean对实体进行脏检查和懒加载支持。

使用经验

以下是一些个人的使用经验,仅供参考:

  1. JPA的所有API,并不是每一个都必须用到。比如字段上的@Basic(fetch=FetchType.LAZY)懒式加载,我就不建议使用。因为在实际工作中,你不能确保每个人都理解懒式加载的应用场景;比如它的JPQL,我们完全没有必要又另学一种SQL,再者Java API的调用方式才应该是推荐;
  2. 使用Java配置类对Ebean进行配置,而不是使用官网介绍properties配置。只有这样才足够灵活应对将来的多数据源需求;
  3. 在实体类中不要直接使用Ebean的技术,而是在实体类中调用repository接口,再由repository的实现调到Ebean。

如果各位想看更多的Ebean的文章,请点赞并转发。


Last modified on 2024-01-02