Umsetzung des Datenzugriffs
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.
Die Nutzung ist unter den Lizenzbedingungen der Creative Commons Namensnennung 4.0 International gestattet.
Die Umsetzung des Datenzugriffs 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.
Klassen | Package |
---|---|
DAO |
|
Entity |
|
Um den Anteil an Boilerplate Code bei der Umsetzung des Datenzugriffs 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
.
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 IT-System). Falls die Datenhoheit keiner einzelnen Komponente zugewiesen werden kann, erhält die Komponente Basisdaten die Datenhoheit (siehe auch Detailkonzept Komponente Anwendungskern). 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 im Datenzugriff 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.
@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.
@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.
@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.
@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.
@Entity
public class MyEntity { }
Primärschlüssel werden wie folgt annotiert.
@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.
@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.
@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.
@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
.
@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.
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.
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.
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.
@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.
@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.