Umsetzung der Persistenzschicht

IFS-Logo Diese Seite ist ein Teil der IsyFact-Standards. Alle Inhalte der Seite, insbesondere Texte und Grafiken, sind urheberrechtlich geschützt. Alle urheberrechtlichen Nutzungs- und Verwertungsrechte liegen beim Bundesverwaltungsamt.

Creative Commons Namensnennung Die Nutzung ist unter den Lizenzbedingungen der Creative Commons Namensnennung 4.0 International gestattet.

Die Umsetzung der Persistenzschicht mit JPA und Hibernate beachtet die Vorgaben und Konventionen zu diesem Thema. Das Produkt Spring Data JPA vereinfacht die Umsetzung weiter.

1. Umsetzung der Data Access Objects

Für die Ablage der Data Access Objects (DAOs) und Entitäten gibt es eine Konvention.

Tabelle 1. Vorgaben zur Paketstruktur für Klassen der Persistenzschicht
Klassen Package

DAO

<organisation>.<domäne>.<system>.persistence.<komponente>.dao

Entity

<organisation>.<domäne>.<system>.persistence.<komponente>.entity

Um den Anteil an Boilerplate Code bei der Umsetzung der Persistenzschicht deutlich zu reduzieren, werden (Spring Data) Repositories eingesetzt. Die häufig verwendeten CRUD-Methoden (Create, Read, Update, Delete) werden vom Interface CrudRepository (siehe Methoden von CrudRepository) zur direkten Verwendung angeboten. Zur Implementierung werden zwei Typ-Parameter benötigt: der Typ der Entität T und der Typ des Primärschlüssels ID.

Listing 1. Methoden von CrudRepository
public interface CrudRepository<T,ID> {
    long        count();
    void        delete(T entity);
    void        deleteAll();
    void        deleteAll(Iterable<? extends T> entities);
    void        deleteById(ID id);
    boolean     existsById(ID id);
    Iterable<T> findAll();
    Iterable<T> findAllById(Iterable<ID> ids);
    Optional<T> findById(ID id);
    T           save(T entity);
    Iterable<T> saveAll(Iterable<T> entities);
}

Der Zugriff auf die Datenbank aus dem Anwendungskern heraus erfolgt immer über diese Repositories. Die Repositories werden als Spring-Beans in den Anwendungskern injiziert. Für jede Entität wird ein entsprechendes Repository als Interface angelegt.

Die Repositories werden im Package der Fachkomponente abgelegt, welche die Datenhoheit über die Tabelle(n) des Repositories besitzt (zum Thema Datenhoheit siehe Referenzarchitektur Backend). Die Repositories werden nur von Klassen der Fachkomponente mit Datenhoheit aufgerufen.

Gemäß der Referenzarchitektur dürfen Entitäten nur von der Fachkomponente im Anwendungskern verändert werden, welche zur Fachkomponente in der Persistenzschicht gehört. Der Anwendungskern darf für andere Fachkomponenten nur Geschäftsobjekte an seiner Fachkomponentenschnittstelle anbieten.

Für ein konkretes DAO ist ein eigenes Interface von der Basisschnittstelle CrudRepository abzuleiten. Die Benennung erfolgt gemäß der Namenskonvention. In der Dao-Klasse können weitere DAO-Operationen definiert werden, zum Beispiel zur Durchführung von Queries. Ein Beispiel hierfür ist in Beispiel für ein eigenes Data Access Object zu sehen.

Weiterhin ist das eigene Interface mit der Annotation @Repository zu versehen, damit alle vom Entity Manager erzeugten Exceptions in die besser auszuwertenden Exceptions von Spring Data umgewandelt werden.

Listing 2. Beispiel für ein eigenes Data Access Object
@Repository
public interface PersonRepository extends Repository<Person, Long> { }

Damit die DAOs von Spring automatisch als Beans erzeugt werden, muss eine Konfigurationsklasse der Anwendung mit der Annotation @EnableJpaRepositories annotiert werden.

Listing 3. Automatische Erstellung von DAO-Beans durch Spring
@Configuration
@EnableJpaRepositories("<organisation>.<domäne>.<system>.persistence")
class PersistenceConfiguration { }

1.1. Definition von Query Methoden

Der von Spring Data erzeugte Proxy für das Repository-Interface kann die Queries auf zwei Arten ableiten.

1.1.1. Ableitung des Queries über den Namen der Methode

