根对象保存模式
保存指令支持3种保存模式,控制聚合根本身的保存方式
-
UPSERT: 这是默认的模式。先通过查询判断被保存的聚合根对象是否存在:
-
如果不存在:执行INSERT语句
-
如果存在:执行UPDATE语句
-
-
INSERT_ONLY: 无条件执行INSERT语句
-
UPDATE_ONLY: 无条件执行UPDATE语句
保存模式仅影响聚合根对象,不影响其他关联对象。
对于关联对象而言,请参考关联对象保存模式。
指定对象的id
让我们来看一个例子
- Java
- Kotlin
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);
val book = new(Book::class).by {
id = 20
name = "SQL in Action"
edition = 1
price = BigDecimal("39.9")
store = makeIdOnly(BookStore::class, 2L)
}
sqlClient.save(book)
在这个例子中,save(book)
是一个简写方式,和save(book, SaveMode.UPSERT)
等价,因为UPSERT
是默认的保存方式。
执行代码会生成两句SQL
-
查询该对象在数据库中是否存在
select
tb_1_.ID
from BOOK tb_1_
where
tb_1_.ID = ? /* 20 */ -
第二条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。
- Java
- Kotlin
@Entity
public interface Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
long id();
String name();
int edition();
...省略其他属性...
}
@Entity
public interface Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long
val name: String
val edition: Int
...省略其他属性...
}
这里,Book.id
被@GeneratedValue
修饰,采用了自动编号。因此,插入Book时无需指定id。
为了插入id属性缺失的对象,有两种方法
@Key是保存指令中一个非常重要的概念,后文会做详细介绍,这里暂不讨论。
不指定@Key属性 (不推荐)
- Java
- Kotlin
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);
val book = new(Book::class).by {
name = "SQL in Action"
edition = 1
price = BigDecimal("39.9")
store = 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,如下:
- Java
- Kotlin
Book book = ...略...;
long newestBookId = sqlClient.save(book)
.getNewEntity()
.getId();
val book = ...略...
val newestBookId = sqlClient.save(book)
.modifiedEntity
.id
save
函数返回一个对象,包含 (但不限于) 两个属性:originalEntity
和modifiedEntity
。其中,originalEntity
就是期望被保存的原始数据结构;
而modifiedEntity
表示一个新的数据结构,其形状和originalEntity
的形状完全一致,它们的区别在于:
-
如果
originalEntity
中某些对象的id属性没有被指定,那么modifiedEntity
中对应的对象的id属性会被指定 -
如果
originalEntity
中某些对象具备乐观锁属性且对应了UPDATE语句,那么modifiedEntity
中对应的对象的乐观锁字段会持有新的版本号
因此,我们可以通过modifiedEntity.id
获取聚合根对象被插入数据库后被分配的id。
指定@Key属性 (推荐)
如果某个实体的id被指定了某种自动生成策略 (比如自动编号、序列、UUID、雪花ID),那么就会带来一个问题,实体的id属性除了充当唯一性标识外,没有任何实际的义务意义。
为了应对这种情况,Jimmer引入了一个叫做@Key
的概念,为实体引入一个具备实际业务意义的“第二主键”。限于篇幅,请点击
对保存指令而言,@Key
是一个极其重要的概念。
只要实体的id除了充当唯一性标识外没有任何实际的义务意义,就应该为实体配置key属性。
-
为实体类型
Book
定义@Key
属性为实体定义Key属性有两种方法
-
在实体上用注解静态配置,静态配置是全局的。
-
在代码中代码动态配置,但动态配置只影响当前保存指令。动态配置可以覆盖静态配置。
下面我们给出两种方法的示范
-
静态配置 (默认配置,供绝大部分业务使用)
- Java
- Kotlin
Book.java@Entity
public interface Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
long id();
@Key
String name();
@Key
int edition();
...省略其他属性...
}Book.kt@Entity
public interface Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long
@Key
val name: String
@Key
val edition: Int
...省略其他属性...
}这个例子中,
name
和edition
两个属性联合组成key,这表示,这两个属性联合起来形成一个唯一性约束,作为具备业务意义的第二主键。即,虽然书名可以重复,但仅限在不同的发行版本中重名,书名和发行版本联合起来一定唯一。这意味着你需要为表添加如下唯一约束:
alter table BOOK
add constraint UQ_BOOK
unique(NAME, EDITION); -
运行时覆盖 (仅针对单条保存指令,极少数有特殊需求的业务使用)
- Java
- Kotlin
sqlClient
.getEntities()
.saveCommand(book)
.setKeyProps(BookProps.NAME, BookProps.EDITION)
.execute();sqlClient.save(book) {
.setKeyProps(Book::name, Book::edition)
}信息若无特殊情况,绝大部分情况下,应该使用注解静态配置实体类型Key属性。
-
-
使用保存指令保存无id对象
- Java
- Kotlin
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);val book = new(Book::class).by {
name = "SQL in Action"
edition = 1
price = BigDecimal("39.9")
store = makeIdOnly(BookStore::class, 2L)
}
sqlClient.save(book)这次,将会生成两条SQL
-
查询该对象在数据库中是否存在
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 (
name
和edition
) 来判断即将被保存的无id对象。 -
第二条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属性 (对这个例子而言,就是
name
和edition
属性)如果某个key属性是一个基于外键 (无论真伪) 的关 联对象,那么这个关联对象
-
要么为null
-
要么至少具备id属性
-
否则,保存指令回抛出异常,指明对象的某些
key
属性未被设置。 -
总结
INSERT_ONLY
和UPDATE_ONLY
模式非常简单,无需总结,这里重点讨论UPSERT
模式。
如果实体类型配置了Key属性,那么UPSERT
模式的行为如下
前提 | 判断对象是否存在 | 判断结果 | 最终行为 |
---|---|---|---|
id属性被指定 | 按照id属性查询数据是否存在 | 数据存在 | 按照id更新被指定的属性,包括key属性 |
数据不存在 | 插入数据,因为id是已知的,无需id生成策略介入 | ||
id属性未被指定 | 按照所有key属性查询数据是否存在 | 数据存在 | 按照查询到的id更新被指定的属性,不包括key属性 |
数据不存在 | 插入数据,因为id是未知的,需要id生成策略介入 |