How can I prevent JPA from setting an embeddable o

2020-07-09 02:00发布

问题:

Although counterintuitive and apparently not required by the JPA standard, both Eclipselink and Hibernate go to great lengths to create the following source of NullPointerExceptions: When all fields of an object embedded in an entity are null, then they replace the field itself by null. Here is a simplified example:

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

The idea is that we can write things like po1.getPeriod().equals(po2.getPeriod()). We pay for this by an additional degree of indirection when accessing the Date fields directly: po1.getPeriod().getStart(). This is often a good trade-off, but not when we have to write po1.getPeriod() == null ? null : po1.getPeriod().getStart() instead because our JPA provider loves null.

How can we make sure getPeriod() never returns null? Unfortunately, in standard JPA there exist neither Java annotations nor XML settings to solve this problem either globally or for specific embeddings or embeddables.

回答1:

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.



回答2:

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.