Spring Data JPA 中的 @DynamicInsert
1、概览
Spring Data JPA 中的 @DynamicInsert 注解通过在 SQL 语句中只包含非 null
字段来优化插入操作。这一过程加快了结果查询的速度,减少了不必要的数据库交互。
虽然它提高了对具有许多可为空字段的实体的效率,但也引入了一些运行时开销。因此,在只有在排除空列的好处超过性能成本的情况下,有选择地使用它。
2、JPA 中 INSERT 的默认行为
使用 EntityManager
或 Spring Data JPA 的 save()
方法持久化 JPA 实体时,Hibernate 会生成一条 SQL 插入(INSERT
)语句。该语句包括每个实体列,即使某些列包含 null
值。因此,在处理包含许多可选字段的大型实体时,插入操作的效率会很低。
先来看一个简单的 Account
实体:
@Entity
public class Account {
@Id
private int id;
@Column
private String name;
@Column
private String type;
@Column
private boolean active;
@Column
private String description;
// Getter / Setter
}
为 Account
实体创建 JPA Repository
:
@Repository
public interface AccountRepository extends JpaRepository<Account, Integer> {}
现在, 保存一个新的 Account
对象:
Account account = new Account();
account.setId(ACCOUNT_ID);
account.setName("account1");
account.setActive(true);
accountRepository.save(account);
Hibernate 会生成一条包含所有列的 SQL 插入语句,即使只有少数列的值为非空:
insert into Account (active,description,name,type,id) values (?,?,?,?,?)
这种行为并不总是最佳的,特别是对于某些字段可能为 null
或稍后才初始化的大型实体。
3、使用 @DynamicInsert
我们可以在实体上使用 @DynamicInsert
注解来优化这种插入行为。使用后,Hibernate 将生成一条 SQL 插入语句,该语句只包含非 null
值的列,从而避免在 SQL 查询中出现不必要的列。
为 Account
实体添加 @DynamicInsert
注解:
@Entity
@DynamicInsert
public class Account {
@Id
private int id;
@Column
private String name;
@Column
private String type;
@Column
private boolean active;
@Column
private String description;
// Getter / Setter
}
现在,当我们保存一个某些列设置为 null
的新 Account
实体时,Hibernate 会生成一条优化过的 SQL 语句:
Account account = new Account();
account.setId(ACCOUNT_ID);
account.setName("account1");
account.setActive(true);
accountRepository.save(account);
生成的 SQL 只包含非空列:
insert into Account (active,name,id) values (?,?,?)
4、@DynamicInsert 原理
在 Hibernate 层,@DynamicInsert
会影响框架生成和执行 SQL 插入语句的方式。默认情况下,Hibernate 会预先生成并缓存静态 SQL 插入语句,这些语句包括每个映射列,即使在持久化实体时某些列包含空值。这是 Hibernate 性能优化的一部分,因为它会重复使用预编译的 SQL 语句,而不会每次都重新生成它们。
然而,当对实体应用 @DynamicInsert
注解时,Hibernate 会改变这种行为,并在运行时动态生成插入 SQL 语句。
5、什么时候使用 @DynamicInsert?
@DynamicInsert
注解是 Hibernate 的一项强大功能,但应根据具体情况有选择地使用。其中一种情况涉及具有许多可空字段的实体。当实体中的字段可能并不总是被填充时,@DynamicInsert
会通过从 SQL 查询中排除未设置的列来优化插入操作。这样可以减少生成查询的大小并提高性能。
当某些数据库列有默认值时,该注解也很有用。防止 Hibernate 插入 null
值可以让数据库使用默认设置来处理这些字段。例如,当插入记录时,created_at
列可能会自动设置当前时间戳。通过在插入语句中排除该字段,Hibernate 可以保留数据库的逻辑,防止覆盖默认行为。
此外,@DynamicInsert
在插入性能非常重要的情况下也很有优势。对于字段较多、可能无法全部填充的实体,该注解可确保只向数据库发送相关数据。这在高性能系统中尤为有用,因为最大限度地减少 SQL 语句的大小可显著提高效率。
6、什么时候不使用 @DynamicInsert?
虽然正如我们所看到的,该注解有很多优点,但在某些情况下它可能不是最佳选择。我们不应该使用 @DynamicInsert
的最具体的情况是,我们的实体大多具有非 null
值。在这种情况下,动态 SQL 生成可能会增加不必要的复杂性,而不会带来显著效果。在这种情况下,由于大多数字段都是在插入操作过程中填充的,因此 @DynamicInsert
提供的优化变得多余。
此外,不仅有很多非空字段的表不适合使用 @DynamicInsert
,属性很少的表也不适合使用 @DynamicInsert
。对于简单的实体或只有几个字段的小表,使用 @DynamicInsert
的优势微乎其微,而且排除 null
值所带来的性能提升也不大明显。
在涉及批量插入的情况下,@DynamicInsert
的动态性质可能会导致效率低下。由于 Hibernate 会为每个实体重新生成 SQL,而不是重复使用预先编译好的查询,因此批量插入的效率可能不如静态 SQL 插入。
在某些情况下,@DynamicInsert
可能无法很好地与复杂的数据库配置或 Schema 保持一致,尤其是涉及复杂的约束或触发器时。例如,如果 Schema 的约束要求某些列具有特定值,那么 @DynamicInsert
可能会省略这些为空的列,从而导致违反约束或出错。
假设我们的 Account
实体有一个数据库触发器,如果文章类型(type
)为空,触发器就会插入 UNKNOWN
:
CREATE TRIGGER `account_type` BEFORE INSERT ON `account` FOR EACH ROW BEGIN
IF NEW.type IS NULL THEN
SET NEW.type = 'UNKNOWN';
END IF;
END
如果实体不提供 type
值,Hibernate 会完全排除该列。因此,触发器可能无法触发。
7、总结
本文介绍了 Spring Data JPA 中 @DynamicInsert
注解的原理和用法,以及它的优缺点和应用场景。
Ref:https://www.baeldung.com/spring-data-dynamicinsert