Skip to content

Lesson 9 Companion — Working with Custom Persistence ​

This companion expands Lesson 9 — Working with Custom Persistence.

Today's purpose is low-level persistence literacy.

The architecture is temporarily:

text
ABAP class
   ↓
ABAP SQL
   ↓
custom table

The course will deliberately leave this architecture behind when RAP begins.


1. Database Tables as ABAP types ​

A Dictionary Database Table can type an ABAP structure:

abap
DATA issue TYPE zissue_###.

or internal table:

abap
DATA issues TYPE STANDARD TABLE OF zissue_###
  WITH EMPTY KEY.

This is useful because application code does not need to redefine the row shape manually.


2. UUID generation ​

The guided code uses:

abap
cl_system_uuid=>create_uuid_x16_static( )

which matches the SYSUUID_X16 key type.

Handle:

text
CX_UUID_ERROR

explicitly.

This same API appears in current SAP examples.


3. Why X16? ​

SYSUUID_X16 represents a 16-byte RAW UUID.

That format also aligns with later RAP managed UUID numbering.

Lesson 9 generates the key manually so students can see what RAP will later automate.


4. INSERT semantics ​

Guided statement:

abap
INSERT zresv_### FROM @reservation.

It attempts to create a new row.

If a duplicate primary key is inserted without a duplicate-handling addition, a database exception can be raised.

The lesson avoids this by generating a fresh UUID for every run.


5. sy-dbcnt ​

For the simple guided operations:

text
INSERT one row
UPDATE one row
DELETE one row

expected sy-dbcnt is:

text
1

A value of:

text
0

during a targeted update/delete usually means the condition matched no row.

That is diagnostically useful.


6. sy-subrc ​

The course uses sy-subrc mainly to verify the final SELECT SINGLE.

General rule:

text
check a system field immediately after the statement whose result it describes

Do not execute unrelated statements first.


7. Client handling ​

Because the table begins with:

abap
key client : abap.clnt not null;

normal ABAP SQL uses implicit client handling.

That is why the exercise does not:

text
fill client manually
add WHERE client = @sy-mandt

This behavior is separate from authorization.


8. Why admin fields remain initial ​

The table was designed for future RAP.

But today's write class is not RAP.

So fields such as:

text
local_created_at
local_last_changed_at

do not become meaningful automatically just because their types are RAP reuse Data Elements.

The RAP provider will later manage them when the corresponding BO semantics/behavior are in place.


9. Safe UPDATE design ​

Prefer a precise condition:

abap
UPDATE zresv_###
  SET status = 'C'
  WHERE uuid = @uuid.

Before running a write statement, state in plain language:

Which rows can this modify?

If the answer is not precise, do not run it yet.


10. Safe DELETE design ​

Training cleanup:

abap
DELETE FROM zresv_###
  WHERE uuid = @uuid.

Do not use an unrestricted delete as a shortcut.

The Practice reinforces this by keeping two rows alive at the same time.


11. Why verify after writes? ​

A strong backend workflow is:

text
write
  ↓
inspect result count
  ↓
read back
  ↓
confirm intended state

That habit is useful later with RAP responses as well.


12. Why no MODIFY statement in the guided path? ​

An ABAP SQL MODIFY can combine insert/update-like behavior.

That is useful in some scenarios but weakens today's conceptual separation:

text
INSERT = create
UPDATE = change
DELETE = remove

The lesson wants those operations to remain visually distinct before RAP introduces standard BO operations.


13. Why no explicit COMMIT WORK lesson? ​

The exercise does all changes and cleanup within one class run.

Its purpose is not manual LUW design.

Explicit transaction control deserves careful context because later RAP uses its own transaction model and:

text
COMMIT ENTITIES

becomes the meaningful BO save boundary.

The course avoids teaching a "commit after every DML" habit.


14. Guided class reference ​

abap
CLASS zcl_resv_sql_### DEFINITION
  PUBLIC
  FINAL
  CREATE PUBLIC.

  PUBLIC SECTION.
    INTERFACES if_oo_adt_classrun.

ENDCLASS.


