如何阻止JPA仅将其可嵌入对象的所有字段都设置为null?

5 hibernate jpa eclipselink

尽管JPA标准违反直觉并且显然不是必需的,但是Eclipselink和Hibernate都竭尽全力创建以下NullPointerExceptions源:当实体中嵌入的对象的所有字段均为null时,它们将字段本身替换为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;
    }
}
Run Code Online (Sandbox Code Playgroud)

这个想法是我们可以写类似的东西po1.getPeriod().equals(po2.getPeriod())。当Date直接访问字段时,我们通过间接的方式支付此费用:po1.getPeriod().getStart()。这通常是一个很好的权衡,但是当我们不得不编写时却po1.getPeriod() == null ? null : po1.getPeriod().getStart()不是,因为我们的JPA提供程序喜欢null。

我们如何确保getPeriod()永不返回null?不幸的是,在标准的JPA中,既没有Java注释也没有XML设置来全局或特定嵌入或可嵌入对象解决此问题。

小智 6

Eclipselink 官方解决方案

对于每个受影响的实体,DescriptorCustomizer按照此处所述编写单独的实现,并使用此处所述的@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);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

为了解决我们具体例子中的空问题,我们必须修改PeriodOwner如下:

@Entity
@Customizer(MyUniversalCustomizer.class)
public class PeriodOwner {
    @Embedded @AttributeOverrides({...})
    private Period period = new Period();
    public Period getPeriod() {
        return period;
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,除了@Customizer注释之外,我们还初始化字段periodnew 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,这synchronized是线程安全所必需的。

Hibernate 的简单破解

代替上述对Periodand 的更改,向 中PeriodOwner添加一个未使用的私有非空字段Period。例如:

@Formula("1")
private int workaroundForBraindeadJpaImplementation;
Run Code Online (Sandbox Code Playgroud)

通过使用@Formula(一个 Hibernate 扩展),我们避免在数据库中为此字段添加额外的列。Tomáš Záluský在这里描述了这个解决方案。对于那些只想在某些情况下改变行为的人来说,它可能很有用。