Modul 04 · Migration Deep Dive · Stufe 4 Daten und Transaktionen

Persistenz

JDBC und DAO wird nachvollziehbar in Spring Data JPA überführt. Die Akte zeigt nicht nur das Ziel, sondern die tatsächlichen Dateien, APIs, Dependencies, Codebelege, Tests, Risiken und Cutover-Schritte.

DAORepositoryService Layer
4 → 4Produktionsdateien
103 → 95Java-Zeilen
1 → 1Testdateien
1 / 2Dependencies entfernt / neu
NIEDRIGRisiko · Score 0

Was sich konkret ändert

DimensionLegacyModernMigrationskonsequenz
ProgrammiermodellOverride, exampleBean, Entity, GeneratedValue, Id, Override, Service, SpringBootApplication, Table, Transactional, exampleAnnotationen und Containerfunktionen werden nur dort eingesetzt, wo sie eine konkrete technische Verantwortung übernehmen.
Abhängigkeiten2 direkte Dependencies3 direkte Dependencies2 neu, 1 entfernt; Versionen und transitive Auswirkungen im erfolgreichen Online-Build prüfen.
Öffentliche API3 erkannte Methoden7 erkannte MethodenMethoden werden nach fachlicher Rolle gemappt; reine Bootstrap- und Framework-Methoden sind kein fachlicher Vertrag.
Datenmodellclass JdbcCustomerDao, class PersistenceLegacyApplication, interface CustomerDao, record Customerclass CustomerEntity, class CustomerService, class PersistenceModernApplication, interface CustomerRepositoryFeldnamen, IDs, Null-Semantik, Gleichheit und Serialisierungsform werden separat regressionstestet.
FehlerverhaltenLegacy-Exceptions und Rückgabewerteexplizitere Fach-/Framework-FehlerabbildungFehler dürfen nicht nur technisch übersetzt werden; Status, Ursache, Retrybarkeit und Client-Vertrag müssen erhalten oder versioniert werden.
TestsBestands- und Golden-Master-TestsUnit-, Slice-, Contract- und IntegrationstestsDer Modern-Pfad wird zuerst gegen denselben fachlichen Vektor geprüft und danach um neue technische Risiken ergänzt.
BetriebLegacy-Start/Lifecyclemodernes Packaging, Health und externe KonfigurationMigration mit produktionsnahen Daten, Backups, Restore-Probe und Query-/Lock-Monitoring absichern.
RollbackLegacy-Artefakt bleibt unverändertModern-Artefakt getrennt deploybarKein Rollback über Datenverlust: Schema, Nachrichten und verschlüsselte Daten müssen rückwärtslesbar oder durch Dual-Read abgesichert sein.

Fachlicher Vertrag: unverändert zu erhalten

  • Tabellen-, Spalten- und Schlüsselstruktur bleibt bis zur kontrollierten Schemaänderung kompatibel.
  • Transaktionsgrenzen liegen im Service und nicht im Controller/Repository.
  • Lazy Loading tritt nicht außerhalb einer aktiven Transaktion auf.

Hauptrisiko und Testfokus

schema drift, lazy loading and transaction boundaries

repository slice, service transaction and rollback tests

Die Modernisierung gilt erst als abgeschlossen, wenn der fachliche Vertrag automatisiert belegt ist.

Verbindliches Code- und Rollenmapping

Die Zuordnung ist semantisch: Eine Legacy-Klasse kann in mehrere moderne Rollen zerlegt werden.

Legacy-Rolle / DateiModern-Rolle / DateiBedeutung
CustomerDao.java + JdbcCustomerDao.javaCustomerRepository.javaDAO/SQL-Zugriff wird Repository-Abstraktion.
Customer.javaCustomerEntity.javaDatenobjekt wird explizite JPA Entity.
PersistenceLegacyApplication.javaPersistenceModernApplication.java + CustomerService.javaBootstrap und transaktionaler Use Case werden getrennt.

Modern-Quellinventar

