Spring Data JPA / JDBC

Diese Seite bleibt auch ohne JavaScript lesbar. Suche und Buttons sind Zusatzkomfort.

Spring Data, JPA, JDBC und Transaktionen

Datenzugriff als Architekturthema: Repositories, Aggregate, SQL, Locking und Konsistenz.

Data Access: Repository-Abstraktion und Transaktionsgrenze Use Case@Transactional RepositoryInterface JPA/JDBCMapping/SQL DatenbankACID/Index Fachliche Konsistenz entsteht an der Service-/Use-Case-Grenze, nicht im Controller.N+1, Lazy Loading und Locking sind Architekturthemen, keine Kleinigkeiten.
1. Repository ist nicht automatisch gutes Domain Design

Spring Data reduziert Implementierungsaufwand, aber es entscheidet nicht, was ein Aggregat ist. Ein Repository sollte fachliche Aggregate laden und speichern, nicht beliebige Tabellenfragmente als globale Utility-Schicht anbieten.

Für relationale CRUD-Anwendungen ist Spring Data JPA stark. Für hochoptimierte Reports, komplexe Joins, große Datenmengen oder bewusstes SQL kann Spring JDBC/JdbcClient klarer und kontrollierbarer sein.

JPA Entity mit Optimistic Locking
@Entity
@Table(name = "orders")
class OrderEntity {
    @Id
    private UUID id;

    @Version
    private long version;

    @Enumerated(EnumType.STRING)
    private OrderStatus status;

    protected OrderEntity() {}

    void approve() {
        if (status != OrderStatus.SUBMITTED) {
            throw new IllegalStateException("Only submitted orders can be approved");
        }
        status = OrderStatus.APPROVED;
    }
}
Repository Interface
interface OrderJpaRepository extends JpaRepository<OrderEntity, UUID> {
    @EntityGraph(attributePaths = "lines")
    Optional<OrderEntity> findWithLinesById(UUID id);

    @Query("select o from OrderEntity o where o.status = :status")
    Page<OrderEntity> findByStatus(@Param("status") OrderStatus status, Pageable pageable);
}
2. Transaktionsgrenzen

@Transactional gehört in der Regel an die Use-Case-/Application-Service-Grenze. Dort wird eine fachliche Einheit verarbeitet. Controller-Transaktionen sind oft zu breit, Repository-Transaktionen oft zu klein.

Read-only-Transaktionen können Optimierungen und klarere Absicht liefern. Schreibtransaktionen sollten externe IO-Aufrufe nicht unnötig lange einschließen, weil sonst Datenbanklocks und Remote-Wartezeit gekoppelt werden.

Use Case als Transaktionsgrenze
@Service
class SubmitOrderService {
    private final OrderJpaRepository orders;
    private final DomainEventPublisher events;

    @Transactional
    public UUID submit(SubmitOrder command) {
        OrderEntity order = OrderEntity.create(command.customerId(), command.lines());
        orders.save(order);
        events.publishAfterCommit(new OrderSubmitted(order.id()));
        return order.id();
    }
}
3. N+1 und Lazy Loading

N+1 ist kein reines Performance-Problem, sondern ein Architektur-Signal: Die API braucht Daten in einer Form, die das Repository nicht bewusst bereitstellt. Lösungen sind EntityGraph, Fetch Join, DTO-Projektion, getrennte Read Models oder explizites SQL.

DTO-Projektion für Read API
public record OrderSummary(UUID id, String customerName, BigDecimal total) {}

interface OrderReadRepository extends Repository<OrderEntity, UUID> {
    @Query("""
        select new com.example.OrderSummary(o.id, c.name, o.total)
        from OrderEntity o join o.customer c
        where o.status = :status
    """)
    List<OrderSummary> summaries(OrderStatus status);
}

Enterprise-Prüffragen

  • Repository pro fachlichem Aggregate statt Tabellen-Utility?
  • Transaktion am Use Case?
  • N+1 bewusst getestet?
  • Locking-Strategie für konkurrierende Änderungen definiert?