Repository Pattern in ABAP Cloud: Saubere Datenzugriffsschicht

kategorie
Best Practices
Veröffentlicht
autor
Johannes

Das Repository Pattern abstrahiert den Datenzugriff und entkoppelt die Geschaeftslogik von der Persistenzschicht. In ABAP Cloud ist es besonders wertvoll fuer komplexe Domaenen, Legacy-Integration und testbare Architekturen.

Grundkonzept

Ein Repository stellt eine Sammlung von Domaenenobjekten dar und bietet Methoden zum Laden, Speichern und Suchen. Die Geschaeftslogik arbeitet nur mit dem Repository-Interface, nicht direkt mit der Datenbank.

┌─────────────────────────────────────────────┐
│ Geschaeftslogik │
│ (Kennt nur Repository-Interface) │
└───────────────────┬─────────────────────────┘
│ Verwendet
┌─────────────────────────────────────────────┐
│ Repository Interface │
│ find_by_id(), find_all(), save() │
└───────────────────┬─────────────────────────┘
│ Implementiert
┌───────────┴───────────┐
▼ ▼
┌───────────────┐ ┌───────────────┐
│ DB-Repository │ │ Mock-Repository│
│ (Produktiv) │ │ (Tests) │
└───────────────┘ └───────────────┘

Abgrenzung zu direktem Datenbankzugriff

AspektDirekter ZugriffRepository Pattern
TestbarkeitSchwer (DB erforderlich)Einfach (Mocks)
WartbarkeitVerstreute SQL-StatementsZentralisiert
FlexibilitaetStarr an DB gebundenAustauschbar
AbstraktionKeineDomaenenobjekte
KomplexitaetNiedrigHoeher

Wann Repository Pattern einsetzen?

Sinnvoll bei:

  • Komplexer Domaenenlogik
  • Mehreren Datenquellen (DB, RFC, HTTP)
  • Hohen Testanforderungen
  • Legacy-Integration (Unmanaged RAP)

Overkill bei:

  • Einfachen CRUD-Operationen
  • Reinen Managed-RAP-Szenarien
  • Prototypen und MVPs

Repository Interface Definition

Das Interface definiert den Vertrag fuer den Datenzugriff. Es verwendet Domaenenobjekte, keine Datenbankstrukturen.

INTERFACE zif_order_repository PUBLIC.
TYPES:
BEGIN OF ty_order,
order_id TYPE sysuuid_x16,
customer_id TYPE sysuuid_x16,
order_date TYPE dats,
status TYPE char2,
total_amount TYPE p LENGTH 16 DECIMALS 2,
currency TYPE waerk,
END OF ty_order,
tt_orders TYPE STANDARD TABLE OF ty_order WITH KEY order_id.
TYPES:
BEGIN OF ty_order_item,
item_id TYPE sysuuid_x16,
order_id TYPE sysuuid_x16,
product_id TYPE sysuuid_x16,
quantity TYPE i,
unit_price TYPE p LENGTH 16 DECIMALS 2,
currency TYPE waerk,
END OF ty_order_item,
tt_order_items TYPE STANDARD TABLE OF ty_order_item WITH KEY item_id.
" Lesen
METHODS find_by_id
IMPORTING
iv_order_id TYPE sysuuid_x16
RETURNING
VALUE(rs_order) TYPE ty_order
RAISING
zcx_not_found.
METHODS find_all
RETURNING
VALUE(rt_orders) TYPE tt_orders.
METHODS find_by_customer
IMPORTING
iv_customer_id TYPE sysuuid_x16
RETURNING
VALUE(rt_orders) TYPE tt_orders.
METHODS find_by_status
IMPORTING
iv_status TYPE char2
RETURNING
VALUE(rt_orders) TYPE tt_orders.
" Items
METHODS get_items
IMPORTING
iv_order_id TYPE sysuuid_x16
RETURNING
VALUE(rt_items) TYPE tt_order_items.
" Schreiben
METHODS save
IMPORTING
is_order TYPE ty_order
it_items TYPE tt_order_items OPTIONAL
RAISING
zcx_save_error.
METHODS delete
IMPORTING
iv_order_id TYPE sysuuid_x16
RAISING
zcx_not_found.
" Existenzpruefung
METHODS exists
IMPORTING
iv_order_id TYPE sysuuid_x16
RETURNING
VALUE(rv_exists) TYPE abap_bool.
ENDINTERFACE.

