CQRS Pattern in ABAP Cloud: Lese- und Schreiboperationen optimal trennen

kategorie
Best Practices
Veröffentlicht
autor
Johannes

CQRS (Command Query Responsibility Segregation) ist ein Architekturmuster, das Lese- und Schreiboperationen strikt voneinander trennt. In ABAP Cloud ermoeglicht CQRS hochoptimierte Systeme, bei denen Query- und Command-Pfade unabhaengig voneinander skaliert und optimiert werden koennen.

Was ist CQRS?

CQRS basiert auf dem Prinzip der Command Query Separation (CQS), erweitert dieses aber auf Architekturebene:

┌─────────────────────────────────────────────────────────────────────────────┐
│ CQRS Architecture │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────┐ │
│ │ Client │ │
│ └─────────┬─────────┘ │
│ │ │
│ ┌─────────────┴─────────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Command API │ │ Query API │ │
│ │ (Schreiben) │ │ (Lesen) │ │
│ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Command Model │ │ Query Model │ │
│ │ (Validierung, │ Event │ (Optimiert │ │
│ │ Geschaefts- │ ──────> │ fuer Lesen, │ │
│ │ logik) │ │ Denormalisiert│ │
│ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Write Store │ │ Read Store │ │
│ │ (Normalisiert) │ │ (Denormalisiert)│ │
│ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘

Grundprinzipien

PrinzipBeschreibung
TrennungLese- und Schreibmodelle sind vollstaendig getrennt
OptimierungJedes Modell ist fuer seinen Zweck optimiert
SkalierbarkeitQuery- und Command-Seite unabhaengig skalierbar
FlexibilitaetUnterschiedliche Speicher fuer Lesen und Schreiben moeglich

Wann ist CQRS sinnvoll?

CQRS lohnt sich nicht fuer jedes Projekt. Hier sind Entscheidungskriterien:

CQRS empfohlen

SzenarioGrund
Hohe Leserate (>90% Reads)Query-Optimierung bringt groessten Nutzen
Komplexe LesemodelleAggregierte, denormalisierte Views sinnvoll
Unterschiedliche SkalierungLese- und Schreiblast stark unterschiedlich
Event Sourcing geplantCQRS ist natuerliche Ergaenzung
Komplexe DomaenenlogikCommand-Seite kann fokussiert bleiben

CQRS nicht empfohlen

SzenarioGrund
Einfache CRUD-AnwendungenOverhead ueberwiegt Nutzen
Balancierte Lese-/SchreiblastKeine eindeutige Optimierungsrichtung
Starke KonsistenzanforderungEventual Consistency problematisch
Kleines TeamZusaetzliche Komplexitaet schwer wartbar

CQRS in ABAP Cloud

In ABAP Cloud laesst sich CQRS elegant mit RAP umsetzen. Das Command Model entspricht dem transaktionalen RAP BO, waehrend das Query Model durch optimierte CDS Views repraesentiert wird.

Architekturueberblick

┌─────────────────────────────────────────────────────────────────────────────┐
│ CQRS in ABAP Cloud │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Fiori App │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │ │
│ │ Mutations │ Queries │
│ ▼ ▼ │
│ ┌─────────────────────────────┐ ┌──────────────────────────────────┐ │
│ │ Transactional Service │ │ Analytical Service │ │
│ │ (Command API) │ │ (Query API) │ │
│ │ │ │ │ │
│ │ ┌───────────────────────┐ │ │ ┌────────────────────────────┐ │ │
│ │ │ ZC_Order (Projection) │ │ │ │ ZC_OrderAnalytics (Read) │ │ │
│ │ │ - create, update │ │ │ │ - Aggregationen │ │ │
│ │ │ - delete, actions │ │ │ │ - Joins, Berechnungen │ │ │
│ │ └───────────────────────┘ │ │ └────────────────────────────┘ │ │
│ │ │ │ │ │ │ │
│ │ ▼ │ │ │ │ │
│ │ ┌───────────────────────┐ │ │ │ │ │
│ │ │ ZI_Order (Interface) │ │ │ │ │ │
│ │ │ + Behavior Definition │ │ │ │ │ │
│ │ └───────────────────────┘ │ │ │ │ │
│ │ │ │ │ │ │ │
│ └─────────────┼───────────────┘ └───────────────┼──────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Database Tables │ │
│ │ (Normalisiert, Single Source of Truth) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘

Query Model (Optimiert fuer Lesen)

Das Query Model in ABAP Cloud besteht aus CDS Views, die fuer schnelle Lesezugriffe optimiert sind.

Basis: Denormalisierte Lese-View

@EndUserText.label: 'Bestellungen - Query Model'
@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #CHECK
@Metadata.ignorePropagatedAnnotations: true
@ObjectModel.usageType: {
serviceQuality: #A,
sizeCategory: #L,
dataClass: #TRANSACTIONAL
}
define view entity ZI_OrderQuery
as select from zorder as Order
-- Denormalisierung: Kundendaten direkt eingebunden
inner join zcustomer as Customer
on Order.customer_id = Customer.customer_id
-- Denormalisierung: Materialstamm
left outer join zmaterial as Material
on Order.material_id = Material.material_id
{
key Order.order_id as OrderId,
Order.order_date as OrderDate,
Order.status as Status,
-- Kundendaten (denormalisiert)
Customer.customer_id as CustomerId,
Customer.customer_name as CustomerName,
Customer.city as CustomerCity,
Customer.country as CustomerCountry,
Customer.credit_limit as CustomerCreditLimit,
-- Materialdaten (denormalisiert)
Material.material_id as MaterialId,
Material.material_text as MaterialText,
Material.material_group as MaterialGroup,
-- Berechnete Felder
Order.quantity as Quantity,
Order.unit_price as UnitPrice,
Order.quantity * Order.unit_price as TotalAmount,
-- Berechneter Status-Text
case Order.status
when 'N' then 'Neu'
when 'P' then 'In Bearbeitung'
when 'C' then 'Abgeschlossen'
when 'X' then 'Storniert'
else 'Unbekannt'
end as StatusText,
-- Zeitliche Klassifizierung
case
when Order.order_date >= $session.system_date then 'Heute'
when Order.order_date >= $session.system_date - 7 then 'Diese Woche'
when Order.order_date >= $session.system_date - 30 then 'Dieser Monat'
else 'Aelter'
end as TimeCategory
}

Aggregierte Analyse-View

@EndUserText.label: 'Bestellungen - Aggregierte Analyse'
@Analytics.dataCategory: #CUBE
define view entity ZI_OrderAnalytics
as select from ZI_OrderQuery
{
-- Dimensionen
@AnalyticsDetails.query.axis: #ROWS
CustomerCountry,
@AnalyticsDetails.query.axis: #ROWS
MaterialGroup,
@AnalyticsDetails.query.axis: #ROWS
TimeCategory,
@AnalyticsDetails.query.axis: #ROWS
Status,
-- Kennzahlen
@Aggregation.default: #SUM
TotalAmount,
@Aggregation.default: #SUM
Quantity,
@Aggregation.default: #COUNT
@EndUserText.label: 'Anzahl Bestellungen'
cast(1 as abap.int4) as OrderCount,
@Aggregation.default: #AVG
@EndUserText.label: 'Durchschnittlicher Bestellwert'
TotalAmount as AvgOrderValue
}

Query Service Definition

@EndUserText.label: 'Order Query Service'
@ObjectModel.query.implementedBy: 'ABAP:ZCL_ORDER_QUERY_PROVIDER'
define custom entity ZCE_OrderQuery
{
key OrderId : abap.char(10);
OrderDate : abap.dats;
CustomerName : abap.char(80);
CustomerCountry: land1;
MaterialText : abap.char(40);
TotalAmount : abap.dec(15,2);
StatusText : abap.char(20);
-- Erweiterte Query-Felder
CustomerRanking: abap.int4;
DaysOpen : abap.int4;
}

Query Provider Implementation

CLASS zcl_order_query_provider DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_rap_query_provider.
ENDCLASS.
CLASS zcl_order_query_provider IMPLEMENTATION.
METHOD if_rap_query_provider~select.
" Paging und Sortierung aus Request
DATA(lv_top) = io_request->get_paging( )->get_page_size( ).
DATA(lv_skip) = io_request->get_paging( )->get_offset( ).
DATA(lt_sort) = io_request->get_sort_elements( ).
DATA(lt_filter) = io_request->get_filter( )->get_as_ranges( ).
" Optimierte Query zusammenbauen
SELECT
order~order_id AS orderid,
order~order_date AS orderdate,
cust~customer_name AS customername,
cust~country AS customercountry,
mat~material_text AS materialtext,
order~quantity * order~unit_price AS totalamount,
CASE order~status
WHEN 'N' THEN 'Neu'
WHEN 'P' THEN 'In Bearbeitung'
WHEN 'C' THEN 'Abgeschlossen'
ELSE 'Unbekannt'
END AS statustext,
-- Ranking basierend auf Umsatz
DENSE_RANK( ) OVER( ORDER BY order~quantity * order~unit_price DESC )
AS customerranking,
DATS_DAYS_BETWEEN( order~order_date, @sy-datum ) AS daysopen
FROM zorder AS order
INNER JOIN zcustomer AS cust ON order~customer_id = cust~customer_id
LEFT OUTER JOIN zmaterial AS mat ON order~material_id = mat~material_id
INTO TABLE @DATA(lt_result)
UP TO @lv_top ROWS
OFFSET @lv_skip.
" Ergebnis zurueckgeben
io_response->set_data( lt_result ).
io_response->set_total_number_of_records( lines( lt_result ) ).
ENDMETHOD.
ENDCLASS.

Command Model (Optimiert fuer Schreiben)

Das Command Model fokussiert sich auf Geschaeftslogik, Validierungen und transaktionale Integritaet.

Interface View (Normalisiert)

@EndUserText.label: 'Bestellung - Command Model'
@AccessControl.authorizationCheck: #CHECK
define view entity ZI_Order
as select from zorder
{
key order_id as OrderId,
customer_id as CustomerId,
material_id as MaterialId,
order_date as OrderDate,
quantity as Quantity,
unit_price as UnitPrice,
status as Status,
created_by as CreatedBy,
created_at as CreatedAt,
changed_by as ChangedBy,
changed_at as ChangedAt,
-- Assoziationen (keine Denormalisierung)
_Customer,
_Material,
_Items
}

Behavior Definition (Commands)

managed implementation in class zbp_i_order unique;
strict ( 2 );
with draft;
define behavior for ZI_Order alias Order
persistent table zorder
draft table zorder_d
lock master total etag ChangedAt
authorization master ( instance )
etag master ChangedAt
{
// Standard-Commands
create;
update;
delete;
// Feldsteuerung
field ( readonly ) OrderId, CreatedBy, CreatedAt, ChangedBy, ChangedAt;
field ( mandatory ) CustomerId, MaterialId, Quantity;
// Business Actions (Commands)
action confirm result [1] $self;
action cancel result [1] $self;
action reopen result [1] $self;
// Factory Action
factory action copyOrder [1];
// Validierungen
validation validateCustomer on save { create; update; field CustomerId; }
validation validateQuantity on save { create; update; field Quantity; }
validation validateStatus on save { field Status; }
// Determinations
determination calculatePrice on modify { field Quantity, MaterialId; }
determination setInitialStatus on modify { create; }
// Events fuer Query-Synchronisation
event orderCreated;
event orderConfirmed;
event orderCancelled;
event orderChanged;
draft action Edit;
draft action Activate;
draft action Discard;
draft action Resume;
draft determine action Prepare;
}

Behavior Implementation

CLASS zbp_i_order DEFINITION LOCAL.
ENDCLASS.
CLASS lhc_order DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
METHODS get_instance_authorizations FOR INSTANCE AUTHORIZATION
IMPORTING keys REQUEST requested_authorizations FOR Order RESULT result.
METHODS validateCustomer FOR VALIDATE ON SAVE
IMPORTING keys FOR Order~validateCustomer.
METHODS validateQuantity FOR VALIDATE ON SAVE
IMPORTING keys FOR Order~validateQuantity.
METHODS confirm FOR MODIFY
IMPORTING keys FOR ACTION Order~confirm RESULT result.
METHODS cancel FOR MODIFY
IMPORTING keys FOR ACTION Order~cancel RESULT result.
METHODS calculatePrice FOR DETERMINE ON MODIFY
IMPORTING keys FOR Order~calculatePrice.
METHODS setInitialStatus FOR DETERMINE ON MODIFY
IMPORTING keys FOR Order~setInitialStatus.
ENDCLASS.
CLASS lhc_order IMPLEMENTATION.
METHOD get_instance_authorizations.
ENDMETHOD.
METHOD validateCustomer.
" Lese betroffene Bestellungen
READ ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
FIELDS ( CustomerId )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_orders).
" Pruefe Kunden
SELECT customer_id, credit_limit, blocked
FROM zcustomer
FOR ALL ENTRIES IN @lt_orders
WHERE customer_id = @lt_orders-CustomerId
INTO TABLE @DATA(lt_customers).
LOOP AT lt_orders INTO DATA(ls_order).
READ TABLE lt_customers INTO DATA(ls_customer)
WITH KEY customer_id = ls_order-CustomerId.
IF sy-subrc <> 0.
APPEND VALUE #(
%tky = ls_order-%tky
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = |Kunde { ls_order-CustomerId } existiert nicht| )
) TO reported-order.
APPEND VALUE #( %tky = ls_order-%tky ) TO failed-order.
CONTINUE.
ENDIF.
IF ls_customer-blocked = abap_true.
APPEND VALUE #(
%tky = ls_order-%tky
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = |Kunde { ls_order-CustomerId } ist gesperrt| )
) TO reported-order.
APPEND VALUE #( %tky = ls_order-%tky ) TO failed-order.
ENDIF.
ENDLOOP.
ENDMETHOD.
METHOD validateQuantity.
READ ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
FIELDS ( Quantity )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_orders).
LOOP AT lt_orders INTO DATA(ls_order) WHERE Quantity <= 0.
APPEND VALUE #(
%tky = ls_order-%tky
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = |Menge muss groesser als 0 sein| )
%element-Quantity = if_abap_behv=>mk-on
) TO reported-order.
APPEND VALUE #( %tky = ls_order-%tky ) TO failed-order.
ENDLOOP.
ENDMETHOD.
METHOD confirm.
" Lese Bestellungen
READ ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(lt_orders).
LOOP AT lt_orders INTO DATA(ls_order).
" Pruefe ob Bestaetigung moeglich
IF ls_order-Status <> 'N'.
APPEND VALUE #(
%tky = ls_order-%tky
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = |Nur neue Bestellungen koennen bestaetigt werden| )
) TO reported-order.
APPEND VALUE #( %tky = ls_order-%tky ) TO failed-order.
CONTINUE.
ENDIF.
" Status aendern
MODIFY ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
UPDATE FIELDS ( Status )
WITH VALUE #( (
%tky = ls_order-%tky
Status = 'C' " Confirmed
) )
REPORTED DATA(lt_update_reported)
FAILED DATA(lt_update_failed).
" Event ausloesen fuer Query-Synchronisation
RAISE ENTITY EVENT zi_order~orderConfirmed
FROM VALUE #( ( OrderId = ls_order-OrderId ) ).
ENDLOOP.
" Ergebnis zurueckgeben
READ ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(lt_result).
result = VALUE #( FOR order IN lt_result (
%tky = order-%tky
%param = order
) ).
ENDMETHOD.
METHOD cancel.
READ ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(lt_orders).
LOOP AT lt_orders INTO DATA(ls_order).
IF ls_order-Status = 'X'.
APPEND VALUE #(
%tky = ls_order-%tky
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-warning
text = |Bestellung bereits storniert| )
) TO reported-order.
CONTINUE.
ENDIF.
MODIFY ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
UPDATE FIELDS ( Status )
WITH VALUE #( (
%tky = ls_order-%tky
Status = 'X'
) )
REPORTED DATA(lt_update_reported).
RAISE ENTITY EVENT zi_order~orderCancelled
FROM VALUE #( ( OrderId = ls_order-OrderId ) ).
ENDLOOP.
READ ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(lt_result).
result = VALUE #( FOR order IN lt_result (
%tky = order-%tky
%param = order
) ).
ENDMETHOD.
METHOD calculatePrice.
READ ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
FIELDS ( MaterialId Quantity )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_orders).
" Hole Preise aus Materialstamm
SELECT material_id, standard_price
FROM zmaterial
FOR ALL ENTRIES IN @lt_orders
WHERE material_id = @lt_orders-MaterialId
INTO TABLE @DATA(lt_prices).
LOOP AT lt_orders INTO DATA(ls_order).
READ TABLE lt_prices INTO DATA(ls_price)
WITH KEY material_id = ls_order-MaterialId.
IF sy-subrc = 0.
MODIFY ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
UPDATE FIELDS ( UnitPrice )
WITH VALUE #( (
%tky = ls_order-%tky
UnitPrice = ls_price-standard_price
) ).
ENDIF.
ENDLOOP.
ENDMETHOD.
METHOD setInitialStatus.
MODIFY ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
UPDATE FIELDS ( Status )
WITH VALUE #( FOR key IN keys (
%tky = key-%tky
Status = 'N' " New
) ).
ENDMETHOD.
ENDCLASS.

