How can I prevent JPA from setting an embeddable object to null just because all its fields are null?

社会主义新天地 提交于 2019-12-04 08:33:15

Official solution for Eclipselink

For each affected entity write a separate DescriptorCustomizer implementation as described here and make it active using the @Customizer annotation as described here. Without tricks we cannot use the same customizer for every class affected, as the customizer needs to know the names of the embeddable fields for which it is to invoke setIsNullAllowed(false).

Here is a universal DescriptorCustomizer implementation that can be used to make every embeddable from among a fixed list of classes non-nullable:

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);
            }
        }
    }
}

To fix the null problem in our specific example, we have to modify PeriodOwner as follows:

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

Note that in addition to the @Customizer annotation, we also initialize the field period with new Period() because otherwise new Entities would still have null period fields.

Official solution for Hibernate

Apparently, since Hibernate 5.1 there is a setting hibernate.create_empty_composites.enabled. (Since I am not using Hibernate, I didn't try to find out where this setting goes.)

Pedestrian workaround

The following takes care of the problem without polluting the code too much, but it's still quite messy.

@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);
    }
}

Note that synchronized is required for thread safety.

Simple hack for Hibernate

Instead of the above changes to Period and PeriodOwner, add one unused private non-null field to Period. For example:

@Formula("1")
private int workaroundForBraindeadJpaImplementation;

By using @Formula (a Hibernate extension) we avoid adding an extra column for this field in the database. This solution was described by Tomáš Záluský here. It may be useful for those who want to change the behaviour only in some cases.

Neil Stockton

The JPA spec totally ignores handling of null embedded objects and leaves it up to implementations to do what they feel like (nice, yes?). It has been requested for JPA 2.2+ but who knows if Oracle will ever bother to provide that.

DataNucleus JPA provides 2 extension properties for an embedded field/property

@Extension(key="null-indicator-column", value="MY_COL")
@Extension(key="null-indicator-value", value="SomeValue")

and so when the embedded object is null then this column is set to this value, and likewise when reading in objects it can detect a null embedded object and return it correctly to the user.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!