Interface-Design-Prinzipien

  1. Domaenensprache verwenden: find_by_customer statt select_where_customer
  2. Exceptions statt Rueckgabewerte: Klare Fehlerbehandlung
  3. Keine technischen Details: Keine SQL, keine Tabellenstruktur im Interface
  4. Immutable Inputs: Nur IMPORTING, keine CHANGING

Repository Implementation

Die Implementation kapselt alle Datenbankzugriffe und uebersetzt zwischen Domaenenobjekten und Datenbankstrukturen.

CLASS zcl_order_repository DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES zif_order_repository.
ALIASES:
find_by_id FOR zif_order_repository~find_by_id,
find_all FOR zif_order_repository~find_all,
find_by_customer FOR zif_order_repository~find_by_customer,
find_by_status FOR zif_order_repository~find_by_status,
get_items FOR zif_order_repository~get_items,
save FOR zif_order_repository~save,
delete FOR zif_order_repository~delete,
exists FOR zif_order_repository~exists.
PRIVATE SECTION.
TYPES:
BEGIN OF ty_order_db,
order_uuid TYPE sysuuid_x16,
customer_uuid TYPE sysuuid_x16,
order_date TYPE dats,
order_status TYPE char2,
total_amount TYPE p LENGTH 16 DECIMALS 2,
currency_code TYPE waerk,
created_by TYPE syuname,
created_at TYPE timestampl,
changed_by TYPE syuname,
changed_at TYPE timestampl,
END OF ty_order_db.
METHODS map_to_domain
IMPORTING
is_db_order TYPE ty_order_db
RETURNING
VALUE(rs_order) TYPE zif_order_repository=>ty_order.
METHODS map_to_db
IMPORTING
is_order TYPE zif_order_repository=>ty_order
RETURNING
VALUE(rs_db) TYPE ty_order_db.
ENDCLASS.
CLASS zcl_order_repository IMPLEMENTATION.
METHOD find_by_id.
DATA ls_db_order TYPE ty_order_db.
SELECT SINGLE *
FROM zorder_header
WHERE order_uuid = @iv_order_id
INTO @ls_db_order.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_not_found
EXPORTING
textid = zcx_not_found=>order_not_found
order_id = iv_order_id.
ENDIF.
rs_order = map_to_domain( ls_db_order ).
ENDMETHOD.
METHOD find_all.
DATA lt_db_orders TYPE STANDARD TABLE OF ty_order_db.
SELECT *
FROM zorder_header
ORDER BY order_date DESCENDING
INTO TABLE @lt_db_orders.
LOOP AT lt_db_orders INTO DATA(ls_db_order).
APPEND map_to_domain( ls_db_order ) TO rt_orders.
ENDLOOP.
ENDMETHOD.
METHOD find_by_customer.
DATA lt_db_orders TYPE STANDARD TABLE OF ty_order_db.
SELECT *
FROM zorder_header
WHERE customer_uuid = @iv_customer_id
ORDER BY order_date DESCENDING
INTO TABLE @lt_db_orders.
LOOP AT lt_db_orders INTO DATA(ls_db_order).
APPEND map_to_domain( ls_db_order ) TO rt_orders.
ENDLOOP.
ENDMETHOD.
METHOD find_by_status.
DATA lt_db_orders TYPE STANDARD TABLE OF ty_order_db.
SELECT *
FROM zorder_header
WHERE order_status = @iv_status
ORDER BY order_date DESCENDING
INTO TABLE @lt_db_orders.
LOOP AT lt_db_orders INTO DATA(ls_db_order).
APPEND map_to_domain( ls_db_order ) TO rt_orders.
ENDLOOP.
ENDMETHOD.
METHOD get_items.
SELECT item_uuid, order_uuid, product_uuid,
quantity, unit_price, currency_code
FROM zorder_item
WHERE order_uuid = @iv_order_id
INTO TABLE @rt_items.
ENDMETHOD.
METHOD save.
DATA ls_db_order TYPE ty_order_db.
ls_db_order = map_to_db( is_order ).
" Timestamp setzen
GET TIME STAMP FIELD DATA(lv_timestamp).
ls_db_order-changed_at = lv_timestamp.
ls_db_order-changed_by = cl_abap_context_info=>get_user_technical_name( ).
" Check ob neu oder Update
IF exists( is_order-order_id ).
" Update
UPDATE zorder_header
SET customer_uuid = @ls_db_order-customer_uuid,
order_date = @ls_db_order-order_date,
order_status = @ls_db_order-order_status,
total_amount = @ls_db_order-total_amount,
currency_code = @ls_db_order-currency_code,
changed_by = @ls_db_order-changed_by,
changed_at = @ls_db_order-changed_at
WHERE order_uuid = @is_order-order_id.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_save_error
EXPORTING
textid = zcx_save_error=>update_failed.
ENDIF.
ELSE.
" Insert
ls_db_order-created_at = lv_timestamp.
ls_db_order-created_by = cl_abap_context_info=>get_user_technical_name( ).
INSERT zorder_header FROM @ls_db_order.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_save_error
EXPORTING
textid = zcx_save_error=>insert_failed.
ENDIF.
ENDIF.
" Items speichern falls vorhanden
IF it_items IS NOT INITIAL.
" Alte Items loeschen
DELETE FROM zorder_item WHERE order_uuid = @is_order-order_id.
" Neue Items einfuegen
INSERT zorder_item FROM TABLE @it_items.
ENDIF.
ENDMETHOD.
METHOD delete.
IF NOT exists( iv_order_id ).
RAISE EXCEPTION TYPE zcx_not_found
EXPORTING
textid = zcx_not_found=>order_not_found
order_id = iv_order_id.
ENDIF.
" Items zuerst loeschen
DELETE FROM zorder_item WHERE order_uuid = @iv_order_id.
" Dann Header
DELETE FROM zorder_header WHERE order_uuid = @iv_order_id.
ENDMETHOD.
METHOD exists.
SELECT COUNT(*)
FROM zorder_header
WHERE order_uuid = @iv_order_id
INTO @DATA(lv_count).
rv_exists = xsdbool( lv_count > 0 ).
ENDMETHOD.
METHOD map_to_domain.
rs_order = VALUE #(
order_id = is_db_order-order_uuid
customer_id = is_db_order-customer_uuid
order_date = is_db_order-order_date
status = is_db_order-order_status
total_amount = is_db_order-total_amount
currency = is_db_order-currency_code
).
ENDMETHOD.
METHOD map_to_db.
rs_db = VALUE #(
order_uuid = is_order-order_id
customer_uuid = is_order-customer_id
order_date = is_order-order_date
order_status = is_order-status
total_amount = is_order-total_amount
currency_code = is_order-currency
).
ENDMETHOD.
ENDCLASS.