Bei dieser Ableitung wird das Präfix des Methodennamens abgeschnitten und der Rest geparst. Nach dem ersten By beginnen die eigentlichen Abfragekriterien. In den Abfragekriterien werden Bedingungen auf Feldern der Entität definiert und diese können mit And und Or verknüpft werden.

Listing 4. Beispiele für die Ableitung des Queries aus dem Methodennamen
@Repository
public interface PersonRepository extends Repository<Person, Long> {

    List<Person> findByEmailAdresseAndNachname(EmailAdresse emailAdresse, String nachname);

    // Verwendung von DISTINCT
    List<Person> findDistinctPeopleByNachnameOrVorname(String nachname, String vorname);
    List<Person> findPeopleDistinctByNachnameOrVorname(String nachname, String vorname);

    // Ignorieren der Groß-/Kleinschreibung für ein bestimmtes Feld
    List<Person> findByNachnameIgnoreCase(String nachname);
    // Ignorieren der Groß-/Kleinschreibung für alle betroffenen Felder
    List<Person> findByNachnameAndVornameAllIgnoreCase(String nachname, String vorname);

    // Statisches Sortieren mit ORDER BY
    List<Person> findByNachnameOrderByVornameAsc(String nachname);
    List<Person> findByNachnameOrderByVornameDesc(String nachname);

}
Eine Übersicht zur Ableitung von Queries aus Methodennamen befindet sich in der Referenzdokumentation zu Spring Data JPA.

1.1.2. Ableitung über eine manuell definierte Query.

Die Query wird über die @Query-Annotation in JPQL direkt an die Methode des DAO geschrieben.

Listing 5. Beispiele für die Ableitung des Queries aus dem Methodennamen.
@Repository
public interface PersonRepository extends Repository<Person, Long> {

    @Query("select p from Person p where p.emailAdresse = ?1")
    Person findByEmailAdresse(String emailAdresse);

}

Bevorzugt wird die Ableitung der Queries über den Methodennamen. Kann die Query nicht über den Methodennamen ausgedrückt werden, werden manuell definierte Queries verwendet.

2. Definition des O/R-Mappings

Entitäten werden über Annotationen gekennzeichnet.

Listing 6. Definition einer Entität
@Entity
public class MyEntity { }

Primärschlüssel werden wie folgt annotiert.

Listing 7. Konfiguration der ID und Sequenz
@Entity
public class MyEntity {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO, generator="my_seq")
    @SequenceGenerator(name="my_seq",sequenceName="MY_SEQ", allocationSize=50)
    private int id;

}

Es muss unbedingt darauf geachtet werden, das Inkrement (INCREMENT BY) der zur ID-Generierung genutzt Datenbanksequenz auf denselben Wert einzustellen, der auch beim SequenceGenerator im Parameter allocationSize angegeben ist.

2.1. Assoziationen

1-zu-n-Assoziationen werden entweder über eine unsortierte Menge oder eine von der Datenbank vorsortierten Liste definiert.

Listing 8. Definition von 1-zu-n-Assoziationen
@Entity
public class MyEntity {

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "unsorted_id")
    private Set<UnsortedEntity> unsortedEntities = new HashSet<>();

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "sorted_id")
    @OrderBy("field ASC")
    private List<SortedEntity> sortedEntities = new ArrayList<>();

}

2.1.1. Lazy Loading

Standardmäßig soll für alle Assoziationen Lazy Loading verwendet werden. Um Lazy Loading auch für 1-zu-1-Assoziationen einzuschalten, wird das fetch-Attribut der Annotation @OneToOne auf FetchType.LAZY gesetzt. Damit das Lazy Loading über Proxies funktioniert, muss die Assoziation nicht optional sein, d.h. das Feld darf nicht null sein.

Listing 9. Lazy Loading bei 1-zu-1-Assoziationen
@Entity
public class MyEntity {

    @OneToOne(optional = false, fetch = FetchType.LAZY)
    private OtherEntity otherEntity;

}

Für n-zu-1-Assoziationen wird genauso verfahren und das fetch-Attribut auf FetchType.LAZY gesetzt.

Listing 10. Lazy Loading bei n-zu-1-Assoziationen
@Entity
public class MyEntity {

    @ManyToOne(fetch = FetchType.LAZY)
    private OtherEntity otherEntity;

}

Anders als bei 1-zu-1-Assoziationen ist hier erlaubt, Eager Loading zu verwenden, wenn dieses Verhalten Sinn ergibt und keine negativen Auswirkungen zu erwarten sind. Typische negative Auswirkungen sind N+1-Queries (die umgekehrte Assoziation von OtherEntity zu MyEntity benutzt Eager Loading) oder das Auslesen zu vieler Daten (OtherEntity besitzt viele Assoziationen mit Eager Loading).