DateiTypAnnotationenöffentliche APIPatternZeilen
CustomerEntity.javaclass CustomerEntityEntity, GeneratedValue, Id, Override, TableLong getId()
String getName()
String getEmail()
String toString()
41
CustomerRepository.javainterface CustomerRepositoryRepository8
CustomerService.javaclass CustomerServiceService, TransactionalCustomerEntity create(String name, String email)
List<CustomerEntity> findAll()
Service Layer25
PersistenceModernApplication.javaclass PersistenceModernApplicationBean, SpringBootApplication, examplevoid main(String[] args)21

Dependency- und Laufzeitmigration

StatusDependencyVersionScopePrüfung
ENTFERNTorg.junit.jupiter:junit-jupiterBOM/ParenttestLegacy-Laufzeit entfällt oder wird durch Plattform/BOM ersetzt.
NEUorg.springframework.boot:spring-boot-starter-data-jpaBOM/ParentcompileNeue Laufzeit-/Testabhängigkeit; transitiv, lizenz- und security-seitig prüfen.
NEUorg.springframework.boot:spring-boot-starter-testBOM/ParenttestNeue Laufzeit-/Testabhängigkeit; transitiv, lizenz- und security-seitig prüfen.
GEÄNDERTcom.h2database:h2BOM/Parent → BOM/Parentcompile → runtimeGemeinsame Dependency; Version und Scope im effektiven POM prüfen.
Direkte POM-Daten ersetzen keinen erfolgreichen Online-Build mit transitiver SBOM- und CVE-Prüfung.

Vorher-/Nachher-Codebelege

Die folgenden Ausschnitte stammen direkt aus den enthaltenen Projekten. Dadurch ist sichtbar, welche Verantwortung tatsächlich verschoben wurde.

Legacy
CustomerDao.java + JdbcCustomerDao.java
Modern
CustomerRepository.java

Migrationsbedeutung: DAO/SQL-Zugriff wird Repository-Abstraktion.

Legacy-Code

projects/04-persistence/legacy/src/main/java/at/aydin/lab/persistence/legacy/CustomerDao.java

package at.aydin.lab.persistence.legacy;

import java.util.List;

// Design Pattern: DAO
// Zweck: SQL und JDBC werden hinter einer fachlichen Schnittstelle gekapselt.
public interface CustomerDao {
    Customer save(String name, String email);
    List<Customer> findAll();
}

Legacy-Code

projects/04-persistence/legacy/src/main/java/at/aydin/lab/persistence/legacy/JdbcCustomerDao.java

package at.aydin.lab.persistence.legacy;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

public final class JdbcCustomerDao implements CustomerDao {
    private final String url;

    public JdbcCustomerDao(String url) {
        this.url = url;
        initialize();
    }

    private Connection connection() throws SQLException {
        return DriverManager.getConnection(url, "sa", "");
    }

    private void initialize() {
        String sql = "create table if not exists customer("
                + "id bigint generated by default as identity primary key, "
                + "name varchar(120), email varchar(180))";

        try (Connection connection = connection();
                Statement statement = connection.createStatement()) {
            statement.executeUpdate(sql);
        } catch (SQLException exception) {
            throw new IllegalStateException("Schema konnte nicht erstellt werden", exception);
        }
    }

    @Override
    public Customer save(String name, String email) {
        String sql = "insert into customer(name,email) values (?,?)";
        try (Connection connection = connection();
                PreparedStatement statement = connection.prepareStatement(
                        sql, Statement.RETURN_GENERATED_KEYS)) {
            statement.setString(1, name);
            statement.setString(2, email);
            statement.executeUpdate();

            try (ResultSet keys = statement.getGeneratedKeys()) {
                if (!keys.next()) {
                    throw new SQLException("Keine ID erhalten");
                }
                return new Customer(keys.getLong(1), name, email);
            }
        } catch (SQLException exception) {
            throw new IllegalStateException("Kunde konnte nicht gespeichert werden", exception);
        }
    }

    @Override
    public List<Customer> findAll() {
        List<Customer> result = new ArrayList<>();
        String sql = "select id,name,email from customer order by id";

        try (Connection connection = connection();
                Statement statement = connection.createStatement();
                ResultSet resultSet = statement.executeQuery(sql)) {
            while (resultSet.next()) {
                result.add(new Customer(
                        resultSet.getLong("id"),
                        resultSet.getString("name"),
                        resultSet.getString("email")));
            }
            return result;
        } catch (SQLException exception) {
            throw new IllegalStateException("Kunden konnten nicht gelesen werden", exception);
        }
    }
}