CLASS zcl_resv_sql_### IMPLEMENTATION.

  METHOD if_oo_adt_classrun~main.

    DATA uuid TYPE sysuuid_x16.

    TRY.
        uuid = cl_system_uuid=>create_uuid_x16_static( ).
      CATCH cx_uuid_error INTO DATA(uuid_error).
        out->write( uuid_error->get_text( ) ).
        RETURN.
    ENDTRY.

    DATA(reservation) = VALUE zresv_###(
      uuid             = uuid
      reservation_no   = '00000001'
      customer_name    = 'Ada Martin'
      customer_email   = 'ada@example.com'
      workshop_code    = 'ABAP01'
      reservation_date = sy-datum
      status           = 'N'
      fee              = '125.00'
      currency_code    = 'EUR'
      notes            = 'Lesson 9 SQL exercise'
    ).

    INSERT zresv_### FROM @reservation.
    DATA(inserted_rows) = sy-dbcnt.

    out->write( |Inserted rows: { inserted_rows }| ).

    SELECT SINGLE FROM zresv_###
      FIELDS uuid,
             reservation_no,
             customer_name,
             workshop_code,
             status,
             fee,
             currency_code
      WHERE uuid = @uuid
      INTO @DATA(created_reservation).

    out->write( 'After INSERT' ).
    out->write( created_reservation ).

    UPDATE zresv_###
      SET status = 'C'
      WHERE uuid = @uuid.

    DATA(updated_rows) = sy-dbcnt.

    out->write( |Updated rows: { updated_rows }| ).

    SELECT SINGLE FROM zresv_###
      FIELDS uuid,
             reservation_no,
             customer_name,
             workshop_code,
             status
      WHERE uuid = @uuid
      INTO @DATA(updated_reservation).

    out->write( 'After UPDATE' ).
    out->write( updated_reservation ).

    DELETE FROM zresv_###
      WHERE uuid = @uuid.

    DATA(deleted_rows) = sy-dbcnt.

    out->write( |Deleted rows: { deleted_rows }| ).

    SELECT SINGLE FROM zresv_###
      FIELDS uuid
      WHERE uuid = @uuid
      INTO @DATA(deleted_check).

    IF sy-subrc <> 0.
      out->write( 'Cleanup verified: row no longer exists.' ).
    ELSE.
      out->write( 'Cleanup check failed.' ).
    ENDIF.

  ENDMETHOD.

ENDCLASS.

15. Practice strategy: use two rows ​

The Practice intentionally uses two Support Issues.

Why?

Because with only one row, this statement:

abap
UPDATE zissue_###
  SET status = 'I'
  WHERE uuid = @uuid_a.

proves that A changed.

With two rows, students can additionally prove:

text
B did not change

That makes WHERE targeting concrete.


16. Practice reference data ​

Suggested Issue A:

text
IssueNo:        00000001
Title:          Cannot publish service
Priority:       H
Status:         O
ReportedOn:     current date
EstimatedCost:  75.00
Currency:       EUR

Suggested Issue B:

text
IssueNo:        00000002
Title:          Data preview mismatch
Priority:       M
Status:         O
ReportedOn:     current date
EstimatedCost:  40.00
Currency:       EUR

17. Full Practice reference solution ​

abap
CLASS zcl_issue_sql_### DEFINITION
  PUBLIC
  FINAL
  CREATE PUBLIC.

  PUBLIC SECTION.
    INTERFACES if_oo_adt_classrun.

ENDCLASS.


