diff --git a/java/fiori-drafts.md b/java/fiori-drafts.md index 618d4d773f..70f909c671 100644 --- a/java/fiori-drafts.md +++ b/java/fiori-drafts.md @@ -100,6 +100,48 @@ The `DRAFT_CREATE` is an internal event that is not triggered by OData requests For more examples, see the [Bookshop sample application](https://github.com/SAP-samples/cloud-cap-samples-java/tree/master/srv/src/main/java/my/bookshop/handlers/AdminServiceHandler.java). +## Deleting Drafts { #draft-deletion-in-cap-java } + +When a draft is deleted — whether by user cancellation, during activation, or by garbage collection — CAP Java removes the draft rows from the database. The deletion strategy is controlled via: + +```yaml +cds.drafts.deleteStrategy: flat +``` + +Three strategies are available: + +| Strategy | Behaviour | Performance | +|---|---|---| +| `flat` *(default)* | Deletes each draft table's rows individually by draft UUID. | Fast | +| `deep` | Issues a delete on the draft root; the runtime [cascades it along compositions](./working-with-cql/query-api#delete), removing child rows without explicit per-table statements. | Slow | +| `dad` | Deletes only the `DraftAdministrativeData` record and relies on database `ON DELETE CASCADE` constraints to remove the associated draft rows. | Fastest | + +### Cascade Deletion on Database { #draft-deletion-dad } + +The `dad` strategy avoids separate deletion operations for each draft table. Before enabling it, the database must have `ON DELETE CASCADE` constraints on each draft table's `DraftAdministrativeData_DraftUUID` foreign key. Generate these constraints by enabling: + +```yaml +cds.fiori.draft_admin_integrity: true +``` + +On SAP HANA and PostgreSQL, the constraints are deployed with `NOT VALIDATED` / `NOT VALID`, so existing data is not checked and no orphan cleanup is required before deploying. + +Once the constraints are deployed, switch to the `dad` strategy: + +```yaml +cds.drafts.deleteStrategy: dad +``` + +::: warning Check Custom Handlers +Switching strategies changes which event handlers are called during draft deletion. Verify the behavior of your custom handlers before adopting `dad`. +::: + +::: tip Retain `flat` Until Migration Is Complete +Keep `cds.drafts.deleteStrategy: flat` on existing databases until the cascade constraints have been deployed. +::: + +[Learn more about the shared migration requirements for CAP Node.js and CAP Java.](../releases/2026/oct26#optimized-draft-deletion){.learn-more} + ## Activating Drafts When you finish editing drafts by pressing the *Save* button, a draft gets activated. That means, either a single `CREATE` or `UPDATE` event is triggered to create or update the active entity with all of its compositions through a deeply structured document. You can register to these events to validate the activated data. @@ -116,6 +158,27 @@ public void validateOrderItem(CdsCreateEventContext context, OrderItems orderIte During activation the draft data is deleted from the database. This happens before the active entity is created or updated within the same transaction. In case the create or update operation raises an error, the transaction is rolled back and the draft data is restored. +### Delta Save Mode { #draft-delta-save-mode } + +By default, draft activation sends the full document tree to the `CREATE` or `UPDATE` handler (`saveMode: full`). With delta save mode enabled, CAP Java performs a deep comparison of the draft against the active image and issues update operations only for entities that were actually changed — entities on an unchanged path are skipped entirely, resulting in more efficient database operations for large documents. + +Enable delta save mode via: + +```yaml +cds.drafts.saveMode: delta +``` + +Compared to the default `full` mode, the observable impact is: + +- **Update handlers** receive only the [delta representation](../working-with-cql/query-api#deep-update-delta) — the changed subtree — rather than the full document. Nested composition collections are delivered as delta lists. +- **Managed timestamps** (`modifiedAt`, `createdAt`) are updated only for nodes on the changed path, accurately reflecting which parts of the document were touched. + +::: warning Check Custom Update Handlers +Make sure your `UPDATE` handlers can deal with a delta payload. Handlers that assume they always receive the full document need to be adapted. Retain `cds.drafts.saveMode: full` (the default) if your handlers require the complete document. +::: + +[Learn more about processing delta payloads in a handler.](../working-with-cql/query-api#deep-update-delta-processing){.learn-more} + ## Read-Only Fields in Drafts { #readonly-in-drafts } By default, `@readonly` and `@Core.Computed` fields are only enforced when a draft is activated, that means during the `CREATE` or `UPDATE` event on the active entity. Until then, such fields can still be changed on the draft, for example through an OData `PATCH` request. @@ -212,7 +275,7 @@ cds.security.authorization.draftProtection.enabled: false ## Draft Garbage Collection { #draft-gc } -Stale drafts are automatically deleted after a timeout (30 days default). You can configure the timeout with the following application configuration property: +Stale drafts are automatically deleted after a timeout (30 days default). The [deletion strategy](#draft-deletion-in-cap-java) configured via `cds.drafts.deleteStrategy` also applies during garbage collection. You can configure the timeout with the following application configuration property: ```yaml cds.drafts.deletionTimeout: 8w diff --git a/java/working-with-cql/query-api.md b/java/working-with-cql/query-api.md index b9472fe81a..175d168209 100644 --- a/java/working-with-cql/query-api.md +++ b/java/working-with-cql/query-api.md @@ -1340,6 +1340,8 @@ See the result of the updated *Order*: In **delta** representation, nested entity collections in the update data can be partial: the runtime only processes entities that are contained in the collection but entities that aren't contained remain untouched. Entities that shall be removed need to be included in the list and explicitly _marked for removal_. +##### Creating a Delta Representation + Using the same sample _Order_ as in the previous full-set chapter, do a deep delta update with the following update data: ```java @@ -1359,10 +1361,47 @@ order.setItems(delta(item1, item2.forRemoval(), item4)); Update.entity(ORDER).data(order); ``` -> Create delta collections via `CdsList` and `CdsData`. +> Create delta collections via `CdsList.delta()` and `CdsData.forRemoval()`. The deep update with order items in delta representation has similar effects as the update with items in full set representation. The only difference is that `OrderItem 3` is not deleted. +##### Processing a Delta Representation in a Custom Handler { #deep-update-delta-processing} + +Custom handlers registered on the `UPDATE` event may receive a delta payload — either from an explicit `CdsList.delta()` update statement, or from draft activation when [`cds.drafts.saveMode: delta`](../fiori-drafts#draft-delta-save-mode) is enabled. Such a handler needs to inspect the nested collections to distinguish delta lists from full-set lists, and to tell apart entries to be removed from entries to be created or updated. + +Two API methods are relevant: + +- [**`CdsList.isDelta()`**](https://javadoc.io/doc/com.sap.cds/cds4j-api/latest/com/sap/cds/CdsList.html#isDelta()) — Returns `true` if a nested collection is a delta list. To call it, cast the collection to `CdsList`. If the list is not a delta list, it is a full-set list and all pre-existing items not contained in it will be deleted. + +- [**`CdsData.isForRemoval()`**](https://javadoc.io/doc/com.sap.cds/cds4j-api/latest/com/sap/cds/CdsData.html#isForRemoval()) — Returns `true` if an individual entry in a delta list is marked for removal. Entries not marked for removal are to be created or updated. + +The following example shows how to handle both cases in a `@Before` handler: + +```java +@Before +public void beforeUpdateOrder(CdsUpdateEventContext context, List orders) { + for (Orders order : orders) { + List items = order.getItems(); + if (items instanceof CdsList cdsList && cdsList.isDelta()) { + // delta list — only the changed subset is present + for (OrderItem item : items) { + if (item.isForRemoval()) { + // item is marked for removal + } else { + // item is to be created or updated + } + } + } else if (items != null) { + // full-set list — pre-existing items not in this list will be deleted + } + } +} +``` + +::: warning Adapt handlers when using delta save mode +Handlers that assume the full document is always present must be adapted before enabling `cds.drafts.saveMode: delta`. Retain `cds.drafts.saveMode: full` (the default) if your handlers require the complete document. +::: + ### Bulk Update: Update Multiple Entity Records with Individual Data {#bulk-update}