2.2. Datum & Zeit

Für Datumsangaben werden die Datums- und Zeitklassen aus der Java 8 Date Time API verwendet.

Der folgende, hervorgehobene Absatz wird nur noch aus historischen Gründen erwähnt und ist obsolet.

Für alte Anwendungen, die nicht die Java 8 Date Time API, sondern noch java.util.Date verwenden, gelten die folgenden Vorgaben.

Die Definition von Datumsangaben erfolgt in einem Attribut vom Typ TemporalType.TIMESTAMP oder TemporalType.DATE.

Listing 11. Definition von Datumsangaben
@Entity
public class MyEntity {

    @Temporal(TemporalType.TIMESTAMP)
    private Date genauesDatum;

    @Temporal(TemporalType.DATE)
    private Date ungenauesDatum;

}

Vergleiche von Datumsangaben sind nur mittels compareTo und immer auf dem Attribut mit der höheren Genauigkeit durchzuführen.

Listing 12. Vergleiche von Datumsangaben
public class MyUseCase {

    public void calculateDates(MyEntity entity) {
        entity.getGenauesDatum().compareTo(entity.getUngenauesDatum()); // OK

        entity.getUngenauesDatum().compareTo(entity.getGenauesDatum()); // NICHT OK
        entity.getGenauesDatum().equals(entity.getUngenauesDatum());    // NICHT OK
        entity.getUngenauesDatum().equals(entity.getGenauesDatum());    // NICHT OK
    }
}

Für Berechnungen auf Datumsangaben ist der Typ java.util.Calendar zu verwenden.

2.3. Enum-Variablen

Enums werden über zwei spezielle Hibernate User-Types definiert. Diese ermöglichen es, Enum-Werte auf fest definierte Zeichenketten abzubilden.

Die Klasse EnumUserType erlaubt es, in einem Enum per Annotation die gewünschte Datenbankdarstellung zu jeder Ausprägung anzugeben.

Listing 13. Definition eines Enums zur Verwendung mit EnumUserType
public enum Richtung {

    @PersistentValue("L")
    LINKS,
    @PersistentValue("R")
    RECHTS,
    @PersistentValue("G")
    GERADEAUS

}

Die Klasse EnumWithIdUserType erlaubt die Persistierung von Enums, die einen fachlichen Schlüssel besitzen.

Listing 14. Definition eines Enums zur Verwendung mit EnumWithIdUserType
public enum RichtungMitId {

    LINKS("L"),
    RECHTS("R"),
    GERADEAUS("G");

    private final String id;

    RichtungMitId(String id) {
        this.id = id;
    }

    @EnumId
    public String getId() {
        return id;
    }

}

Das folgende Beispiel zeigt die Verwendung dieser Enums in einer Entität.

Listing 15. Verwendung von Enums in Entitäten
@Entity
public class MyEntity {

  @Column(nullable = false, length = 1)
  @Type(type = "de.bund.bva.isyfact.persistence.usertype.EnumUserType",
    parameters = { @Parameter(name = "enumClass",value = "<package>.Richtung") })
  private Richtung richtung;

  @Column(nullable = false, length = 1)
  @Type(type = "de.bund.bva.isyfact.persistence.usertype.EnumWithIdUserType",
    parameters = { @Parameter(name = "enumClass",value = "<package>.RichtungMitId") })
  private RichtungMitId richtungMitId;

}

2.4. Initialisieren von String-Feldern

Für die Verarbeitung in Regelwerken ist es hilfreich, dass String-Felder initialisiert werden, da ansonsten in nahezu allen Regeln zwischen einer leeren Zeichenkette und null differenziert werden müsste. In Objekten, die in das Regelwerk eingegeben werden sollen, wird daher bei der Definition von String-Feldern initial ein Leer-String gesetzt.

@Entity
public class MyEntity {

   private String name = "";

}

2.5. Optimistisches Locking

Zur Umsetzung des optimistischen Lockings wird in Entitäten eine numerische Property mit der Annotation @Version gekennzeichnet.

Listing 16. Versionierung von Entitäten für optimistisches Locking
@Entity
public class MyEntity {

    private int version;

    @Version
    public int getVersion() {
        return version;
    }

    public void setVersion(int version) {
        this.version = version;
    }

}

Dieses Feld wird einzig von Hibernate verwaltet. Es ist weder zu lesen noch zu schreiben.