2016-12-05 2 views
2

Obwohl Eclipselink und Hibernate nicht intuitiv und offensichtlich nicht vom JPA-Standard gefordert werden, gehen sie sehr weit, um die folgende Quelle von NullPointerExceptions zu erstellen: Wenn alle Felder eines in eine Entität eingebetteten Objekts null sind, ersetzen sie das Feld selbst durch null . Hier ist ein vereinfachtes Beispiel:Wie kann ich verhindern, dass JPA ein einbettbares Objekt auf null setzt, nur weil alle Felder null sind?

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

Die Idee ist, dass wir die Dinge wie po1.getPeriod().equals(po2.getPeriod()) schreiben kann. Dafür zahlen wir beim indirekten Zugriff auf die Felder Date direkt: po1.getPeriod().getStart(). Dies ist oft ein guter Kompromiss, aber nicht, wenn wir stattdessen po1.getPeriod() == null ? null : po1.getPeriod().getStart() schreiben müssen, weil unser JPA-Provider Null liebt.

Wie können wir sicherstellen, getPeriod() gibt nie Null zurück? Leider gibt es in der Standard-JPA weder Java-Anmerkungen noch XML-Einstellungen, um dieses Problem entweder global oder für bestimmte Einbettungen oder eingebettete Dateien zu lösen.

Antwort

1

Offizielle Lösung für Eclipse

Für jede betroffene Person eine separate DescriptorCustomizer Implementierung wie here schreiben und es zu aktivieren mit der @Customizer Annotation als here beschrieben. Ohne Tricks können wir nicht den gleichen Customizer für jede betroffene Klasse verwenden, da der Customizer die Namen der einzubettenden Felder kennen muss, für die er aufgerufen werden soll .

Hier ist eine universelle DescriptorCustomizer Implementierung, die jede integrierbare aus einer festen Liste von Klassen nicht-nullable machte verwendet werden kann:

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

das Null-Problem in unserem speziellen Beispiel zu beheben, müssen wir ändern PeriodOwner wie folgt:

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

Beachten Sie, dass zusätzlich zu der @Customizer Anmerkung, wir auch das Feld initialisieren period mit new Period() da sonst neue Entitäten hätten immer noch null period Felder.

Offizielle Lösung für Hibernate

Offensichtlich, da Hibernate 5.1 eine Einstellung hibernate.create_empty_composites.enabled ist. (Da ich nicht Hibernate, ich habe nicht versucht, um herauszufinden, wo diese Einstellung geht.)

Fußgänger Abhilfe

Die folgende kümmert sich um das Problem, ohne den Code umweltfreundlich zu viel, aber es ist immer noch ziemlich chaotisch.

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

anzumerken, dass für synchronized Thread-Sicherheit erforderlich ist.

Einfacher Hack für Hibernate

Anstelle der oben genannten Änderungen Period und PeriodOwner, fügen Sie ein nicht verwendetes privates Nicht-Null-Feld Period. Zum Beispiel:

@Formula("0") 
private int workaroundForBraindeadJpaImplementation; 

Durch die Verwendung von @Formula (eine Hibernate-Erweiterung) wir eine zusätzliche Spalte für dieses Feld in der Datenbank zu vermeiden hinzufügen. Diese Lösung wurde von Tomáš Záluský here beschrieben. Es kann für diejenigen nützlich sein, die das Verhalten nur in einigen Fällen ändern möchten.

+0

AFAIK, wenn Sie synchronisieren rund um den Zugriff auf eine Variable benötigen Sie nicht volatile. https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html – narduk

+0

Danke. Ich habe "flüchtig" entfernt, da es unnötig erscheint. –

1

Die JPA-Spezifikation ignoriert vollständig die Verarbeitung von null eingebetteten Objekten und überlässt es Implementierungen, das zu tun, was sie wollen (nett, ja?). It has been requested for JPA 2.2+ Aber wer weiß, ob Oracle sich jemals darum kümmern wird.

Datanucleus JPA bietet 2 Erweiterungseigenschaften für ein eingebettetes Feld/Eigenschaft

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

und so, wenn das eingebettete Objekt null ist, dann wird diese Spalte auf diesen Wert gesetzt, und ebenfalls, wenn sie in Objekte lesen kann eine Erkennung NULL eingebettetes Objekt und gibt es korrekt an den Benutzer zurück.

Verwandte Themen