Modern-Code

projects/04-persistence/modern/src/main/java/at/aydin/lab/persistence/modern/CustomerRepository.java

package at.aydin.lab.persistence.modern;

import org.springframework.data.jpa.repository.JpaRepository;

// Design Pattern: Repository
// Zweck: Spring Data erzeugt die technische Implementierung aus dem Interface.
public interface CustomerRepository extends JpaRepository<CustomerEntity, Long> {
}
Legacy
Customer.java
Modern
CustomerEntity.java

Migrationsbedeutung: Datenobjekt wird explizite JPA Entity.

Legacy-Code

projects/04-persistence/legacy/src/main/java/at/aydin/lab/persistence/legacy/Customer.java

package at.aydin.lab.persistence.legacy;

public record Customer(long id, String name, String email) {
}

Modern-Code

projects/04-persistence/modern/src/main/java/at/aydin/lab/persistence/modern/CustomerEntity.java

package at.aydin.lab.persistence.modern;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "customer")
public class CustomerEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String email;
    protected CustomerEntity() {
    }

    public CustomerEntity(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }

    @Override
    public String toString() {
        return "CustomerEntity[id=" + id + ", name=" + name + ", email=" + email + "]";
    }
}
Technischen Unified Diff öffnen
--- Customer.java
+++ CustomerEntity.java
@@ -1,4 +1,41 @@
-package at.aydin.lab.persistence.legacy;
+package at.aydin.lab.persistence.modern;
 