CLASS zcl_issue_sql_### IMPLEMENTATION.

  METHOD if_oo_adt_classrun~main.

    DATA uuid_a TYPE sysuuid_x16.
    DATA uuid_b TYPE sysuuid_x16.

    TRY.
        uuid_a = cl_system_uuid=>create_uuid_x16_static( ).
        uuid_b = cl_system_uuid=>create_uuid_x16_static( ).
      CATCH cx_uuid_error INTO DATA(uuid_error).
        out->write( uuid_error->get_text( ) ).
        RETURN.
    ENDTRY.

    DATA(issue_a) = VALUE zissue_###(
      uuid            = uuid_a
      issue_no        = '00000001'
      title           = 'Cannot publish service'
      requester_email = 'student@example.com'
      priority        = 'H'
      status          = 'O'
      reported_on     = sy-datum
      estimated_cost  = '75.00'
      currency_code   = 'EUR'
    ).

    DATA(issue_b) = VALUE zissue_###(
      uuid            = uuid_b
      issue_no        = '00000002'
      title           = 'Data preview mismatch'
      requester_email = 'student@example.com'
      priority        = 'M'
      status          = 'O'
      reported_on     = sy-datum
      estimated_cost  = '40.00'
      currency_code   = 'EUR'
    ).

    INSERT zissue_### FROM @issue_a.
    DATA(inserted_a) = sy-dbcnt.

    INSERT zissue_### FROM @issue_b.
    DATA(inserted_b) = sy-dbcnt.

    out->write( |Inserted A: { inserted_a }| ).
    out->write( |Inserted B: { inserted_b }| ).

    SELECT FROM zissue_###
      FIELDS uuid,
             issue_no,
             title,
             priority,
             status
      WHERE uuid = @uuid_a
         OR uuid = @uuid_b
      ORDER BY issue_no
      INTO TABLE @DATA(created_issues).

    out->write( 'After INSERT' ).
    out->write( created_issues ).

    UPDATE zissue_###
      SET status = 'I'
      WHERE uuid = @uuid_a.

    DATA(updated_a) = sy-dbcnt.

    out->write( |Updated A: { updated_a }| ).

    SELECT SINGLE FROM zissue_###
      FIELDS issue_no,
             status
      WHERE uuid = @uuid_a
      INTO @DATA(check_a).

    SELECT SINGLE FROM zissue_###
      FIELDS issue_no,
             status
      WHERE uuid = @uuid_b
      INTO @DATA(check_b).

    out->write( 'A after targeted UPDATE' ).
    out->write( check_a ).

    out->write( 'B should still be Open' ).
    out->write( check_b ).

    DELETE FROM zissue_###
      WHERE uuid = @uuid_a.

    DATA(deleted_a) = sy-dbcnt.
    out->write( |Deleted A: { deleted_a }| ).

    SELECT SINGLE FROM zissue_###
      FIELDS issue_no,
             status
      WHERE uuid = @uuid_b
      INTO @DATA(b_still_exists).

    IF sy-subrc = 0.
      out->write( 'B still exists after deleting A.' ).
      out->write( b_still_exists ).
    ENDIF.

    DELETE FROM zissue_###
      WHERE uuid = @uuid_b.

    DATA(deleted_b) = sy-dbcnt.
    out->write( |Cleanup B: { deleted_b }| ).

  ENDMETHOD.

ENDCLASS.

18. What the Practice proves ​

The student should be able to demonstrate:

text
two independent keys
two successful inserts
targeted update affects one row
other row remains unchanged
targeted delete removes one row
other row still exists
final cleanup removes remaining row

That is stronger evidence of safe DML understanding than a single-row script.


19. Optional zero-row diagnostic ​

An advanced optional check:

  1. generate a UUID that was never inserted;
  2. run a targeted UPDATE with it;
  3. inspect sy-dbcnt.

Expected:

text
0

This teaches that zero processed rows can be a meaningful diagnostic result rather than a syntax error.


20. Direct SQL vs RAP ​

Once the table is RAP persistence, direct application writes can bypass BO rules.

From the RAP part of the course onward:

text
direct DML
=
understanding / special infrastructure cases

EML / RAP service
=
normal BO consumer path

That boundary should be explicit before students begin RAP.


21. Troubleshooting ​

CX_UUID_ERROR ​

Handle it and stop the run.

Do not continue with an initial key.

INSERT raises duplicate-key database error ​

Check:

text
fresh UUID?
same UUID assigned twice?

UPDATE row count is 0 ​

Check:

text
correct UUID variable?
row still exists?
current client?

DELETE removes 0 rows ​

Check whether the row was already deleted.

Practice leaves data behind ​

Use Data Preview to identify only your rows, then write a precise cleanup condition.

Do not use unrestricted DELETE.


22. Current SAP references ​

Relevant current SAP topics:

text
ABAP Keyword Documentation
  → INSERT
  → UPDATE
  → DELETE
  → SELECT
  → system fields

ABAP Cloud / RAP Help
  → client handling
  → UUID examples

Basic ABAP Programming
  → database access
  → Business Objects / EML transition

The guided APIs and write patterns are compatible with SAP BTP ABAP Trial.