Unit of Work Pattern

Das Unit of Work Pattern sammelt alle Aenderungen und schreibt sie gebuendelt in einer Transaktion. Es verhindert inkonsistente Zwischenzustaende.

INTERFACE zif_unit_of_work PUBLIC.
METHODS register_new
IMPORTING
iv_entity_type TYPE string
is_entity TYPE any.
METHODS register_dirty
IMPORTING
iv_entity_type TYPE string
is_entity TYPE any.
METHODS register_deleted
IMPORTING
iv_entity_type TYPE string
is_entity TYPE any.
METHODS commit
RAISING
zcx_uow_error.
METHODS rollback.
ENDINTERFACE.
CLASS zcl_unit_of_work DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES zif_unit_of_work.
ALIASES:
register_new FOR zif_unit_of_work~register_new,
register_dirty FOR zif_unit_of_work~register_dirty,
register_deleted FOR zif_unit_of_work~register_deleted,
commit FOR zif_unit_of_work~commit,
rollback FOR zif_unit_of_work~rollback.
METHODS constructor
IMPORTING
io_order_repository TYPE REF TO zif_order_repository.
PRIVATE SECTION.
DATA mo_order_repository TYPE REF TO zif_order_repository.
DATA mt_new_orders TYPE zif_order_repository=>tt_orders.
DATA mt_dirty_orders TYPE zif_order_repository=>tt_orders.
DATA mt_deleted_orders TYPE zif_order_repository=>tt_orders.
METHODS clear_state.
ENDCLASS.
CLASS zcl_unit_of_work IMPLEMENTATION.
METHOD constructor.
mo_order_repository = io_order_repository.
ENDMETHOD.
METHOD register_new.
CASE iv_entity_type.
WHEN 'ORDER'.
APPEND INITIAL LINE TO mt_new_orders ASSIGNING FIELD-SYMBOL(<ls_order>).
<ls_order> = is_entity.
WHEN OTHERS.
" Weitere Entity-Typen hinzufuegen
ENDCASE.
ENDMETHOD.
METHOD register_dirty.
CASE iv_entity_type.
WHEN 'ORDER'.
APPEND INITIAL LINE TO mt_dirty_orders ASSIGNING FIELD-SYMBOL(<ls_order>).
<ls_order> = is_entity.
WHEN OTHERS.
" Weitere Entity-Typen hinzufuegen
ENDCASE.
ENDMETHOD.
METHOD register_deleted.
CASE iv_entity_type.
WHEN 'ORDER'.
APPEND INITIAL LINE TO mt_deleted_orders ASSIGNING FIELD-SYMBOL(<ls_order>).
<ls_order> = is_entity.
WHEN OTHERS.
" Weitere Entity-Typen hinzufuegen
ENDCASE.
ENDMETHOD.
METHOD commit.
TRY.
" Neue Entities speichern
LOOP AT mt_new_orders INTO DATA(ls_new_order).
mo_order_repository->save( ls_new_order ).
ENDLOOP.
" Geaenderte Entities speichern
LOOP AT mt_dirty_orders INTO DATA(ls_dirty_order).
mo_order_repository->save( ls_dirty_order ).
ENDLOOP.
" Geloeschte Entities entfernen
LOOP AT mt_deleted_orders INTO DATA(ls_deleted_order).
mo_order_repository->delete( ls_deleted_order-order_id ).
ENDLOOP.
" Zustand zuruecksetzen
clear_state( ).
CATCH zcx_save_error zcx_not_found INTO DATA(lx_error).
rollback( ).
RAISE EXCEPTION TYPE zcx_uow_error
EXPORTING
previous = lx_error.
ENDTRY.
ENDMETHOD.
METHOD rollback.
clear_state( ).
ENDMETHOD.
METHOD clear_state.
CLEAR: mt_new_orders,
mt_dirty_orders,
mt_deleted_orders.
ENDMETHOD.
ENDCLASS.

