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

2023-12-22

虽然违反直觉并且显然不是 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;
    }
}

我们的想法是我们可以写这样的东西po1.getPeriod().equals(po2.getPeriod())。我们在访问时通过额外的间接程度来为此付出代价Date直接字段:po1.getPeriod().getStart()。这通常是一个很好的权衡,但当我们必须编写时就不是这样了po1.getPeriod() == null ? null : po1.getPeriod().getStart()相反,因为我们的 JPA 提供者喜欢 null。

我们怎样才能确定getPeriod()永远不会返回 null?不幸的是,在标准 JPA 中,既不存在 Java 注释,也不存在 XML 设置来全局或针对特定嵌入或可嵌入解决此问题。


Eclipselink官方解决方案

为每个受影响的实体编写单独的DescriptorCustomizer实施如所描述的here http://wiki.eclipse.org/EclipseLink/UserGuide/JPA/Basic_JPA_Development/Entities/Embeddable#Nullable_embedded_values并使用@Customizer注释如所描述here http://www.eclipse.org/eclipselink/documentation/2.5/jpa/extensions/a_customizer.htm。如果没有技巧,我们就无法对每个受影响的类使用相同的定制器,因为定制器需要知道它要调用的可嵌入字段的名称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 with new Period()因为否则新实体仍然为空period fields.

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 and PeriodOwner,添加一个未使用的私有非空字段到Period。例如:

@Formula("1")
private int workaroundForBraindeadJpaImplementation;

通过使用@Formula(Hibernate 扩展)我们避免在数据库中为此字段添加额外的列。 Tomáš Záluský 描述了该解决方案here https://stackoverflow.com/questions/1324266/can-i-make-an-embedded-hibernate-entity-non-nullable。对于那些只想在某些情况下改变行为的人来说可能很有用。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何防止 JPA 将可嵌入对象设置为 null 仅仅因为它的所有字段都为 null? 的相关文章

随机推荐