JPA/Hibernate 在字段上设置默认值时插入 NULL

AEv*_*ans 6 java hibernate jpa default lombok

我有一个具有布尔值的实体,该值作为“Y”或“N”字符持久存在于数据库中:

@Data
@Entity
@Table(name = "MYOBJECT", schema = "MYSCHEMA")
@EqualsAndHashCode(of = "id")
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MyObject {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "MYOBJECT_SEQ")
    @SequenceGenerator(name = "MYOBJECT_SEQ", sequenceName = "MYSCHEMA.MYOBJECT_SEQ")
    private Long id;

    @NotEmpty(message = "Name may not be empty")
    @SafeHtml(whitelistType = WhiteListType.NONE, message = "Name may not contain html or javascript")
    @Column(name = "NAME", nullable = false, length = 60)
    private String name;

    // Other fields... 

    @Type(type = "yes_no")
    @Column(name = "ACTIVE", length = 1, nullable = false)
    private Boolean isActive = Boolean.TRUE;

    @SafeHtml(whitelistType = WhiteListType.NONE, message = "username may not contain html or javascript")
    @Column(name = "USERNAME", nullable = false, length = 30)
    private String username;
}
Run Code Online (Sandbox Code Playgroud)

然而,当我尝试运行测试以将其保存到数据库时,尽管我将其初始化为 Boolean.TRUE,但我会收到一个NULL Constraint Violation 错误,尽管我将其初始化为 Boolean.TRUE,如您在上面的代码中所见。当我打开 Hibernate 的调试日志时,我能够验证它是否在插入语句中发送 NULL。下面是我的测试代码:

@Test
public void testSave() {
    // Setup Data
    final String NAME = "Test name";
    final String USERNAME = "Test username";

    MyObject stagedObject = MyObject.builder().name(NAME).username(USERNAME).build();

    // Run test
    MyObject savedObject = repo.save(stagedObject);
    entityManager.flush(); // force JPA to sync with db
    entityManager.clear(); // force JPA to fetch a fresh copy of the objects instead of relying on what's in memory

    // Assert
    MyObject fetchedObject = repo.findOne(savedObject.getId());
    assertThat(fetchedObject.getIsActive(), is(Boolean.TRUE));
    // other assertions
}
Run Code Online (Sandbox Code Playgroud)

AEv*_*ans 9

事实证明,问题不在于 Hibernate,而在于 Lombok。如果您使用的是@Data@EqualsAndHashCodeOf@NoArgsConstructor@AllArgsConstructor@Builder 等注释,则您的域对象可能正在使用 Lombok。(Groovy中有一些类似的注解,只是检查,如果你进口使用groovy.transform龙目岛作为包前缀)

导致我的问题的代码位在我的测试类中:

MyObject.builder().name(NAME).username(USERNAME).build();
Run Code Online (Sandbox Code Playgroud)

经过多次反复试验,很明显,虽然我的 JPA/Hibernate 设置是正确的,但初始化默认值:

private Boolean activeIndicator = Boolean.TRUE;
Run Code Online (Sandbox Code Playgroud)

没有被处决。

进行了一些挖掘,但显然Lombok 的 Builder 没有使用 Java 的初始化,而是使用它自己的方法来创建对象。这意味着当您调用 build() 时会忽略初始化默认值,因此如果您没有在构建器链中明确设置该字段,则该字段将保持为 NULL。请注意,正常的 Java 初始化和构造函数不受此影响;该问题仅在使用 Lombok 的构建器创建对象时才会出现。

详细说明此问题的问题以及 Lombok 关于他们为何不实施修复的解释在此处详细说明:https : //github.com/rzwitserloot/lombok/issues/663#issuecomment-121397262

对此有许多变通方法,您可能想要使用的方法取决于您的需要。

  • 自定义构造函数:可以创建自定义构造函数来代替构建器或与构建器一起创建。当您需要设置默认值时,请使用您的构造函数而不是构建器。- 这个解决方案让你看起来甚至不应该打扰建设者。您可能想要保留构建器的唯一原因是构建器是否能够将创建委托给您的构造函数,因此在构建时获得初始化的默认值。我不知道这是否可行,因为我自己还没有测试过,所以我决定走不同的路线。如果您进一步研究了此选项,请做出回应。
  • 覆盖构建方法:如果您打算广泛使用构建器并且不想编写一堆构造函数,则此解决方案适合您。您可以在自己的 build 方法实现中设置默认值,并委托给 super 来执行实际构建。只要确保很好地记录您的代码,以便未来的维护者知道在对象和构建方法中设置默认值,以便无论对象如何初始化都设置它们。
  • JPA PrePersist Annotation:如果您对默认值的要求是针对数据库约束,并且您正在使用 JPA 或 Hibernate,则可以使用此选项。此注释将在每次插入和更新实体之前运行它所附加的方法,而不管它是如何初始化的。这很好,这样您就不必像使用构建覆盖策略那样在两个地方设置默认值。我选择了这个选项,因为我的业务需求在这个领域有很强的默认要求。我将下面的代码添加到我的实体类中以解决问题。我还删除了 isActive 字段声明的“= Boolean.TRUE”部分,因为它不再需要。

    @PrePersist
    public void defaultIsActive() {
        if(isActive == null) {
            isActive = Boolean.TRUE;
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)