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

前端 未结 2 1757
再見小時候
再見小時候 2021-02-14 08: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 NullPointerExcept

2条回答
  •  情书的邮戳
    2021-02-14 08:20

    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 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.

提交回复
热议问题