Verwendung mit Unit of Work

METHOD create_order_with_items.
" Unit of Work erstellen
DATA(lo_uow) = NEW zcl_unit_of_work( mo_order_repository ).
" Neue Order anlegen
DATA(ls_order) = VALUE zif_order_repository=>ty_order(
order_id = cl_system_uuid=>create_uuid_x16_static( )
customer_id = iv_customer_id
order_date = cl_abap_context_info=>get_system_date( )
status = 'NE' " Neu
total_amount = 0
currency = 'EUR'
).
lo_uow->register_new( iv_entity_type = 'ORDER'
is_entity = ls_order ).
" Items hinzufuegen
LOOP AT it_items INTO DATA(ls_item).
ls_order-total_amount = ls_order-total_amount +
( ls_item-quantity * ls_item-unit_price ).
ENDLOOP.
" Order mit Total aktualisieren
lo_uow->register_dirty( iv_entity_type = 'ORDER'
is_entity = ls_order ).
" Alles in einer Transaktion speichern
TRY.
lo_uow->commit( ).
CATCH zcx_uow_error INTO DATA(lx_error).
" Fehlerbehandlung
RAISE EXCEPTION TYPE zcx_order_creation_failed
EXPORTING
previous = lx_error.
ENDTRY.
ENDMETHOD.

