Einleitung: Wenn CRUD nicht mehr ausreicht
\n\nStellen Sie sich vor, Sie entwickeln ein System für den Börsenhandel. Jede Sekunde – tausende Orders. Jede Order ist nicht einfach "Datensatz einfügen". Es umfasst Kontostandsprüfung, Portfolio-Neuberechnung, Audit, Benachrichtigungen. Und nun stellen Sie sich vor, Sie müssen nicht nur den aktuellen Zustand speichern, sondern auch wiederherstellen, was um 14:35:22.003 gestern passiert ist. Herkömmliches CRUD (Create, Read, Update, Delete) versagt hier. Willkommen in der Welt von CQRS und Event Sourcing in Java.
\n\nDiese beiden Muster sind nicht nur Modewörter. Sie sind Werkzeuge, mit denen Sie Systeme mit hoher Leistung, vollständiger Auditierung und der Möglichkeit des "Zurückspulens der Zeit" erstellen können. In diesem Artikel analysieren wir, wie man CQRS (Command Query Responsibility Segregation) und Event Sourcing in Java implementiert, unter Verwendung moderner Frameworks und sauberem Code. Ohne Umschweife, mit Beispielen.
\n\nWas ist CQRS in einfachen Worten
\n\nCQRS ist die Trennung der Verantwortlichkeiten zwischen Befehlen (Schreiben) und Abfragen (Lesen). Im klassischen CRUD verwenden Sie dasselbe Modell für Lesen und Schreiben. Bei CQRS erstellen Sie zwei verschiedene Modelle: eines zum Ändern von Daten (Befehle), ein anderes zum Abrufen (Abfragen).
\n\nWarum wird dies benötigt?
\n- \n
- Leistung: Lesen und Schreiben können unabhängig skaliert werden. Sie können 10 Replikate zum Lesen und 2 zum Schreiben haben. \n
- Sicherheit: Befehle können Geschäftsregeln prüfen, während Abfragen einfach Daten liefern. \n
- Flexibilität: Für das Lesen können denormalisierte Ansichten verwendet werden, für das Schreiben normalisierte. \n
Codebeispiel: Trennung von Befehlen und Abfragen
\n\nAngenommen, wir haben ein Auftragsverwaltungssystem. Anstatt OrderService.save(order) zu verwenden, trennen wir:
// Ein Befehl ist die Absicht, den Zustand zu ändern\npublic class CreateOrderCommand {\n private final String orderId;\n private final String customerId;\n private final List<OrderItem> items;\n\n // Konstruktor, Getter...\n}\n\n// Befehlshandler\n@Component\npublic class CreateOrderCommandHandler {\n @Autowired\n private OrderRepository orderRepository;\n\n public void handle(CreateOrderCommand command) {\n // Validierung der Geschäftsregeln\n if (command.getItems().isEmpty()) {\n throw new IllegalArgumentException("Order must have at least one item");\n }\n Order order = new Order(command.getOrderId(), command.getCustomerId(), command.getItems());\n orderRepository.save(order);\n }\n}\n\n// Eine Abfrage ist das Abrufen von Daten\npublic class GetOrderQuery {\n private final String orderId;\n // Konstruktor, Getter...\n}\n\n// Abfragehandler\n@Component\npublic class GetOrderQueryHandler {\n @Autowired\n private OrderReadRepository orderReadRepository;\n\n public OrderDTO handle(GetOrderQuery query) {\n // Direktes Lesen aus einer optimierten Datenbank\n return orderReadRepository.findById(query.getOrderId());\n }\n}\n\nBeachten Sie: OrderRepository (Schreiben) und OrderReadRepository (Lesen) sind unterschiedliche Schnittstellen. Sie können auf verschiedene Datenbanken oder verschiedene Schemata verweisen.
Event Sourcing: Nicht den Zustand, sondern die Geschichte speichern
\n\nEvent Sourcing ist ein Muster, bei dem wir nicht den aktuellen Zustand eines Objekts speichern, sondern die Abfolge von Ereignissen, die zu diesem Zustand geführt haben. Anstatt "Auftrag Nr. 123: Status = DELIVERED" speichern wir: "Ereignis 1: Auftrag erstellt", "Ereignis 2: Artikel hinzugefügt", "Ereignis 3: Auftrag bezahlt", "Ereignis 4: Auftrag geliefert".
\n\nWie es in Java funktioniert
\n\nJedes Ereignis ist ein unveränderliches Objekt (immutable). Wir speichern sie in einem Event Store (spezialisierte Datenbank). Der aktuelle Zustand des Aggregats wird durch das Abspielen (replaying) aller Ereignisse berechnet.
\n\n// Basisereignis\npublic interface Do