当在字段上设置默认值时,JPA/Hibernate 插入 NULL
JPA / Hibernate is Inserting NULL When Default Value is Set on Field
我有一个实体,它有一个布尔值,作为 '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;
}
然而,当我尝试 运行 测试将其保存到数据库时,我会收到 activeIndicator 的 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
}
原来问题不在Hibernate,而在Lombok。如果您使用的注释例如 @Data、@EqualsAndHashCodeOf、@NoArgsConstructor、@AllArgsConstructor,或@Builder,你的领域对象可能正在使用Lombok。 (Groovy 有一些类似的注释,只需检查您的导入是否使用 groovy.transform 或 lombok 作为包前缀)
导致我出现问题的代码位在我的测试中 class:
MyObject.builder().name(NAME).username(USERNAME).build();
经过反复试验,很明显,虽然我的 JPA/Hibernate 设置是正确的,但初始化默认值:
private Boolean activeIndicator = Boolean.TRUE;
没有被执行。
它进行了一些挖掘,但显然 Lombok 的生成器不使用 Java 的初始化,而是使用它自己的方法来创建对象。这意味着当您调用 build() 时 初始化默认值被忽略,因此如果您没有在构建器链中明确设置它,该字段将保持为 NULL。 请注意,正常的 Java 初始化和构造函数不受此影响;只有在使用 Lombok 的构建器创建对象时才会出现此问题。
详细说明此问题的问题以及 Lombok 为何不实施修复的解释详见此处:https://github.com/rzwitserloot/lombok/issues/663#issuecomment-121397262
对此有很多解决方法,您可能想要使用哪种方法取决于您的需要。
- Custom Constructor(s):可以创建自定义构造函数来代替构建器,或与构建器一起创建。当您需要设置默认值时,请使用您的构造函数而不是构建器。 - 这个解决方案让你看起来甚至不应该为建造者而烦恼。您可能希望保留构建器的唯一原因是构建器是否能够将创建委托给您的构造函数,从而在构建时获得初始化的默认值。我不知道这是否可行,因为我自己还没有测试过,所以我决定走另一条路。如果您进一步研究了此选项,请务必回复。
- 重写构建方法: 如果您要广泛使用构建器并且不想编写一堆构造函数,那么此解决方案适合您。您可以在自己的构建方法实现中设置默认值,并委托给 super 来进行实际构建。请务必妥善记录您的代码,以便未来的维护者知道在对象和构建方法中设置默认值,这样无论对象如何初始化,它们都会被设置。
JPA PrePersist 注释: 如果您对默认值的要求是针对数据库约束并且您使用的是 JPA 或 Hibernate,则可以使用此选项。此注释将 运行 在实体的每次插入和更新之前附加到的方法,无论它是如何初始化的。这很好,因此您不必像使用构建覆盖策略那样在两个地方设置默认值。我选择了这个选项,因为我的业务需求对该字段有很强的默认要求。我将下面的代码添加到我的实体 class 中以解决问题。我还删除了 isActive 字段声明的“= Boolean.TRUE”部分,因为它不再是必需的。
@PrePersist
public void defaultIsActive() {
if(isActive == null) {
isActive = Boolean.TRUE;
}
}
在具有默认值的字段上使用@Builder.Deafult 注释。
我有一个实体,它有一个布尔值,作为 '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;
}
然而,当我尝试 运行 测试将其保存到数据库时,我会收到 activeIndicator 的 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
}
原来问题不在Hibernate,而在Lombok。如果您使用的注释例如 @Data、@EqualsAndHashCodeOf、@NoArgsConstructor、@AllArgsConstructor,或@Builder,你的领域对象可能正在使用Lombok。 (Groovy 有一些类似的注释,只需检查您的导入是否使用 groovy.transform 或 lombok 作为包前缀)
导致我出现问题的代码位在我的测试中 class:
MyObject.builder().name(NAME).username(USERNAME).build();
经过反复试验,很明显,虽然我的 JPA/Hibernate 设置是正确的,但初始化默认值:
private Boolean activeIndicator = Boolean.TRUE;
没有被执行。
它进行了一些挖掘,但显然 Lombok 的生成器不使用 Java 的初始化,而是使用它自己的方法来创建对象。这意味着当您调用 build() 时 初始化默认值被忽略,因此如果您没有在构建器链中明确设置它,该字段将保持为 NULL。 请注意,正常的 Java 初始化和构造函数不受此影响;只有在使用 Lombok 的构建器创建对象时才会出现此问题。
详细说明此问题的问题以及 Lombok 为何不实施修复的解释详见此处:https://github.com/rzwitserloot/lombok/issues/663#issuecomment-121397262
对此有很多解决方法,您可能想要使用哪种方法取决于您的需要。
- Custom Constructor(s):可以创建自定义构造函数来代替构建器,或与构建器一起创建。当您需要设置默认值时,请使用您的构造函数而不是构建器。 - 这个解决方案让你看起来甚至不应该为建造者而烦恼。您可能希望保留构建器的唯一原因是构建器是否能够将创建委托给您的构造函数,从而在构建时获得初始化的默认值。我不知道这是否可行,因为我自己还没有测试过,所以我决定走另一条路。如果您进一步研究了此选项,请务必回复。
- 重写构建方法: 如果您要广泛使用构建器并且不想编写一堆构造函数,那么此解决方案适合您。您可以在自己的构建方法实现中设置默认值,并委托给 super 来进行实际构建。请务必妥善记录您的代码,以便未来的维护者知道在对象和构建方法中设置默认值,这样无论对象如何初始化,它们都会被设置。
JPA PrePersist 注释: 如果您对默认值的要求是针对数据库约束并且您使用的是 JPA 或 Hibernate,则可以使用此选项。此注释将 运行 在实体的每次插入和更新之前附加到的方法,无论它是如何初始化的。这很好,因此您不必像使用构建覆盖策略那样在两个地方设置默认值。我选择了这个选项,因为我的业务需求对该字段有很强的默认要求。我将下面的代码添加到我的实体 class 中以解决问题。我还删除了 isActive 字段声明的“= Boolean.TRUE”部分,因为它不再是必需的。
@PrePersist public void defaultIsActive() { if(isActive == null) { isActive = Boolean.TRUE; } }
在具有默认值的字段上使用@Builder.Deafult 注释。