功能介绍
概念
右上角: 用户传入一个任意形状的数据结构,让Jimmer写入数据库。
这和其他ORM框架的save方法之间存在本质差异。 以JPA/Hibernate为例,对象的普通属性是否需要被保存通过Column.insertable和Column.updatable控制, 关联属性是否需要被保存通过OneToOne.cascade、ManyToOne.cascade、OenToMany.cascade和ManyToOne.cascade控制。 然而,无论如何开发人员如何配置,JPA/Hibernate能够为你保存的数据结构的形状是固定的。
Jimmer采用完全不同方法,被保存的Jimmer对象虽然是强类型的,但具备动态性 (即, 不设置对象属性和把对象对象属性设置为null是完全同的两码事), 被设置的属性会被保存,而未被设置的属性会被忽略,这样,就可以保存任意形状的数据结构。
左上角: 从数据库中查询已有的数据结构,用于和用户传入的新数据结构对比。
用户传入什么形状的数据结构,就从数据查询什么形状的数据结构,新旧数据结构的形状完全一致。所以,查询成本和对比成本由用户传入的数据结构的复杂度决定。
下方: 对比新旧数据结构,找出DIFF并执行相应的SQL操作,让新旧数据一致:
- 橙色部分:对于在新旧数据结构中存在的实体对象,如果某些标量属性发生变化,修改数据
- 蓝色部分:对于在新旧数据结构中存在的实体对象,如果某些关联发生变化,修改关联
- 绿色部分:对于在新数据结构中存在但在旧数据结构中不存在实体对象,插入数据并建立关联
- 红色部分:对于在旧数据结构中存在但在新数据结构中不存在实体对象,对此对象进行脱钩,清除关联并有可能删除数据
使用场景
应用程序修改数据的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
}