Mock-Repositories fuer Tests

Mock-Repositories simulieren die Datenzugriffsschicht und ermoeglichen Unit-Tests ohne Datenbank.

CLASS lcl_mock_order_repository DEFINITION FOR TESTING.
PUBLIC SECTION.
INTERFACES zif_order_repository.
" Test-Helper
METHODS add_test_order
IMPORTING
is_order TYPE zif_order_repository=>ty_order.
METHODS get_saved_orders
RETURNING
VALUE(rt_orders) TYPE zif_order_repository=>tt_orders.
METHODS clear.
PRIVATE SECTION.
DATA mt_orders TYPE zif_order_repository=>tt_orders.
DATA mt_saved_orders TYPE zif_order_repository=>tt_orders.
ENDCLASS.
CLASS lcl_mock_order_repository IMPLEMENTATION.
METHOD zif_order_repository~find_by_id.
READ TABLE mt_orders INTO rs_order
WITH KEY order_id = iv_order_id.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_not_found.
ENDIF.
ENDMETHOD.
METHOD zif_order_repository~find_all.
rt_orders = mt_orders.
ENDMETHOD.
METHOD zif_order_repository~find_by_customer.
rt_orders = VALUE #(
FOR ls_order IN mt_orders
WHERE ( customer_id = iv_customer_id )
( ls_order )
).
ENDMETHOD.
METHOD zif_order_repository~find_by_status.
rt_orders = VALUE #(
FOR ls_order IN mt_orders
WHERE ( status = iv_status )
( ls_order )
).
ENDMETHOD.
METHOD zif_order_repository~get_items.
" Vereinfacht: Keine Items im Mock
rt_items = VALUE #( ).
ENDMETHOD.
METHOD zif_order_repository~save.
" Zur Liste der gespeicherten Orders hinzufuegen
APPEND is_order TO mt_saved_orders.
" In-Memory aktualisieren
READ TABLE mt_orders ASSIGNING FIELD-SYMBOL(<ls_order>)
WITH KEY order_id = is_order-order_id.
IF sy-subrc = 0.
<ls_order> = is_order.
ELSE.
APPEND is_order TO mt_orders.
ENDIF.
ENDMETHOD.
METHOD zif_order_repository~delete.
DELETE mt_orders WHERE order_id = iv_order_id.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_not_found.
ENDIF.
ENDMETHOD.
METHOD zif_order_repository~exists.
rv_exists = xsdbool( line_exists( mt_orders[ order_id = iv_order_id ] ) ).
ENDMETHOD.
METHOD add_test_order.
APPEND is_order TO mt_orders.
ENDMETHOD.
METHOD get_saved_orders.
rt_orders = mt_saved_orders.
ENDMETHOD.
METHOD clear.
CLEAR: mt_orders, mt_saved_orders.
ENDMETHOD.
ENDCLASS.

Unit-Test mit Mock-Repository

