Konfiguration des O/R-Mappings

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.

In den folgenden Abschnitten werden konkrete Vorgaben gemacht, welche Konfigurationen für die Umsetzung der Persistenzschicht verwendet werden sollen.

1. Konfiguration von JPA über Spring Beans durchführen

Die für die Verwendung von JPA benötigten Beans werden von Spring Boot beim Start der Anwendung automatisch instanziiert.

Teile dieser automatischen Konfiguration können bei Bedarf überschrieben werden. Soll z. B. für die Entwicklung eine andere Datenbank verwendet werden, kann die automatisch konfigurierte DataSource-Bean durch eine andere überschrieben werden. Das Gleiche gilt für die Anbindung einer zweiten Datenbank, siehe dazu Nutzung und Anbindung einer zweiten Datenbank.

2. Konfiguration des EntityManagers

Der EntityManager wird von Spring Boot automatisch konfiguriert. Eine zusätzliche Konfiguration kann über application.properties erfolgen. Grundsätzlich können nach dem Schema spring.jpa.properties.<Schlüssel>=<Wert> beliebige native Properties für Hibernate gesetzt werden (Listing 1).

Listing 1. Konfiguration des EntityManagers in application.properties
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=false

spring.jpa.properties.hibernate.dialect=<Database Dialect>
spring.jpa.properties.hibernate.connection.isolation=4
spring.jpa.properties.hibernate.connection.useUnicode=true
spring.jpa.properties.hibernate.connection.characterEncoding=utf-8
spring.jpa.properties.hibernate.jdbc.batch_size=0
spring.jpa.properties.hibernate.jdbc.use_streams_for_binary=true
spring.jpa.properties.hibernate.format_sql=false
spring.jpa.properties.hibernate.default_schema=<Default Schema>
spring.jpa.properties.hibernate.ejb.metamodel.generation=enabled

# Folgender Parameter ist optional, da er dem Standard entspricht
spring.jpa.properties.hibernate.transaction.coordinator_class=jdbc

3. Konfiguration der Datasource

Als Datasource-Implementierung muss die Implementierung aus de.bund.bva.isyfact.persistence.datasource.IsyDataSource genutzt werden. Bei der Verwendung von isy-persistence wird automatisch eine Bean mit dem Namen appDataSource erzeugt. Diese prüft die Version des Datenbankschemas (siehe Abschnitt Prüfen der Schema-Version) und dient als Wrapper für die eigentliche Spring DataSource.

Die Autokonfiguration der IsyFact 3 Version von isy-persistence verwendet Spring Boot Mechanismen, um die DataSource zu konfigurieren. Unter dem Präfix isy.persistence.datasource können alle Properties konfiguriert werden, welche in Spring unter dem Präfix spring.datasource bekannt sind (vgl. Common Application Properties). Die Konfiguration bietet damit die Flexibilität, Datenbanken verschiedener Anbieter zu nutzen, sofern diese von Spring Boot unterstützt sind.

Die Nutzung der IsyPersistenceAutoConfiguration führt durch das Verpacken der DataSource in eine IsyDataSource mit der @Primary- und @DependsOnDatabaseInitialization-Annotation zusammen mit der SqlInitializationAutoConfiguration von Spring Boot (betrifft SqlDataSourceScriptDatabaseInitializer-Bean) zu einer zyklischen Abhängigkeit.

Dieses Problem kann durch Setzen der Property spring.sql.init.mode=never in der application.properties gelöst werden.

3.1. Standardmäßig Lazy Loading verwenden

Standardmäßig verwendet Hibernate für alle 1:n und n:m Assoziationen ein Lazy Loading über dynamische Proxies und für n:1 oder 1:1 Assoziationen wird Eager Loading eingesetzt. Standardmäßig soll für alle Assoziationen Lazy Loading verwendet werden, wobei Bytecode-Manipulationen für Lazy Loading nicht verwendet werden sollen.

Um Lazy Loading auch für 1:1 Assoziationen einzuschalten, wird das fetch-Attribut auf FetchType.LAZY gesetzt. Damit das Lazy Loading über Proxies funktioniert, muss die Assoziation nicht optional sein, d. h., dass Feld darf nicht null sein.

@OneToOne(optional = false, fetch = FetchType.LAZY)
private SomeEntity someEntity;

Ist ein 1:1 assoziiertes Feld optional und kann den Wert null annehmen, kann Lazy Loading nur über Bytecode-Manipulation realisiert werden. Für n:1 Assoziationen wird genauso verfahren und das fetch-Attribut auf FetchType.LAZY gesetzt. Es ist erlaubt und erwünscht, dieses Verhalten für Assoziationen zu überschreiben, bei denen Eager Loading Sinn ergibt. Hierfür ist das Attribut fetch der jeweiligen Mapping-Annotation wie folgt zu setzen:

@OneToMany(fetch = FetchType.EAGER)

3.2. Standardmäßig optimistisches Locking verwenden

Standardmäßig ist für Hibernate ein optimistisches Locking zu verwenden: Objekte werden bei dieser Locking-Strategie nicht per „select for update“ gesperrt. Stattdessen wird am Ende der Transaktion geprüft, ob lokal veränderte Objekte parallel in der Datenbank geändert wurden. Ist dies der Fall, wird eine Ausnahme geworfen.

Dieser Vorgehensweise liegt die Annahme zugrunde, dass konkurrierende schreibende Zugriffe in einer Geschäftsanwendung nicht oder höchstens in Ausnahmefällen vorkommen. Sollte dies nicht zutreffen, muss explizites Locking verwendet werden (vgl. Abschnitt Explizites Locking). In der Anwendung ist keine explizite Fehlerbehandlung (etwa durch das Mergen der Daten) zu implementieren. Die geworfene Ausnahme ist (gewrappt) an den Aufrufer weiterzugeben.

