跳到主要内容

根对象保存模式

保存指令支持3种保存模式,控制聚合根本身的保存方式

  • UPSERT: 这是默认的模式。先通过查询判断被保存的聚合根对象是否存在:

    • 如果不存在:执行INSERT语句

    • 如果存在:执行UPDATE语句

  • INSERT_ONLY: 无条件执行INSERT语句

  • UPDATE_ONLY: 无条件执行UPDATE语句

警告

保存模式仅影响聚合根对象,不影响其他关联对象。

对于关联对象而言,请参考关联对象保存模式

指定对象的id

让我们来看一个例子

Book book = Objects.createBook(draft -> {
draft.setId(20L);
draft.setName("SQL in Action");
draft.setEdition(1);
draft.setPrice(new BigDecimal("39.9"));
draft.setStore(
ImmutableObjects.makeIdOnly(BookStore.class, 2L)
);
});
sqlClient.save(book);

在这个例子中,save(book)是一个简写方式,和save(book, SaveMode.UPSERT)等价,因为UPSERT是默认的保存方式。

执行代码会生成两句SQL

  1. 查询该对象在数据库中是否存在

    select
    tb_1_.ID
    from BOOK tb_1_
    where
    tb_1_.ID = ? /* 20 */
  2. 第二条SQL语句会因第一条SQL的执行结果的不同而不同

    • 如果第一条SQL无法查询到数据,插入

      insert into BOOK(ID, NAME, EDITION, PRICE, STORE_ID)
      values(
      ? /* 20 */, ? /* SQL in Action */,
      ? /* 1 */, ? /* 39.9 */, ? /* 2 */
      )
    • 如果第一条SQL查询到了已有数据,更新

      update BOOK
      set
      NAME = ? /* SQL in Action */,
      EDITION = ? /* 1 */,
      PRICE = ? /* 39.9 */,
      STORE_ID = ? /* 2 */
      where
      ID = ? /* 20 */
信息

某些数据库支持UPSERT (比如Postgres的insert into ... on conflict ...), 这会在Jimmer-1.0.0之前给予支持

这就是默认保存模式UPSERT的用法,另外两种模式的用法很简单:

  • INSERT_ONLY:

    sqlClient.save(book, SaveMode.INSERT_ONLY)

    sqlClient.insert(book)

    如果执行,则只会生成一条SQL语句,就是上面的INSERT语句,不会生成SELECT语句。

  • UPDATE_ONLY:

    sqlClient.save(book, SaveMode.UPDATE_ONLY)

    sqlClient.update(book)

    如果执行,则只会生成一条SQL语句,就是上面的UPDATE语句,不会生成SELECT语句。

不指定对象的id

在上面的例子中,我们为被保存的对象指定了id。

然而,很多时候,我们的实体的id具备自动增长策略,如果为了插入对象,则无需指定id。

Book.java
@Entity
public interface Book {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
long id();

String name();

int edition();

...省略其他属性...
}

这里,Book.id@GeneratedValue修饰,采用了自动编号。因此,插入Book时无需指定id。

为了插入id属性缺失的对象,有两种方法

@Key是保存指令中一个非常重要的概念,后文会做详细介绍,这里暂不讨论。

不指定@Key属性 (不推荐)

Book book = Objects.createBook(draft -> {
draft.setName("SQL in Action");
draft.setEdition(1);
draft.setPrice(new BigDecimal("39.9"));
draft.setStore(
ImmutableObjects.makeIdOnly(BookStore.class, 2L)
);
});
sqlClient.save(book);

很明显,被保存的对象的id属性并未被指定,Book类型也并未声明@Key属性。所以,这是一个既无id也无@Key的对象。

信息

本文中,我们所讨论的既无id也无key的对象是聚合根。

对于关联对象而言,既无id也无key在默认情况下会导致异常。这将在后续文档中讨论。

如果试图既无id也无key的聚合根对象,不同的保存模式对应不同的行为:

  • UPSERT: 不经查询和判断,直接插入对象,如同INSERT_ONLY一般

  • INSERT_ONLY: 插入对象

  • UPDATE_ONLY: 不执行任何SQL,同时告诉开发人员影响行数为0

上面的例子的保存模式是默认的UPSERT,因此,生成如下SQL

insert into BOOK(NAME, EDITION, PRICE, STORE_ID)
values
(? /* SQL in Action */, ? /* 1 */, ? /* 39.9 */, ? /* 2 */)

这里,并未指定ID列的值,采用数据库的自动编号。

开发人员也可以获取数据被插入后被自动分配的id,如下:

Book book = ......;
long newestBookId = sqlClient.save(book)
.getNewEntity()
.getId();