CLASS ltcl_order_service DEFINITION FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA mo_mock_repository TYPE REF TO lcl_mock_order_repository.
DATA mo_order_service TYPE REF TO zcl_order_service.
METHODS setup.
METHODS teardown.
METHODS test_create_order FOR TESTING.
METHODS test_cancel_order FOR TESTING.
METHODS test_cancel_nonexistent FOR TESTING.
ENDCLASS.
CLASS ltcl_order_service IMPLEMENTATION.
METHOD setup.
" Mock-Repository erstellen
mo_mock_repository = NEW lcl_mock_order_repository( ).
" Service mit Mock injizieren
mo_order_service = NEW zcl_order_service(
io_order_repository = mo_mock_repository
).
ENDMETHOD.
METHOD teardown.
mo_mock_repository->clear( ).
ENDMETHOD.
METHOD test_create_order.
" Given
DATA(lv_customer_id) = cl_system_uuid=>create_uuid_x16_static( ).
" When
DATA(ls_order) = mo_order_service->create_order(
iv_customer_id = lv_customer_id
iv_currency = 'EUR'
).
" Then
cl_abap_unit_assert=>assert_not_initial(
act = ls_order-order_id
msg = 'Order ID sollte gesetzt sein'
).
cl_abap_unit_assert=>assert_equals(
act = ls_order-status
exp = 'NE'
msg = 'Status sollte NE (Neu) sein'
).
" Pruefen ob gespeichert wurde
DATA(lt_saved) = mo_mock_repository->get_saved_orders( ).
cl_abap_unit_assert=>assert_equals(
act = lines( lt_saved )
exp = 1
msg = 'Eine Order sollte gespeichert worden sein'
).
ENDMETHOD.
METHOD test_cancel_order.
" Given
DATA(ls_existing_order) = VALUE zif_order_repository=>ty_order(
order_id = cl_system_uuid=>create_uuid_x16_static( )
customer_id = cl_system_uuid=>create_uuid_x16_static( )
order_date = cl_abap_context_info=>get_system_date( )
status = 'NE'
total_amount = 100
currency = 'EUR'
).
mo_mock_repository->add_test_order( ls_existing_order ).
" When
mo_order_service->cancel_order( ls_existing_order-order_id ).
" Then
DATA(lt_saved) = mo_mock_repository->get_saved_orders( ).
cl_abap_unit_assert=>assert_equals(
act = lt_saved[ 1 ]-status
exp = 'CA'
msg = 'Status sollte CA (Cancelled) sein'
).
ENDMETHOD.
METHOD test_cancel_nonexistent.
" Given
DATA(lv_nonexistent_id) = cl_system_uuid=>create_uuid_x16_static( ).
" When / Then
TRY.
mo_order_service->cancel_order( lv_nonexistent_id ).
cl_abap_unit_assert=>fail( msg = 'Exception erwartet' ).
CATCH zcx_not_found.
" Erwartet
ENDTRY.
ENDMETHOD.
ENDCLASS.

Integration mit RAP Unmanaged Scenario

Das Repository Pattern integriert sich nahtlos mit RAP Unmanaged Save, wo die Persistenz manuell implementiert wird.

Behavior Handler mit Repository

CLASS lhc_order DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
DATA mo_repository TYPE REF TO zif_order_repository.
METHODS get_repository
RETURNING
VALUE(ro_repository) TYPE REF TO zif_order_repository.
METHODS create FOR MODIFY
IMPORTING entities FOR CREATE order.
METHODS update FOR MODIFY
IMPORTING entities FOR UPDATE order.
METHODS delete FOR MODIFY
IMPORTING keys FOR DELETE order.
METHODS read FOR READ
IMPORTING keys FOR READ order RESULT result.
ENDCLASS.
CLASS lhc_order IMPLEMENTATION.
METHOD get_repository.
IF mo_repository IS NOT BOUND.
mo_repository = NEW zcl_order_repository( ).
ENDIF.
ro_repository = mo_repository.
ENDMETHOD.
METHOD create.
DATA(lo_repository) = get_repository( ).
LOOP AT entities ASSIGNING FIELD-SYMBOL(<ls_entity>).
DATA(ls_order) = VALUE zif_order_repository=>ty_order(
order_id = <ls_entity>-orderid
customer_id = <ls_entity>-customerid
order_date = <ls_entity>-orderdate
status = <ls_entity>-status
total_amount = <ls_entity>-totalamount
currency = <ls_entity>-currency
).
TRY.
lo_repository->save( ls_order ).
CATCH zcx_save_error INTO DATA(lx_error).
APPEND VALUE #( %cid = <ls_entity>-%cid ) TO failed-order.
APPEND VALUE #( %cid = <ls_entity>-%cid
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = lx_error->get_text( ) )
) TO reported-order.
ENDTRY.
ENDLOOP.
ENDMETHOD.
METHOD update.
DATA(lo_repository) = get_repository( ).
LOOP AT entities ASSIGNING FIELD-SYMBOL(<ls_entity>).
TRY.
DATA(ls_order) = lo_repository->find_by_id( <ls_entity>-orderid ).
" Nur geaenderte Felder uebernehmen
IF <ls_entity>-%control-customerid = if_abap_behv=>mk-on.
ls_order-customer_id = <ls_entity>-customerid.
ENDIF.
IF <ls_entity>-%control-status = if_abap_behv=>mk-on.
ls_order-status = <ls_entity>-status.
ENDIF.
IF <ls_entity>-%control-totalamount = if_abap_behv=>mk-on.
ls_order-total_amount = <ls_entity>-totalamount.
ENDIF.
lo_repository->save( ls_order ).
CATCH zcx_not_found zcx_save_error INTO DATA(lx_error).
APPEND VALUE #( orderid = <ls_entity>-orderid ) TO failed-order.
APPEND VALUE #( orderid = <ls_entity>-orderid
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = lx_error->get_text( ) )
) TO reported-order.
ENDTRY.
ENDLOOP.
ENDMETHOD.
METHOD delete.
DATA(lo_repository) = get_repository( ).
LOOP AT keys ASSIGNING FIELD-SYMBOL(<ls_key>).
TRY.
lo_repository->delete( <ls_key>-orderid ).
CATCH zcx_not_found INTO DATA(lx_error).
APPEND VALUE #( orderid = <ls_key>-orderid ) TO failed-order.
APPEND VALUE #( orderid = <ls_key>-orderid
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = lx_error->get_text( ) )
) TO reported-order.
ENDTRY.
ENDLOOP.
ENDMETHOD.
METHOD read.
DATA(lo_repository) = get_repository( ).
LOOP AT keys ASSIGNING FIELD-SYMBOL(<ls_key>).
TRY.
DATA(ls_order) = lo_repository->find_by_id( <ls_key>-orderid ).
APPEND VALUE #(
orderid = ls_order-order_id
customerid = ls_order-customer_id
orderdate = ls_order-order_date
status = ls_order-status
totalamount = ls_order-total_amount
currency = ls_order-currency
) TO result.
CATCH zcx_not_found.
APPEND VALUE #( orderid = <ls_key>-orderid ) TO failed-order.
ENDTRY.
ENDLOOP.
ENDMETHOD.
ENDCLASS.