Um zu erkennen, ob sich das Objekt in der Datenbank verändert hat, empfiehlt Hibernate die Verwendung eines numerischen Versions-Felds in jeder Datenbank-Tabelle. Dazu wird in den Entitäten eine numerische Property mit der Annotation @Version gekennzeichnet.

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

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

3.3. Nutzung und Anbindung einer zweiten Datenbank

Einige Anwendungsfälle machen es notwendig, eine zweite Datenbank zu nutzen. Das ist beispielsweise notwendig, wenn Daten aus einem Altsystem über die Datenbank für andere Systeme bereitgestellt werden und diese Daten in eine IsyFact-Anwendung über einen Batch importiert werden sollen. Der Batch muss dann sowohl auf die Datenbank der IsyFact-Anwendung, als auch auf die Datenbank des Altsystems zugreifen.

Die Anbindung einer zweiten Datenbank erfolgt analog zur Anbindung der primären Datenbank über Spring und die Nutzung von JPA. Dabei erfolgt der Zugriff auf die zweite Datenbank getrennt über einen weiteren Entity Manager und eine weitere Data Source.

Wird eine zweite Datenbank verwendet, müssen die Beans für EntityManagerFactory und den TransactionManager auch für die primäre Datenbank manuell konfiguriert werden Listing 2. Als DataSource wird hier die von isy-persistence automatisch konfigurierte appDataSource verwendet.

Listing 2. Konfiguration der ersten DataSource
@Configuration
@EnableJpaRepositories(basePackages = "de.beispiel.zweidatasources.persistence", entityManagerFactoryRef = "entityManagerFactoryApp", transactionManagerRef = "transactionManagerApp")
public class PersistenceConfig {

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactoryApp(@Qualifier("appDataSource") DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setPackagesToScan("de.beispiel.zweidatasource.persistence");
        em.setDataSource(dataSource);
        em.setJpaDialect(new HibernateJpaDialect());

        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        vendorAdapter.setGenerateDdl(true);
        vendorAdapter.setDatabase(...); (1)
        vendorAdapter.setShowSql(false);
        em.setJpaVendorAdapter(vendorAdapter);

        return em;
    }

    @Bean
    public PlatformTransactionManager transactionManagerApp(@Qualifier("entityManagerFactory") EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory);
        return transactionManager;
    }
}
1 Korrekten Hersteller setzen.

Für die zweite Datenbankanbindung wird eine weitere Konfiguration angelegt Listing 3. Dafür können die von isy-persistence erweiterten DatabaseProperties verwendet werden, um durch das Verpacken der DataSource in eine IsyDataSource die Schema-validierung zu nutzen.

Listing 3. Konfiguration der zweiten DataSource
@Configuration
@EnableJpaRepositories(basePackages = "de.beispiel.zweidatasources.persistencesec", entityManagerFactoryRef = "entityManagerFactorySec", transactionManagerRef = "transactionManagerSec")
public class Persistence2Config {

    @Bean("secondaryDatabaseProperties")
    @ConfigurationProperties("isy.persistence.datasource2")
    DatabaseProperties secondaryDatabaseProperties() {
        return new DatabaseProperties();
    }

    @Bean("secondaryDataSource")
    @ConfigurationProperties(prefix = "isy.persistence.datasource2.config")
    public DataSource secondaryDataSource(@Qualifier("secondaryDatabaseProperties") DataSourceProperties secondaryDatabaseProperties) {
        return secondaryDatabaseProperties.initializeDataSourceBuilder().build();
    }

    @Bean("secondaryAppDataSource")
    @DependsOnDatabaseInitialization
    public IsyDataSource secondaryAppDataSource(
        @Qualifier("secondaryDataSource") DataSource secondaryDataSource,
        @Qualifier("secondaryDatabaseProperties") DatabaseProperties secondaryDatabaseProperties
    ) {
        IsyDataSource isyDataSource = new IsyDataSource();
        isyDataSource.setSchemaVersion(secondaryDatabaseProperties.getSchemaVersion());
        isyDataSource.setInvalidSchemaVersionAction(secondaryDatabaseProperties.getSchemaInvalidVersionAction());
        isyDataSource.setTargetDataSource(secondaryDataSource);
        return isyDataSource;
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactorySec(
        @Qualifier("secondaryAppDataSource") DataSource secondaryAppDataSource
    ) {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setPackagesToScan("de.beispiel.zweidatasource.persistencesec");
        em.setDataSource(secondaryAppDataSource);
        em.setJpaDialect(new HibernateJpaDialect());

        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        vendorAdapter.setGenerateDdl(true);
        vendorAdapter.setDatabase(...); (1)
        vendorAdapter.setShowSql(false);
        em.setJpaVendorAdapter(vendorAdapter);

        return em;
    }

    @Bean
    public PlatformTransactionManager transactionManagerSec(@Qualifier("entityManagerFactorySec") EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory);
        return transactionManager;
    }
}
1 Korrekten Hersteller setzen.

Die Datei application.properties wird um den neuen Konfigurationsparameter datasource.second.url für die zweite Datenbankverbindung erweitert.

3.4. Konfiguration der ID und Sequenz

Primärschlüssel werden in JPA mittels der @Id und @GeneratedValue Annotation markiert. Der GenerationType der @GeneratedValue Annotation muss in jedem Fall AUTO sein. Als Generator kommt idealerweise ein @SequenceGenerator zum Einsatz, der eine Datenbanksequenz benutzt.

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

Ein Konfigurationsbeispiel kann folgendermaßen aussehen:

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