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.
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.
@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;
}
}
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.
@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.
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?