Best Practices

1. Ein Repository pro Aggregate

" Gut: Repository fuer Order-Aggregate (inkl. Items)
INTERFACE zif_order_repository.
METHODS find_by_id RETURNING VALUE(ro_order) TYPE REF TO zcl_order.
ENDINTERFACE.
" Schlecht: Separate Repositories fuer eng gekoppelte Entities
INTERFACE zif_order_item_repository. " Nicht empfohlen
ENDINTERFACE.

2. Query-Methoden nach Bedarf

" Fuege nur Methoden hinzu, die wirklich benoetigt werden
find_by_id " Fast immer
find_by_customer " Wenn Kundenansicht benoetigt
find_pending " Spezifische Geschaeftsanforderung

3. Pagination bei grossen Datenmengen

METHODS find_all
IMPORTING
iv_offset TYPE i DEFAULT 0
iv_limit TYPE i DEFAULT 100
RETURNING
VALUE(rt_result) TYPE ty_paginated_result.

4. Factory fuer Repository-Erstellung

CLASS zcl_repository_factory DEFINITION PUBLIC FINAL.
PUBLIC SECTION.
CLASS-METHODS get_order_repository
RETURNING
VALUE(ro_repository) TYPE REF TO zif_order_repository.
ENDCLASS.
CLASS zcl_repository_factory IMPLEMENTATION.
METHOD get_order_repository.
" Hier kann zwischen DB, Mock, Cache etc. gewechselt werden
ro_repository = NEW zcl_order_repository( ).
ENDMETHOD.
ENDCLASS.

Zusammenfassung

AspektEmpfehlung
InterfaceDomaenensprache, keine SQL-Details
ImplementationMapping zwischen Domaene und DB
Unit of WorkFuer transaktionale Konsistenz
MockIn-Memory fuer Unit-Tests
RAP-IntegrationIm Behavior Handler via DI

Das Repository Pattern erfordert mehr Initialaufwand, zahlt sich aber bei komplexen Domaenen durch bessere Testbarkeit, Wartbarkeit und Flexibilitaet aus.

Weiterführende Artikel