跳到主要内容

基本用法

简介

备注

一句话保存任意复杂的数据结构,自动找出DIFF并修改数据库,类似于React/Vue

  • 右上角: 用户传入一个任意形状的数据结构,让Jimmer写入数据库。

    这和其他ORM框架的save方法之间存在本质差异。 以JPA/Hibernate为例,对象的普通属性是否需要被保存通过Column.insertableColumn.updatable控制, 关联属性是否需要被保存通过OneToOne.cascadeManyToOne.cascadeOenToMany.cascadeManyToOne.cascade控制。 然而,无论如何开发人员如何配置,JPA/Hibernate能够为你保存的数据结构的形状是固定的。

    Jimmer采用完全不同方法,被保存的Jimmer对象虽然是强类型的,但具备动态性 (即, 不设置对象属性和把对象对象属性设置为null是完全同的两码事), 被设置的属性会被保存,而未被设置的属性会被忽略,这样,就可以保存任意形状的数据结构。

  • 左上角: 从数据库中查询已有的数据结构,用于和用户传入的新数据结构对比。

    用户传入什么形状的数据结构,就从数据查询什么形状的数据结构,新旧数据结构的形状完全一致。所以,查询成本和对比成本由用户传入的数据结构的复杂度决定。

  • 下方: 对比新旧数据结构,找出DIFF并执行相应的SQL操作,让新旧数据一致:

    • 橙色部分:对于在新旧数据结构中存在的实体对象,如果某些标量属性发生变化,修改数据
    • 蓝色部分:对于在新旧数据结构中存在的实体对象,如果某些关联发生变化,修改关联
    • 绿色部分:对于在新数据结构中存在但在旧数据结构中不存在实体对象,插入数据并建立关联
    • 红色部分:对于在旧数据结构中存在但在新数据结构中不存在实体对象,对此对象进行脱钩,清除关联并有可能删除数据
提示

此功能的目的:把任意形状的数据结构作为一个整体,使用一行代码写入数据库,无论中间细节多复杂,都不用关心。

如果你了解Web领域的ReactVue,不难看出这个功能很像`Virtual DOM diff`。

     

基本概念

保存指令将一个任意形状的数据结构写入到到数据库中,例如

  • 保存简单对象

    Book simpleBook = Objects.createBook(draft -> {
    draft.setName("SQL in Action");
    dratt.setEdition(1);
    draft.setPrice(new BigDecimal("59.9"));
    });
    sqlClient.save(simpleBook);
  • 保存复杂数据结构

    Book complexBook = Objects.createBook(draft -> {
    draft.setName("SQL in Action");
    dratt.setEdition(1);
    draft.setPrice(new BigDecimal("59.9"));
    draft.applyStore(store -> {
    store.setName("MANNING");
    })
    draft.addIntoAuthors(author -> {
    author.setFirstName("Dmitry");
    author.setLastName("Jamerov");
    author.setGender(Gender.MALE);
    });
    draft.addIntoAuthors(author -> {
    author.setFirstName("Svetlana");
    author.setLastName("Isakova");
    author.setGender(Gender.FEMALE);
    })
    });
    sqlClient.save(simpleBook);

API

Save指令为不同语言和不同的开发模式提供了多个API,但功能都一样

