JPA - Generische Superklasse


#1

Ausgehend vom Artikel “Temporal Patterns” habe ich eine generische Klasse TemporalRecord<T> entworfen, die einen Wert mit Beobachtungs- und Ereigniszeitpunkt versieht.
Diese Klasse möchte ich gerne als Embeddable weiterverwenden. Probleme habe ich nun mit dem Mapping des eingebetteten Wertes, weil ich nicht weiß, wie ich das Mapping für Enums und Strings definieren soll.
Wie kann ich für den eingebetteten Wert das Mapping deklarieren?

Die Klasse sieht so aus:

import javax.persistence.*;
import java.io.Serializable;
import java.time.LocalDateTime;

import static com.google.common.base.Preconditions.checkNotNull;

@MappedSuperclass
@Access(AccessType.FIELD)
public class TemporalRecord<T> implements Serializable {
    private static final long serialVersionUID = 8327763668012454656L;
    @Embedded // funktioniert für Basistypen und Embeddables problemlos, für Strings mit Einschränkungen (Länge immer 255), für Enums gar nicht
    private T value;
    @Column(nullable = false)
    private LocalDateTime recordDate;
    @Column(nullable = false)
    private LocalDateTime actualDate;

    protected TemporalRecord() {
    }

    public TemporalRecord(@Nonnull T value) {
        this(value, LocalDateTime.now());
    }

    public TemporalRecord(@Nonnull T value, @Nonnull LocalDateTime actualDate) {
        this(value, actualDate, LocalDateTime.now());
    }

    public TemporalRecord(@Nonnull T value, @Nonnull LocalDateTime actualDate, @Nonnull LocalDateTime recordDate) {
        this.value = checkNotNull(value);
        this.actualDate = checkNotNull(actualDate);
        this.recordDate = checkNotNull(recordDate);
    }

    public T getValue() {
        return value;
    }

    public LocalDateTime getRecordDate() {
        return recordDate;
    }

    public LocalDateTime getActualDate() {
        return actualDate;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof TemporalRecord)) return false;

        TemporalRecord that = (TemporalRecord) o;

        return actualDate.equals(that.actualDate) && recordDate.equals(that.recordDate) && value.equals(that.value);
    }

    @Override
    public int hashCode() {
        int result = value.hashCode();
        result = 31 * result + recordDate.hashCode();
        result = 31 * result + actualDate.hashCode();
        return result;
    }
}```

So benutze ich die Klasse dann in etwa:
```import javax.persistence.Embeddable;

@Embeddable
public class TestEmbeddableRecord extends TemporalRecord<String> {
    private static final long serialVersionUID = 6045969975091246866L;
}```
```import javax.persistence.*;

@Entity
public class TestEntity {
    @Id
    private Long id;
    @Embedded
    private TestEmbeddableRecord record;
}```

In diesem Fall generiert hibernate folgendes:
[sql]create table TestEntity (id bigint not null, actualDate datetime not null, recordDate datetime not null, hash integer not null, value varchar(255), primary key (id)) ENGINE=InnoDB[/sql]

Bei einem Integer sieht es so aus (alles richtig):
[sql]create table TestEntity (id bigint not null, actualDate datetime not null, recordDate datetime not null, value integer not null, primary key (id)) ENGINE=InnoDB[/sql]

Für ein Enum sieht das dann so aus (value fehlt):
[sql]create table TestEntity (id bigint not null, actualDate datetime not null, recordDate datetime not null, primary key (id)) ENGINE=InnoDB[/sql]

Ich sehe gerade, dass bei Strings auch noch eine `hash`-Spalte generiert wird, die zuviel ist.

*** Edit ***

Das müsste so etwas wie `@AttributeOverride` nur für Vererbung sein, dabei aber alle JPA-Annotationen zulassen.

*** Edit ***

Funktionierende (aber unschöne) Lösung: das Feld `value` in `TemporalRecord` weglassen, die Klasse abstrakt machen, einen abstrakten, protected setter deklarieren und `getValue` ebenfalls abstrakt machen.
Gibt es eine schönere Lösung?

#2

Wenn die genannte unschöne Lösung funktioniert, sollte(?) auch die Implementierung oben funktionieren, wenn du die ganzen spaltenbezogenen Annotationen an die Getter und nicht an die Instanzvariablen hängst. Wenn das geht, hätte das den Vorteil, dass du getValue nur zu überschreiben bräuchtest, wenn das Standardverhalten nicht ausreicht (also bei Strings, Enums…). Eventuell bräuchtest du dann für numerische Werte überhaupt keine “Zwischenklasse” wie TestEmbeddableRecord, sonder könntest direkt TemporalRecord verwenden. Aber das ist alles nur Spekulation, keine Ahnung, wie sich Hibernate da verhält.


#3

Der Gedanke für dieses Feld Propertyaccess zu verwenden bringt mich zumindest einen Schritt näher an eine schöne Lösung. Zumindest reicht es, wenn ich den Getter überschreibe und das Mapping dort definiere.
Da sich das Mapping (zumindest mit Annotationen) aber nicht verändern lässt, kann ich in der Superklasse das Feld nur als transient markieren und laufe somit Gefahr, dass ich mal vergesse den Getter zu überschreiben und passend zu annotieren.


#4

Funktioniert das für Enums überhaupt so allgemein? IMHO muss man Enums mit der @Enumerated Annotation versehen (falls man nicht XML verwendet)um dann auszuwählen wie das Enum persistiert werden soll (Ordinalwert, Stringwert).


#5

Ohne @Enumerated persistiert zumindest hibernate den Ordinalwert. Ob das Weglassen allerdings JPA-konform ist, weiß ich nicht.

*** Edit ***

Im JSR 338 (JPA 2.1) Kapitel 2.1 “Mapping Defaults for Non-Relationship Fields or Properties” steht, dass auch bei enums das @Basic-Mapping verwendet wird, falls keine Annotation angegeben wurde.
In Kapitel 11.1.18 steht dann das entscheidende:

Die Annotation darf also gem. JPA 2.1 weggelassen werden, auch eine @Basic-Annotation darf zeitgleich vorhanden sein (steht etwas weiter oben im selben Kapitel).