功能介绍
概念
保存指令允许开发人员保存任意形状的数据结构,而非保存简单的对象。
在默认情况下,即在AssociatedSaveMode为REPLACE情况下,Jimmer会用被保存结构去全量替换数据库中已有的数据结构,如图所示:
右上角: 用户传入一个任意形状的数据结构,让Jimmer写入数据库。
左上角: 从数据库中查询已有的数据结构,用于和用户传入的新数据结构对比。
用户传入什么形状的数据结构,就从数据查询什么形状的数据结构,新旧数据结构 的形状完全一致。所以,查询成本和对比成本由用户传入的数据结构的复杂度决定。
下方: 对比新旧数据结构,找出DIFF并执行相应的SQL操作,让新旧数据一致:
- 橙色部分:对于在新旧数据结构中存在的实体对象,如果某些标量属性发生变化,修改数据
- 蓝色部分:对于在新旧数据结构中存在的实体对象,如果某些关联发生变化,修改关联
- 绿色部分:对于在新数据结构中存在但在旧数据结构中不存在实体对象,插入数据并建立关联
- 红色部分:对于在旧数据结构中存在但在新数据结构中不存在实体对象,对此对象进行脱钩,清除关联并有可能删除数据
和其他ORM不同,Jimmer无需在实体模型上描述数据如何保存
- 某些标量属性是否需要被保存以JPA为例,通过Column.insertable和Column.updatable控制。
- 某些关联属性是否需要被保存
Jimmer采用完全不同的策略,其实体对象并非POJO,可以灵活地控制数据结构的形状。
即,实体对象具备动态性,不为实体对象指定某个属性和将实体的某个属性指定为null,是完全不同的事情。
对于任何一个实体对象而言,Jimmer只会保存被指定的属性,而忽略未指定的属性。
因此,Jimmer无需在实体建模时考虑数据的保存行为,而是在运行时通过被保存的数据结构自身来描述期望的行为,具备绝对的灵活性。
使用场景
应用程序修改数据的UI设计可以被分为两种风格
-
全量提交
这类UI往往具有复杂的表单,并提供给一个最终按钮,用户对其进行编辑后,一次性提交表单内所有信息。
-
增量提交
这类UI没有提交按钮,用户每一次完成局部操作后,页面自动提交发生变化的部分,是一种碎片化的提交模式。
Save Command最大的价值在于简化全量提交模式功能的开发。对于两种不同的模式而言,其用法不一样。
全量 提交 | 增量提交 |
---|---|
由Jimmer自动处理内部细节,对比新旧数据找出所有差异并执行相关的修改操作 (Jimmer独有的昂视) 使用保存指令 (参数往往是复杂的数据结构) | 业务代码用多个简单的操作组合来实现复杂操作,由用户自己处理内部细节 (和传统做法一样)。 综合使用多种方法: |
开发人员需要分析自己的业务场景,判断当前的修改操作是全量提交还是增量提交,从而做出正确的选择,不要滥用。
示范
实际开发中,被保存的数据都由客户端提交,服务端被动接受即可 (比如,Spring的@RequestBody
)。
但是这里,为了简化讨论,我们直接硬编码书写被保存的对象,所以被保存参数的代码相对多一点。
-
保存简单对象
- Java
- Kotlin
sqlClient.save(
Immutables.createBook(draft -> {
draft.setName("GraphQL in Action");
draft.setEdition(4);
draft.setPrice(new BigDecimal("59.9"));
})
);sqlClient.save(
Book {
name = "GraphQL in Action"
edition = 4
price = BigDecimal("59.9")
}
)备注这里,被保存的对象的id属性并没有被指定。Jimmer会根据
name
和edition
属性去判断数据库中是否存在相关的数据, 从而决定应该INSERT
还是UPDATE
。这是因为在实体定义中,
Book.name
和Book.edition
被@org.babyfish.jimmer.sql.Key
修饰, 这里的文章仅做快速预览,对此不做深入探讨,有兴趣者可以查看映射篇/进阶映射/Key和修改篇/保存指令。 -
保存多个对象形成的数据结构
- Java
- Kotlin
sqlClient.save(
Immutables.createBook(draft -> {
draft.setName("GraphQL in Action");
draft.setEdition(4);
draft.setPrice(new BigDecimal("59.9"));
draft.applyStore(store -> {
store.setName("MANNING");
store.setWebsite("https://www.manning.com");
});
draft.addIntoAuthors(author -> {
author.setFirstName("Bob");
author.setLastName("Rockefeller");
author.setGender(Gender.MALE);
});
draft.addIntoAuthors(author -> {
author.setFirstName("Eve");
author.setLastName("Procello");
author.setGender(Gender.FEMALE);
});
})
);sqlClient.save(
Book {
name = "GraphQL in Action"
edition = 4
price = BigDecimal("59.9")
store {
name = "MANNING"
website = "https://www.manning.com"
}
authors().addBy {
firstName = "Bob"
lastName = "Rockefeller"
gender = Gender.MALE
}
authors().addBy {
firstName = "Eve"
lastName = "Procello"
gender = Gender.FEMALE
}
}
);
和其他ORM的本质区别
前文中,我们演示了两个例子,一个讲述如何保存简单的对象,一个讲述如何保存聚合根对象并级联保存更多的关联对象。
很明显,Jimmer的保存指令的功能既可以表现得很简单也可以表现得复杂,关键是看用户传递的动态实体所表达的数据结构是简单的还是复杂的。
Jimmer并没有像传统ORM一样为关联属性提供 配置cascade
选项,因为根本不需要。动态实体为Jimmer的保存功能赋予了无限种可能,自然不必限定为某种固定的配置。
这种绝对的灵活性,存在很多妙用,比如:将id为100的书籍的价格修改为60,传统ORM和Jimmer做法不同。
-
传统ORM (以JPA为例) 采用先查再改的方式,虽然直观,但是浪费性能
Book book = entityManager.find(Book.class, 100L);
if (book != null) {
book.setPrice(new BigDecimal(60));
// entityManager.merge(book); //假设当前JPA事务上下问存在,可省略
} -
Jimmer的做法,凭空捏造一个残缺对象,直接update
- Java
- Kotlin
boolean matched = sqlClient.update(
Immutables.createBook(draft -> {
draft.setId(100L);
draft.setPrice(new BigDecimal(60));
// 除了`id`和`price`外,其他属性都没有被指定
// 所以除了`price`属性会被修改外,其他属性不会被影响
})
).getTotalAffectedRowCount() != 0;val matched = sqlClient.update(
Book {
id = 100L
price = BigDecimal(60)
// 除了`id`和`price`外,其他属性都没有被指定
// 所以除了`price`属性会被修改外,其他属性不会被影响
}
).totalAffectedRowCount != 0
注意:不能对外直接暴露
保存任意形状的数据结构的功能过于强大,直接暴露会导致绝大的安全隐患,例如
- Java
- Kotlin
@RestController
public class BookController {
private final JSqlClient sqlClient;
public BookController(JSqlClient sqlClient) {
this.sqlClient = sqlClient;
}
@PutMapping("/book")
pubic int saveBook(
@RequestBody Book book
) {
return sqlClient
.save(book)
.getTotalAffectedRowCount();
}
}
class BookController(
private val sqlClient: KSqlClient
) {
@PutMapping("/book")
fun saveBook(
@RequestBody book: Book
): Int =
sqlClient
.save(book)
.totalAffectedRowCount
}
这个方法可以工作,也非常强大,客户端可以上传任何以Book
为聚合根的数据结构让服务端保存。
但,这也是危险的,你无法限制客户端上传的数据结构的复杂度,客户端可以通过这个Api随意修改任何深度的关联数据。
即使你想办法对book
参数的形状进行验证和限制,也是非常容易疏忽和犯错的。
因此,强大的Jimmer数据保存能力只能在服务内部作为底层支撑,不能用动态实体作为输入参数并将此能力直接暴露给远程客户端,因为这会导致安全门户大开。
如何安全地暴露Jimmer的数据保存能力,请继续阅读下一篇文章:暴露功能