Spring Data JPA 中的 @DynamicInsert

1、概览

Spring Data JPA 中的 @DynamicInsert 注解通过在 SQL 语句中只包含非 null 字段来优化插入操作。这一过程加快了结果查询的速度,减少了不必要的数据库交互。

虽然它提高了对具有许多可为空字段的实体的效率,但也引入了一些运行时开销。因此,在只有在排除空列的好处超过性能成本的情况下,有选择地使用它。

2、JPA 中 INSERT 的默认行为

使用 EntityManagerSpring Data JPAsave() 方法持久化 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