我有一个具有 boolean 值的实体,该 boolean 值作为“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;
}

但是,当我尝试运行测试以将其保存到数据库时,尽管我将初始化为Boolean.TRUE ,但仍会收到activeIndicator 的 NULL约束违规错误,如您在代码中所见以上。当我打开Hibernate的调试日志时,我能够验证它在insert语句中发送的是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
}

最佳答案

事实证明,问题不在于Hibernate,而在于Lombok。如果您使用的是@ Data,@ EqualsAndHashCodeOf,@ NoArgsConstructor,@ AllArgsConstructor或@Builder等注释,则您的域对象可能正在使用Lombok。 (Groovy具有一些类似的注释,只需检查您的导入是否使用 groovy.transform lombok 作为软件包前缀)

导致我出现问题的代码是在我的测试课中:

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

经过多次试验和错误,很明显,尽管我的JPA/Hibernate设置正确,但初始化默认设置为:
private Boolean activeIndicator = Boolean.TRUE;

没有被执行。

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

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

有许多解决方法,您可能要使用哪种方法取决于您的需求。
  • 自定义构造函数:可以代替构建器或与构建器一起创建自定义构造函数。当需要设置默认值时,请使用构造函数,而不要使用构建器。 -这种解决方案使您似乎不应该再为构建器而烦恼。您可能希望保留该构建器的唯一原因是,该构建器是否能够将创建委托(delegate)给您的构造器,从而在构建时获取默认值。我不知道这是否可行,因为我自己还没有测试过,所以我决定走另一条路。如果您进一步研究了此选项,请做出响应。
  • 覆盖构建方法:如果要广泛使用Builder,并且不想编写大量构造函数,则此解决方案适合您。您可以在自己的build方法实现中设置默认值,然后委托(delegate)super进行实际的构建。只要确保很好地记录您的代码,以便将来的维护者就知道在对象和build方法上都设置了默认值,因此无论如何初始化对象,都可以对其进行设置。
  • JPA PrePersist批注:如果对默认值的要求是针对数据库约束的,而您正在使用JPA或Hibernate,则可以使用此选项。该注释将在每次实体插入和更新之前运行它所附加的方法,而不管其初始化方式如何。很好,这样您就不必像在构建覆盖策略中那样必须在两个地方设置默认值。我选择了此选项,因为我的业务要求对此字段具有很强的默认要求。我将以下代码添加到我的实体类中,以解决此问题。我也删除了isActive字段声明的“= Boolean.TRUE”部分,因为它不再需要。
    @PrePersist
    public void defaultIsActive() {
        if(isActive == null) {
            isActive = Boolean.TRUE;
        }
    }
    
  • 10-02 00:34
    查看更多