Spring Data API底层API
追求快捷性的API追求可配置性的API
Java
  • JRepository.save
  • JRepository.saveCommand
  • JRepository.saveAll
  • JRepository.saveAllCommand
  • JRepository.insert
  • JRepository.update
  • JRepository.merge
  • JRepository.append
  • JSqlClient.save
  • JSqlClient.insert
  • JSqlClient.update
  • JSqlClient.merge
  • JSqlClient.append
  • JSqlClient.getEntities().save
  • JSqlClient.getEntities().saveCommand
  • JSqlClient.getEntities().saveAll
  • JSqlClient.getEntities().saveAllCommand
  • Kotlin
    • KRepository.save
    • KRepository.saveAll
    • KRepository.insert
    • KRepository.update
    • KRepository.merge
    • KRepository.append
    • KSqlCient.save
    • KSqlCient.insert
    • KSqlCient.update
    • KSqlCient.merge
    • KSqlCient.append
  • KSqlCient.entities.save
  • KSqlCient.entities.saveAll
  • 其中

    • Java方法中名称中包含Command的方法比较特殊,和其他方法立即执行保存指令不同,这些方法仅仅创建指令,并不马上执行,用户可以对其配置,最后调用execute方法执行。例如

      BookStore store = ......;
      sqlClient
      .getEntities()
      .saveCommand(store)
      .setPessimisticLock()
      .execute();
      • ❶ 创建保存指令,但并不马上执行

      • ❷ 进行某些配置

      • ❸ 完成配之后,最终调用execute执行保存指令

      Kotlin不需要saveCommand方法,它采用了另外一种写法

      val store = ......
      sqlClient.save(store) {
      setPessimisticLock()
      }
    • 名称中包含saveAll的方法表示保存多个对象,而非一个对象

    • 对于被保存的聚合根而言,具备三种保存模式:UPSERT(默认)、INSERT_ONLY、UPDATE_ONLY (更详细的描述请参见保存模式)。这需要通过配置来实现

      BookStore store = ......;
      sqlClient.save(store, SaveMode.INSERT_ONLY);

      insertupdate方法是对INSERT_ONLYUPDATE_ONLY的简写方式,上述代码可以简化为

      BookStore store = ......;
      sqlClient.insert(store);

    重要概念: 全量和增量

    应用程序修改数据的UI设计可以被分为两种风格

    • 全量提交

      这类UI往往具有复杂的表单,并提供给一个最终按钮,用户对其进行编辑后,一次性提交表单内所有信息。

    • 增量提交

      这类UI没有提交按钮,用户每一次完成局部操作后,页面自动提交发生变化的部分,是一种碎片化的提交模式。

    Save Command最大的价值在于简化全量提交模式功能的开发。对于两种不同的模式而言,其用法不一样。

    全量提交增量提交

    由Jimmer自动处理内部细节,对比新旧数据找出所有差异并执行相关的修改操作 (Jimmer独有的昂视)

    使用保存指令 (参数往往是复杂的数据结构)

    业务代码用多个简单的操作组合来实现复杂操作,由用户自己处理内部细节 (和传统做法一样)

    综合使用多种方法:

    开发人员需要分析自己的业务场景,判断当前的修改操作是全量提交还是增量提交,从而做出正确的选择,不要滥用。

    TreeNode treeNode = Objects.createTreeNode(food -> {
    food
    .setParent(null)
    .setName("Food")
    .addIntoChildNodes(drink -> {
    drink
    .setName("Drink")
    .addIntoChildNodes(cococola -> {
    cococola.setName("Cococola");
    })
    .addIntoChildNodes(fanta -> {
    fanta.setName("Fanta");
    });
    ;
    })
    .addIntoAuthors(bread -> {
    bread
    .setName("Bread")
    .addIntoChildNodes(daguette -> {
    daguette.setName("Baguette");
    })
    .addIntoChildNodes(ciabatta -> {
    ciabatta.setName("Ciabatta");
    })
    });
    ;
    });
    sqlClient.save(treeNode);

    这段代码企图保存一棵树

    +-Food
    |
    +---+-Drinks
    | |
    | +-----Cococola
    | |
    | \-----Fanta
    |
    \---+-Bread
    |
    +-----Baguette
    |
    \-----Ciabatta

    其中,Food为聚合根,而其他所有关联对象都是子节点。

    • 聚合根节点

      对应增量操作,最终生成一条INSERT语句或UPDATE语句

    • 子节点(或称关联对象)

      默认情况下,对应全量覆盖操作。

      Food为例,它有两个子节点DrinksBread。然而,这并非仅仅表示对DrinksBread这两个子节点简单地进行insert或update。 在Food节点在数据库中已经存在的情况下,还要考虑其是否有非DrinksBreads的其他子节点,并将这些子节点和父节点脱勾(比如,删除这些子节点)

      例如

      数据库已有数据结构用户期望保存的数据结构
      +-Food
      |
      |
      |
      +-----Meat(忽略子节点)
      |
      \-----Bread(忽略子节点)
      +-Food
      |
      +-----Drinks(忽略子节点)
      |
      |
      |
      \-----Bread(忽略子节点)

      最终,对子节点而言,会导致的操作为

      • 旧数据结构没有Drinks,新数据结构有Drinks,所以插入Drinks

      • 新旧数据结构都有Bread,所以修改Bread

      • 旧数据结构有Meat,新数据结构没有Meat,所以删除Meat

        子节点脱勾方式有多种,这里假设采用删除操作

    由此可见,默认情况下,除了聚合根外,其他的子节点的对应的操作都是全量覆盖,而非增量修改。

    Q & A

    如上所讨论,默认情况下,除了聚合根外,被保存数据结构中的关联对应全量覆盖操作,而非增量修改操作。

    Q:

    为什么除聚合根外的所有关联对象全部默认全量操作?

    A:

    底层数据库的INSERT、UPDATE和DELETE语句本身就是增量操作,即便使用只提供最简单的CRUD能力的SQL方案,也可以轻松实现按照增量修改数据库,这从来就不是业务系统开发的难点。

    真正复杂的问题,是把某个复杂数据结构作为整体进行覆盖保存。如果Jimmer不提供类似的能力,开发人员就需要编写复杂的代码去对比新旧数据发现需发生变化的部分,这会导致复杂数据的修改业务很复杂。

    另外还有一个好处,就是保证幂等性。

    Q:

    使用场景是什么?

    A:

    任何需要把复杂数据结构作为整体进行覆盖保存的场景。其中一种典型的场景是复杂表单,例如

    商品数量单价明细价删除
    268536
    238238
    总额:774
     

    在这个例子中,表单内嵌了子对象表格。用户可以对表单,包括内嵌的子表格,进行任意复杂操作,最终客户端将作为整个表单作为一个整体提交到服务端。

    通过保存指令,服务端只需通过一行代码即可保存这个数据结构即可,无需考虑客户端提交的数据结构和数据库相比有何不同。

    提示

    无论表单多复杂,关联关系嵌套了多深,都可以将其作为一个整体,使用一行代码保存。