save函数返回一个对象,包含 (但不限于) 两个属性:originalEntitymodifiedEntity。其中,originalEntity就是期望被保存的原始数据结构; 而modifiedEntity表示一个新的数据结构,其形状和originalEntity的形状完全一致,它们的区别在于:

  • 如果originalEntity中某些对象的id属性没有被指定,那么modifiedEntity中对应的对象的id属性会被指定

  • 如果originalEntity中某些对象具备乐观锁属性且对应了UPDATE语句,那么modifiedEntity中对应的对象的乐观锁字段会持有新的版本号

因此,我们可以通过modifiedEntity.id获取聚合根对象被插入数据库后被分配的id。

指定@Key属性 (推荐)

如果某个实体的id被指定了某种自动生成策略 (比如自动编号、序列、UUID、雪花ID),那么就会带来一个问题,实体的id属性除了充当唯一性标识外,没有任何实际的义务意义。

为了应对这种情况,Jimmer引入了一个叫做@Key的概念,为实体引入一个具备实际业务意义的“第二主键”。限于篇幅,请点击

查看Key的精确定义。

提示

对保存指令而言,@Key是一个极其重要的概念。

只要实体的id除了充当唯一性标识外没有任何实际的义务意义,就应该为实体配置key属性。

  1. 为实体类型Book定义@Key属性

    为实体定义Key属性有两种方法

    • 在实体上用注解静态配置,静态配置是全局的。

    • 在代码中代码动态配置,但动态配置只影响当前保存指令。动态配置可以覆盖静态配置。

    下面我们给出两种方法的示范

    • 静态配置 (默认配置,供绝大部分业务使用)

      Book.java
      @Entity
      public interface Book {

      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      long id();

      @Key
      String name();

      @Key
      int edition();

      ...省略其他属性...
      }

      这个例子中,nameedition两个属性联合组成key,这表示,这两个属性联合起来形成一个唯一性约束,作为具备业务意义的第二主键。

      即,虽然书名可以重复,但仅限在不同的发行版本中重名,书名和发行版本联合起来一定唯一。这意味着你需要为表添加如下唯一约束:

      alter table BOOK
      add constraint UQ_BOOK
      unique(NAME, EDITION);
    • 运行时覆盖 (仅针对单条保存指令,极少数有特殊需求的业务使用)

      sqlClient
      .getEntities()
      .saveCommand(book)
      .setKeyProps(BookProps.NAME, BookProps.EDITION)
      .execute();
      信息

      若无特殊情况,绝大部分情况下,应该使用注解静态配置实体类型Key属性。

  2. 使用保存指令保存无id对象

    Book book = Objects.createBook(draft -> {
    draft.setName("SQL in Action");
    draft.setEdition(1);
    draft.setPrice(new BigDecimal("39.9"));
    draft.setStore(
    ImmutableObjects.makeIdOnly(BookStore.class, 2L)
    );
    });
    sqlClient.save(book);

    这次,将会生成两条SQL

    1. 查询该对象在数据库中是否存在

      select
      tb_1_.ID,
      tb_1_.NAME,
      tb_1_.EDITION
      from BOOK tb_1_
      where
      /* highlight-next-line */
      tb_1_.NAME = ? /* SQL in Action */
      and
      /* highlight-next-line */
      tb_1_.EDITION = ? /* 1 */

      这里,使用key (nameedition) 来判断即将被保存的无id对象。

    2. 第二条SQL语句会因第一条SQL的执行结果的不同而不同

      • 如果第一条SQL无法查询到数据,插入

        insert into BOOK(NAME, EDITION, PRICE, STORE_ID)
        values(
        ? /* SQL in Action */, ? /* 1 */, ? /* 39.9 */, ? /* 2 */
        )
      • 如果第一条SQL查询到了已有数据,更新

        update BOOK
        set
        NAME = ? /* SQL in Action */,
        EDITION = ? /* 1 */,
        PRICE = ? /* 39.9 */,
        STORE_ID = ? /* 2 */
        where
        ID = ? /* 20 */
      警告

      一旦为实体类型声明了Key属性,对于被保存的对象 (无论是否是聚合根) 而言

      • 要么指定id属性

      • 要么指定所有key属性 (对这个例子而言,就是nameedition属性)

        如果某个key属性是一个基于外键 (无论真伪) 的关联对象,那么这个关联对象

        • 要么为null

        • 要么至少具备id属性

      否则,保存指令回抛出异常,指明对象的某些key属性未被设置。

总结

INSERT_ONLYUPDATE_ONLY模式非常简单,无需总结,这里重点讨论UPSERT模式。

如果实体类型配置了Key属性,那么UPSERT模式的行为如下

前提判断对象是否存在判断结果最终行为
id属性被指定按照id属性查询数据是否存在数据存在按照id更新被指定的属性,包括key属性
数据不存在插入数据,因为id是已知的,无需id生成策略介入
id属性未被指定按照所有key属性查询数据是否存在数据存在按照查询到的id更新被指定的属性,不包括key属性
数据不存在插入数据,因为id是未知的,需要id生成策略介入