Event-basierte Synchronisation

In einer vollstaendigen CQRS-Architektur werden Query- und Command-Modell durch Events synchronisiert.

Event Handler fuer Query-Aktualisierung

CLASS zcl_order_event_handler DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_rap_event_handler.
ENDCLASS.
CLASS zcl_order_event_handler IMPLEMENTATION.
METHOD if_rap_event_handler~handle.
" Dispatch basierend auf Event-Typ
CASE io_event->get_event( ).
WHEN 'ORDERCREATED'.
handle_order_created( io_event ).
WHEN 'ORDERCONFIRMED'.
handle_order_confirmed( io_event ).
WHEN 'ORDERCANCELLED'.
handle_order_cancelled( io_event ).
WHEN 'ORDERCHANGED'.
handle_order_changed( io_event ).
ENDCASE.
ENDMETHOD.
METHOD handle_order_created.
" Query-Cache aktualisieren oder Read-Store synchronisieren
DATA(lt_event_data) = io_event->get_converted_event_data( ).
" Bei separatem Read-Store: Denormalisierte Daten schreiben
" Bei Cache-Loesung: Cache invalidieren
LOOP AT lt_event_data INTO DATA(ls_event).
" Hole vollstaendige Bestelldaten
SELECT SINGLE *
FROM zi_orderquery
WHERE orderid = @ls_event-orderid
INTO @DATA(ls_order_query).
" Schreibe in Read-Store (falls separate Tabelle)
" MODIFY zorder_read FROM ls_order_query.
" Oder: Cache invalidieren
" zcl_order_cache=>invalidate( ls_event-orderid ).
ENDLOOP.
ENDMETHOD.
METHOD handle_order_confirmed.
" Status im Read-Store aktualisieren
ENDMETHOD.
METHOD handle_order_cancelled.
" Status im Read-Store aktualisieren
ENDMETHOD.
METHOD handle_order_changed.
" Geaenderte Felder im Read-Store aktualisieren
ENDMETHOD.
ENDCLASS.

