基本用法
简介
一句话保存任意复杂的数据结构,自动找出DIFF并修改数据库,类似于React/Vue
右上角: 用户传入一个任意形状的数据结构,让Jimmer写入数据库。
这和其他ORM框架的save方法之间存在本质差异。 以JPA/Hibernate为 例,对象的普通属性是否需要被保存通过Column.insertable和Column.updatable控制, 关联属性是否需要被保存通过OneToOne.cascade、ManyToOne.cascade、OenToMany.cascade和ManyToOne.cascade控制。 然而,无论如何开发人员如何配置,JPA/Hibernate能够为你保存的数据结构的形状是固定的。
Jimmer采用完全不同方法,被保存的Jimmer对象虽然是强类型的,但具备动态性 (即, 不设置对象属性和把对象对象属性设置为null是完全同的两码事), 被设置的属性会被保存,而未被设置的属性会被忽略,这样,就可以保存任意形状的数据结构。
左上角: 从数据库中查询已有的数据结构,用于和用户传入的新数据结构对比。
用户传入什么形状的数据结构,就从数据查询什么形状的数据结构,新旧数据结构的形状完全一致。所以,查询成本和对比成本由用户传入的数据结构的复杂度决定。
下方: 对比新旧数据结构,找出DIFF并执行相应的SQL操作,让新旧数据一致:
- 橙色部分:对于在新旧数据结构中存在的实体对象,如果某些标量属性发生变化,修改数据
- 蓝色部分:对于在新旧数据结构中存在的实体对象,如果某些关联发生变化,修改关联
- 绿色部分:对于在新数据结构中存在但在旧数据结构中不存在实体对象,插入数据并建立关联
- 红色部分:对于在旧数据结构中存在但在新数据结构中不存在实体对象,对此对象进行脱钩,清除关联并有可能删除数据
基本概念
保存指令将一个任意形状的数据结构写入到到数据库中,例如
-
保存简单对象
- Java
- Kotlin
Book simpleBook = Immutables.createBook(draft -> {
draft.setName("SQL in Action");
draft.setEdition(1);
draft.setPrice(new BigDecimal("59.9"));
});
sqlClient.save(simpleBook);val simpleBook = Book {
name = "SQL in Action"
edition = 1
price = BigDecimal("59.9")
}
sqlClient.save(simpleBook) -
保存复杂数据结构
- Java
- Kotlin
Book complexBook = Immutables.createBook(draft -> {
draft.setName("SQL in Action");
draft.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);val complexBook = Book {
name = "SQL in Action"
edition = 1
price = BigDecimal("59.9")
store {
name = MANNING;
}
authors().addBy {
firstName = "Dmitry"
lastName = "Jamerov"
gender = Gender.MALE
}
authors().addBy {
firstName = "Svetlana"
lastName = "Isakova"
gender = Gender.FEMALE
}
}
sqlClient.save(complexBook)
API
Save指令为不同语言和不同的开发模式提供了多个API,但功能都一样
Spring Data API | 底层API | ||
---|---|---|---|
追求快捷性的API | 追求可配置性的API | ||
Java |
|
| |
Kotlin |
|
|
其中
-
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 (更详细的描述请参见保存模式)。这需要通过配置来实现
- Java
- Kotlin
BookStore store = ...略...;
sqlClient.save(store, SaveMode.INSERT_ONLY);val store = ...略...
sqlClient.save(store, SaveMode.INSERT_ONLY)insert
和update
方法是对INSERT_ONLY
和UPDATE_ONLY
的简写方式,上述代码可以简化为- Java
- Kotlin
BookStore store = ...略...;
sqlClient.insert(store);BookStore store = ...略...;
sqlClient.insert(store);
重要概念: 全量和增量
应用程序修改数据的UI设计可以被分为两种风格
-
全量提交
这类UI往往具有复杂的表单,并提供给一个最终按钮,用户对其进行编辑后,一次性提交表单内所有信息。
-
增量提交
这类UI没有提交按钮,用户每一次完成局部操作后,页面自动提交发生变化的部分,是一种碎片化的提交模式。
Save Command最大的价值在于简化全量提交模式功能的开发。对于两种不同的模式而言,其用法不一样。
全量提交 | 增量提交 |
---|---|
由Jimmer自动处理内部细节,对比新旧数据找出所有差异并执行相关的修改操作 (Jimmer独有的昂视) 使用保存指令 (参数往往是复杂的数据结构) | 业务代码用多个简单的操作组合来实现复杂操作,由用户自己处理内部细节 (和传统做法一样)。 综合使用多种方法: |
开发人员需要分析自己的业务场景,判断当前的修改操作是全量提交还是增量提交,从而做出正确的选择,不要滥用。
- Java
- Kotlin
TreeNode treeNode = Immutables.createTreeNode(food -> {
food
.setParent(null)
.setName("Food")
.addIntoChildNodes(drink -> {
drink
.setName("Drink")
.addIntoChildNodes(cocacola -> {
cocacola.setName("Cocacola");
})
.addIntoChildNodes(fanta -> {
fanta.setName("Fanta");
});
;
})
.addIntoAuthors(bread -> {
bread
.setName("Bread")
.addIntoChildNodes(baguette -> {
baguette.setName("Baguette");
})
.addIntoChildNodes(ciabatta -> {
ciabatta.setName("Ciabatta");
})
});
;
});
sqlClient.save(treeNode);
val treeNode = TreeNode {
parent = null
name = "Food"
childNodes().addBy {
name = "Drinks"
childNodes().addBy {
name = "Cocacola"
}
childNodes().addBy {
name = "Fanta"
}
}
childNodes().addBy {
name = "Bread"
childNodes().addBy {
name = "Baguette"
}
childNodes().addBy {
name = "Ciabatta"
}
}
}
sqlClient.save(treeNode)
这段代码企图保存一棵树
+-Food
|
+---+-Drinks
| |
| +-----Cocacola
| |
| \-----Fanta
|
\---+-Bread
|
+-----Baguette
|
\-----Ciabatta
其中,Food
为聚合根,而其他所有关联对象都是子节点。
-
聚合根节点
对应增量操作,最终生成一条INSERT语句或UPDATE语句
-
子节点(或称关联对象)
默认情况下,对应全量覆盖操作。
以
Food
为例,它有两个子节点Drinks
和Bread
。然而,这并非仅仅表示对Drinks
和Bread
这两个子节点简单地进行insert或update。 在Food
节点在数据库中已经存在的情况下,还要考虑其是否有非Drinks
和Breads
的其他子节点,并将这些子节点和父节点脱勾(比如,删除这些子节点)例如
数据库已有数据结构 用户期望保存的数据结构 +-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:
任何需要把复杂数据结构作为整体进行覆盖保存的场景。其中一种典型的场景是复杂表单,例如
在这个例子中,表单内嵌了子对象表格。用户可以对表单,包括内嵌的子表格,进行任意复杂操作,最终客户端将作为整个表单作为一个整体提交到服务端。
通过保存指令,服务端只需通过一行代码即可保存这个数据结构即可,无需考虑客户端提交的数据结构和数据库相比有何不同。
无论表单多复杂,关联关系嵌套了多深,都可以将其作为一个整体,使用一行代码保存。