Key
概念
@org.babyfish.jimmer.sql.Key
用于和修改篇/保存指令配合
最初,人们直接用业务字段作为表的主键。这是最直接最容易使用的方式,但存在以下问题。
-
主键不稳定
由于主键本身是业务字段,所以会被修改,这就导致主键不稳定。
虽然数据库中引用它的外键可以使用
on update cascade
来保持一致, 但是对于数据库外的系统,比如缓存,主键不稳定会导致很多问题。即使从技术层面能抽象出主键不稳定问题的统一解决办法,也不推荐,因为这样系统难以理解。
-
多表连接成本高
由于主键本身是业务字段,主键的类型很可能不是数字类型,而是相对较长的字符串类型, 而且还可能是多个列组合而成的联合主键,这会导致多表链接成本高。
为了解决以上问题,人们学会使用没有业务意义的数据作为主键,比如
- 数据库自动编号
- 数据库序列值
- UUID
- 雪花ID
为了让保存指令支持幂等性保存,Jimmer引入了两个概念:Id和Key
-
@Id: 主键
-
@Key: 业务性主键
-
如果Id本身就是业务属性(不推荐),无需指定Key,因为Id本身已经表示了业务层面的唯一约束。
-
如果Id没有业务意义(推荐),那么Key表示业务层面的唯一约束是什么。
-
请看下的例子
- Java
- Kotlin
@Entity
public interface TreeNode {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
long id();
@Key
String name();
@Key
@ManyToOne
@Nullable
TreeNode parent();
@OneToMany(mappedBy = "parent")
List<TreeNode> childNodes();
}
@Entity
interface TreeNode {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long
@Key
val name: String
@Key
@ManyToOne
val parent: TreeNode?
@OneToMany(mappedBy = "parent")
val childNodes: List<TreeNode>
}
这里,TreeNode
实体的Id采用自动编号,并没有业务意义。所以,为了更好地配合保存指令,
这里指定了key
,由两个属性构成:name
和parent
。
对应的DDL是
create table TREE_NODE(
ID bigint not null,
NAME varchar(20) not null,
PARENT_ID bigint
);
alter table TREE_NODE
add constraint PK_TREE_NODE
primary key(ID);
alter table TREE_NODE
add constraint UQ_TREE_NODE
/* highlight-next-line */
unique(NAME, PARENT_ID);
实体类型的属性name
和parent
被@Key修饰,对应于数据库中NAME
和PARENT_ID
列组成的UNIQUE约束 (或UNIQUE INDEX)。
这个唯一性约束可以用计算机文件系统做为类比。文件系统允许同名目录或文件,但如果限定父目录,则不允许同名。
通过这个例子,我们看到
-
Key可以由多个属性组成
-
外键可以作为Key的一部分
让我们再来看另外一个例子,在这个例子中,组成Key的属性全部是基于外键的属性
- Java
- Kotlin
@Entity
public interface OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
long id();
@Key
@ManyToOne
Order order();
@Key
@ManyToOne
Product product();
int quantity();
// Snapshot of `product.price`
BigDecimal unitPrice();
}
@Entity
interface OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long
@Key
@ManyToOne
val order: Order
@Key
@ManyToOne
val product: Product
val quantity: Int
// Snapshot of `product.price`
val unitPrice: BigDecimal
}
本文只介绍Key的配置,至于如何进一步使用,请参见保存指令。
多版本支持
Jimmer支持逻辑删除,该功能会导致被数据只会被隐藏起来,而非被真正删 除。
@Key
所定义的唯一性约束是针对未隐藏数据的,并非针对表中所有数据的,所以不能简单地按照@key
来定义唯一性约束。
-
当软删除标志是日期类型时
- Java
- Kotlin
Book.java@Entity
public interface Book {
@Key
String name();
@LogicalDeleted("now")
@Nullable
LocalDateTime deletedTime();
...省略其他代码...
}Book.kt@Entity
interface Book {
@Key
val name: String
@LogicalDeleted("now")
val deletedTime: LocalDateTime?
...省略其他代码...
}这时,把
@Key
所对应的列 (对这个例子而言,就是name
) 和逻辑删除标志列 (对这个例子而言,就是deletedTime
) 合并到一起定义唯一性约束即可,例如alter table BOOK
add constraint UQ_BOOK
unique(NAME, DELETED_TIME); -
当软删除标志是其他类型时
如果软删除标志不是日期类型,例如
- Java
- Kotlin
Book.java@Entity
public interface Book {
@Key
String name();
@LogicalDeleted("true")
boolean deleted();
...省略其他代码...
}Book.kt@Entity
interface Book {
@Key
val name: String
@LogicalDeleted("true")
val deleted: Boolean
...省略其他代码...
}此时,把
@Key
所对应的列和逻辑删除标志列在一起建立唯一性约束不再是可行的方法,使用唯一性条件索引是唯一的办法。警告不幸的是,并非所有数据库都支持条件索引,不同的数据库下创建条件索引的语法完全不同。这里仅以Postgres为 例
create unique index BOOK_KEY_INDEX
on BOOK(NAME)
// highlight-next-line
where DELETED = false;
动态覆盖
借助于实体中@Key
注解的配置,叫做静态配置。
有的时候,不同的业务可能对@key
有不同的要求。因此,@Key
配置可以在运行时被动态覆盖。
- Java
- Kotlin
sqlClient
.getEntities()
.saveCommand(book)
.setKeyProps(BookProps.NAME, BookProps.EDITION)
.execute();
sqlClient.save(book) {
.setKeyProps(Book::name, Book::edition)
}