Event Registration

CLASS zcl_order_event_registration DEFINITION
PUBLIC FINAL.
PUBLIC SECTION.
CLASS-METHODS class_constructor.
ENDCLASS.
CLASS zcl_order_event_registration IMPLEMENTATION.
METHOD class_constructor.
" Registriere Event Handler
cl_abap_behv_event_registration=>register_event_handler(
handler_class = 'ZCL_ORDER_EVENT_HANDLER'
entity = 'ZI_ORDER'
).
ENDMETHOD.
ENDCLASS.

Integration mit RAP

Die CQRS-Trennung in RAP erfolgt durch separate Services fuer Lesen und Schreiben.

Transactional Service (Commands)

@EndUserText.label: 'Order Management Service'
define service ZUI_ORDER_O4 {
expose ZC_Order as Order;
expose ZC_OrderItem as OrderItem;
}

Analytics Service (Queries)

@EndUserText.label: 'Order Analytics Service'
define service ZUI_ORDER_ANALYTICS_O4 {
expose ZC_OrderAnalytics as OrderAnalytics;
expose ZC_OrderQuery as OrderQuery;
}

Service Binding Trennung

In den Service Bindings wird explizit der Zweck definiert:

  • Transactional Service: OData V4 mit vollstaendigem CRUD
  • Analytics Service: OData V4 Analytics (nur Lesezugriff)