-public record Customer(long id, String name, String email) {
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+
+@Entity
+@Table(name = "customer")
+public class CustomerEntity {
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+    private String name;
+    private String email;
+    protected CustomerEntity() {
+    }
+
+    public CustomerEntity(String name, String email) {
+        this.name = name;
+        this.email = email;
+    }
+
+    public Long getId() {
+        return id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getEmail() {
+        return email;
+    }
+
+    @Override
+    public String toString() {
+        return "CustomerEntity[id=" + id + ", name=" + name + ", email=" + email + "]";
+    }
 }
Legacy
PersistenceLegacyApplication.java
Modern
PersistenceModernApplication.java + CustomerService.java

Migrationsbedeutung: Bootstrap und transaktionaler Use Case werden getrennt.

Legacy-Code

projects/04-persistence/legacy/src/main/java/at/aydin/lab/persistence/legacy/PersistenceLegacyApplication.java

package at.aydin.lab.persistence.legacy;

public final class PersistenceLegacyApplication {
    private PersistenceLegacyApplication() {
    }

    public static void main(String[] args) {
        CustomerDao dao = new JdbcCustomerDao("jdbc:h2:mem:legacy;DB_CLOSE_DELAY=-1");
        dao.save("Aydin Polat", "aydin@example.test");
        dao.findAll().forEach(System.out::println);
    }
}

Modern-Code

projects/04-persistence/modern/src/main/java/at/aydin/lab/persistence/modern/PersistenceModernApplication.java

package at.aydin.lab.persistence.modern;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class PersistenceModernApplication {
    public static void main(String[] args) {
        SpringApplication.run(PersistenceModernApplication.class, args);
    }

    @Bean
    CommandLineRunner demo(CustomerService service) {
        return args -> {
            service.create("Aydin Polat", "aydin@example.test");
            service.findAll().forEach(System.out::println);
        };
    }
}

Modern-Code

projects/04-persistence/modern/src/main/java/at/aydin/lab/persistence/modern/CustomerService.java

package at.aydin.lab.persistence.modern;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;

@Service
// Design Pattern: Service Layer
// Zweck: Transaktions- und Fachgrenze über dem Repository.
public class CustomerService {
    private final CustomerRepository repository;
    public CustomerService(CustomerRepository repository) {
        this.repository = repository;
    }

    @Transactional
    public CustomerEntity create(String name, String email) {
        return repository.save(new CustomerEntity(name, email));
    }

    @Transactional(readOnly = true)
    public List<CustomerEntity> findAll() {
        return repository.findAll();
    }
}

Umsetzungsplan mit Qualitäts-Gates

  1. Arbeitspaket 1
    CustomerDao-SQL und ResultSet-Mapping als Golden-Master-Tests sichern. Nachweis: Commit/PR, automatisierter Test und aktualisierte Betriebsdokumentation.
  2. Arbeitspaket 2
    CustomerEntity exakt gegen bestehendes Schema mappen; automatische DDL-Erzeugung deaktivieren. Nachweis: Commit/PR, automatisierter Test und aktualisierte Betriebsdokumentation.
  3. Arbeitspaket 3
    CustomerRepository zunächst nur für lesende Use Cases aktivieren. Nachweis: Commit/PR, automatisierter Test und aktualisierte Betriebsdokumentation.
  4. Arbeitspaket 4
    CustomerService mit expliziten Transaktionsgrenzen einführen. Nachweis: Commit/PR, automatisierter Test und aktualisierte Betriebsdokumentation.
  5. Arbeitspaket 5
    Schreibpfad per Shadow Read/Write oder Vergleichslauf validieren. Nachweis: Commit/PR, automatisierter Test und aktualisierte Betriebsdokumentation.
  6. Arbeitspaket 6
    N+1-, Locking- und Rollback-Verhalten messen, bevor DAO entfernt wird. Nachweis: Commit/PR, automatisierter Test und aktualisierte Betriebsdokumentation.

Konkreter Test- und Abnahmekatalog

IDEbenePrüfungerforderlicher Nachweis
04-PERSISTENCE-A01Integration/ContractAlle bestehenden Datensätze lassen sich ohne Schemaverlust lesen.Automatisierter Test und CI-Protokoll
04-PERSISTENCE-A02Integration/ContractSchreiboperationen erzeugen identische Schlüssel und Pflichtfelder.Automatisierter Test und CI-Protokoll
04-PERSISTENCE-A03Integration/ContractRollback hinterlässt keine Teildaten.Automatisierter Test und CI-Protokoll
04-PERSISTENCE-A04Integration/ContractKeine unkontrollierte DDL-Änderung beim Start.Automatisierter Test und CI-Protokoll
04-PERSISTENCE-A05Integration/ContractQuery-Anzahl und Laufzeit bleiben innerhalb definierter Grenzen.Automatisierter Test und CI-Protokoll
04-PERSISTENCE-T01UnitFachlogik ohne Container oder externen Dienst testen.Unit-Test
04-PERSISTENCE-T02RegressionLegacy- und Modern-Ergebnis für denselben Golden-Master-Vektor vergleichen.Vergleichsreport
04-PERSISTENCE-T03NegativeFehlerhafte, leere und grenzwertige Eingaben prüfen.Negativtest
04-PERSISTENCE-T04OperationsStart, Health, Shutdown und Konfigurationsfehler prüfen.Deployment-/Startprotokoll
04-PERSISTENCE-T05DatabaseRollback, Constraints, Locking und Schema-Kompatibilität mit produktionsnahen Daten prüfen.DB-Integrationstest
04-PERSISTENCE-F01Fokusrepository slice, service transaction and rollback testsModulspezifischer Testreport

Risikoregister des Moduls

RisikoAuswirkungGegenmaßnahmeGate
schema drift, lazy loading and transaction boundariesNIEDRIGrepository slice, service transaction and rollback testsvor Cutover
Neue Framework-/Library-LaufzeitabhängigkeitenMITTELSBOM, Lizenz-, CVE- und transitive Dependency-Prüfung im Online-Buildvor Release
Legacy-Verhalten war implizit an entfernte Library gekoppeltMITTELContract-/Golden-Master-Tests und gezielte Fehlerpfadevor Abschaltung

Rollback und Koexistenz

DAO und JPA können vorübergehend nebeneinander auf dasselbe stabile Schema zugreifen; Schreibhoheit muss dabei eindeutig einer Variante gehören.

Abbruchkriterien

  • Fachlicher Golden-Master weicht ab.
  • Daten-, Nachrichten- oder API-Kompatibilität ist ungeklärt.
  • Fehlerquote, Latenz oder Ressourcenverbrauch überschreiten das vereinbarte Limit.
  • Monitoring oder Rückfallpfad ist nicht funktionsfähig.

Definition of Done

Direkte Arbeitslinks

⌂ Cockpit