虽然违反直觉并且显然不是JPA标准所要求的,但Eclipselink和Hibernate都竭尽全力创建以下NullPointerExceptions源:当嵌入实体的对象的所有字段都为null时,它们将替换字段本身空值。这是一个简化的例子:
@Embeddable
public class Period {
private Date start;
public Date getStart() {
return start;
}
private Date end;
public Date getEnd() {
return end;
}
public boolean equals(Period other) { // TODO: implement hashCode()!
return Objects.equals(start, other.start) && Objects.equals(end, other.end);
}
}
@Entity
public class PeriodOwner {
@Embedded @AttributeOverrides({...})
private Period period;
public Period getPeriod() {
return period;
}
}
我们的想法是我们可以写po1.getPeriod().equals(po2.getPeriod())
之类的东西。在直接访问Date
字段时,我们会通过额外的间接费用付费:po1.getPeriod().getStart()
。这通常是一个很好的权衡,但是当我们不得不写po1.getPeriod() == null ? null : po1.getPeriod().getStart()
而不是因为我们的JPA提供者喜欢null时。
我们如何确保getPeriod()
永远不会返回null?遗憾的是,在标准JPA中,既没有Java注释也没有XML设置来解决全局或特定嵌入或嵌入式问题。
答案 0 :(得分:2)
Eclipselink官方解决方案
对于每个受影响的实体,按照here所述编写单独的DescriptorCustomizer
实施,并使用here所述的@Customizer
注释使其生效。如果没有技巧,我们就不能对每个受影响的类使用相同的自定义程序,因为自定义程序需要知道要调用setIsNullAllowed(false)
的可嵌入字段的名称。
这是一个通用DescriptorCustomizer
实现,可用于使每个可嵌入的类都固定在不可为空的类的固定列表中:
public class MyUniversalDescriptorCustomizer implements DescriptorCustomizer {
private static final ImmutableSet<Class> NON_NULLABLE_EMBEDDABLES = ImmutableSet.of(Period.class);
@Override
public void customize(ClassDescriptor cd) throws Exception {
Class entityClass = cd.getJavaClass();
for (Field field : entityClass.getDeclaredFields()) {
if (NON_NULLABLE_EMBEDDABLES.contains(field.getType())) {
System.out.println(field.getName());
AggregateObjectMapping aom = (AggregateObjectMapping) cd.getMappingForAttributeName(field.getName());
aom.setIsNullAllowed(false);
}
}
}
}
要修复我们的具体示例中的null问题,我们必须修改PeriodOwner
,如下所示:
@Entity
@Customizer(MyUniversalCustomizer.class)
public class PeriodOwner {
@Embedded @AttributeOverrides({...})
private Period period = new Period();
public Period getPeriod() {
return period;
}
}
请注意,除@Customizer
注释外,我们还会使用period
初始化字段new Period()
,因为否则新实体仍会包含空period
个字段。
Hibernate的官方解决方案
显然,自从Hibernate 5.1以来,有一个设置hibernate.create_empty_composites.enabled
。 (因为我没有使用Hibernate,所以我没有尝试找出这个设置的位置。)
行人解决方法
以下解决了这个问题而不会过多地污染代码,但它仍然非常混乱。
@Embeddable
public class Period {
private Date start;
public Date getStart() {
return start;
}
private Date end;
public Date getEnd() {
return end;
}
public boolean equals(Period other) { // TODO: implement hashCode()!
return Objects.equals(start, other.start) && Objects.equals(end, other.end);
}
public static Period orNew(Period period) {
return period != null ? period : new Period();
}
}
@Entity
public class PeriodOwner {
@Embedded @AttributeOverrides({...})
private Period period;
public synchronized Period getPeriod() {
return period = Period.orNew(period);
}
}
请注意,线程安全需要synchronized
。
Hibernate的简单攻击
不是对Period
和PeriodOwner
进行上述更改,而是将一个未使用的私有非空字段添加到Period
。例如:
@Formula("1")
private int workaroundForBraindeadJpaImplementation;
通过使用@Formula
(Hibernate扩展),我们避免在数据库中为此字段添加额外的列。 TomášZáluskýhere描述了这个解决方案。对于那些只想在某些情况下改变行为的人来说,这可能很有用。
答案 1 :(得分:1)
JPA规范完全忽略了对空嵌入对象的处理,并将其留给实现来做他们想做的事情(很好,是吗?)。 It has been requested for JPA 2.2+但谁知道甲骨文是否愿意提供这一点。
DataNucleus JPA为嵌入字段/属性提供了2个扩展属性
@Extension(key="null-indicator-column", value="MY_COL")
@Extension(key="null-indicator-value", value="SomeValue")
所以当嵌入对象为null时,此列设置为此值,同样在读取对象时,它可以检测到空嵌入对象并将其正确地返回给用户。