Best Practices

1. Klare Verantwortlichkeiten

" Command Handler - fokussiert auf Geschaeftslogik
CLASS zcl_order_command_handler DEFINITION.
PUBLIC SECTION.
METHODS create_order
IMPORTING is_order TYPE zif_order_types=>ts_create_command
RAISING zcx_order_validation.
METHODS confirm_order
IMPORTING iv_order_id TYPE zorder-order_id
RAISING zcx_order_state.
ENDCLASS.
" Query Handler - fokussiert auf Datenabruf
CLASS zcl_order_query_handler DEFINITION.
PUBLIC SECTION.
METHODS get_orders_by_customer
IMPORTING iv_customer_id TYPE zcustomer-customer_id
RETURNING VALUE(rt_orders) TYPE zif_order_types=>tt_order_summary.
METHODS get_order_statistics
IMPORTING iv_date_from TYPE datum
iv_date_to TYPE datum
RETURNING VALUE(rs_stats) TYPE zif_order_types=>ts_statistics.
ENDCLASS.

2. Eventual Consistency beruecksichtigen

" Query mit Konsistenz-Hinweis
METHOD get_order_status.
" Hinweis: Daten koennen bis zu X Sekunden verzoegert sein
SELECT SINGLE status, last_sync_timestamp
FROM zorder_read
WHERE order_id = @iv_order_id
INTO @DATA(ls_result).
" Bei kritischen Abfragen: Direkt aus Command-Store lesen
IF iv_require_consistent = abap_true.
SELECT SINGLE status
FROM zorder
WHERE order_id = @iv_order_id
INTO @DATA(lv_status).
ls_result-status = lv_status.
ENDIF.
rs_result = ls_result.
ENDMETHOD.

3. Separate Datenmodelle

AspektCommand ModelQuery Model
Normalisierung3NF, normalisiertDenormalisiert
OptimierungSchreib-PerformanceLese-Performance
IndizesWrite-optimiertRead-optimiert
ValidierungVollstaendigKeine
GeschaeftslogikJaNein

4. Idempotente Commands

METHOD execute_command.
" Pruefe ob Command bereits ausgefuehrt wurde
SELECT SINGLE @abap_true
FROM zcommand_log
WHERE command_id = @is_command-command_id
INTO @DATA(lv_exists).
IF lv_exists = abap_true.
" Idempotent: Bereits ausgefuehrt, kein Fehler
RETURN.
ENDIF.
" Command ausfuehren
" ...
" Loggen
INSERT zcommand_log FROM @( VALUE #(
command_id = is_command-command_id
executed_at = utclong_current( )
executed_by = sy-uname
) ).
ENDMETHOD.

Zusammenfassung

AspektBeschreibung
Query ModelCDS Views, denormalisiert, lese-optimiert
Command ModelRAP BO mit Behavior Definition, normalisiert
SynchronisationRAP Business Events oder Background Jobs
KonsistenzEventual Consistency akzeptieren
SkalierungGetrennte Services fuer Lesen/Schreiben

CQRS in ABAP Cloud ermoeglicht hochoptimierte Systeme fuer komplexe Anforderungen. Die Trennung von Lese- und Schreibmodell bietet Flexibilitaet bei der Optimierung und Skalierung. Allerdings erfordert das Pattern zusaetzliche Komplexitaet und sollte nur bei klarem Nutzen eingesetzt werden.

Verwandte Themen