What this is about
Sometimes a thing comes in several kinds that have a lot in common, but not everything. A customer can be a private customer (with a date of birth) or a business customer (with a commercial register number). Both have a name, an address and orders.
That is what abstract models are for: a parent model with the shared fields and several concrete child models that inherit from it.
classDiagram
class kunde {
<<abstract>>
name
adresse
auftraege
}
class privatkunde {
geburtsdatum
}
class firmenkunde {
handelsregisternummer
}
kunde <|-- privatkunde
kunde <|-- firmenkunde
The type: @type
Every object carries its concrete type. In the database it is stored in the _MODELTYPE column; in JSON it is called @type. The value is the API path of the child model with dots instead of slashes:
| Child model | API path | @type |
|---|---|---|
| private customer | /crm/privatkunde | crm.privatkunde |
| business customer | /crm/firmenkunde | crm.firmenkunde |
The hub API: one entrance, several targets
For the abstract model, the generator creates a hub API with the usual paths, here /api/rest/crm/kunde/…. It does nothing itself; it forwards every request to the API of the right subtype. Each subtype also has its own normal API.
flowchart LR
C(["Client"]) --> H["Hub API<br/>/crm/kunde"]
H -->|"@type = crm.privatkunde"| P["API<br/>/crm/privatkunde"]
H -->|"@type = crm.firmenkunde"| F["API<br/>/crm/firmenkunde"]
C -.->|directly| P
C -.->|directly| F
Where the hub API gets the type from depends on the operation:
| Operation | Source of the type |
|---|---|
| POST /create | @type in the client's data |
| PUT /update/{id} | @type in the client's data |
| PATCH /update/{id} | @type in the client's data |
| POST /read/{id}, GET /read/{id} | the server looks up the stored type for the id |
| DELETE /delete/{id} | the server looks up the stored type for the id |
| POST /query | the stored type of each match, see search in two phases |
Every operation step by step
When: POST /crm/kunde/create
-
1Client→CDMSsends
{ "data": { "@type": "crm.privatkunde", "name": "Anna Muster", "geburtsdatum": "1990-04-01" }, "response": ["+"] } -
2CDMSreads
@typeand with it the private customer's payload, including its own fieldsWithout@type, CDMS does not know which subtype is meant: 400missing-type-for-abstract-field|data. -
3CDMSforwards the request to the API
/crm/privatkunde -
4CDMS→Databasecreates the object, with the permissions, rules and hooks of the private customer
Result: Response as for the subtype, with "@type": "crm.privatkunde".
When: /read/{id}, /delete/{id}
-
1Client→CDMSsends only the
id, no@typeneeded -
2CDMS→Databaselooks up the stored type for the
id -
3CDMSforwards the request to the API of that subtype
Result: When reading, the response contains @type so the client can tell the kinds apart.
When: PUT and PATCH /update/{id}
As for create, @type must be in data, for PATCH too. It must be the type the object already has: an object never changes its type.
When: /crm/privatkunde/…
You can also address every subtype directly through its own API. Then you need no @type, and the search returns only objects of that subtype. You need the hub API when you want to read or search all kinds together.
Searching across all subtypes: two phases
A search on the hub API has to return objects of different types in one list, correctly sorted and paged. That happens in two phases:
sequenceDiagram
participant C as Client
participant H as Hub API /crm/kunde
participant DB as Database
participant P as API privatkunde
participant F as API firmenkunde
C->>H: POST /query (filter, sorting, page)
Note over H,DB: Phase 1: only IDs and types
H->>DB: SELECT id, _MODELTYPE ... WHERE ... ORDER BY ... LIMIT
DB-->>H: [ (id1, private), (id2, business) ] + totalCount
Note over H,F: Phase 2: read each row through its subtype
par in parallel
H->>P: read id1
and
H->>F: read id2
end
P-->>H: object 1
F-->>H: object 2
H-->>C: data in the order of phase 1 + meta
-
1CDMSPhase 1 filters, sorts and pages on the parent model. It decides which objects come in which order, and returns
totalCount -
2CDMSPhase 2 reads each row through the API of its subtype, with that subtype's permissions and filtersThe read steps run in parallel, by default up to 8 at a time (setting
cdms_hub_query_parallelism). If a write transaction is already open in the same request, CDMS reads them one after another. -
3CDMSEach row gets its own copy of the field selection. So a
+means “all fields of the private customer” for a private customer and “all fields of the business customer” for a business customer -
4CDMS→Clientassembles the list in the order from phase 1Result: A mixed list, each object with its
@typeand its own fields.
Permissions and filters
Each subtype is a complete model with roles of its own. Whoever reads through the hub API therefore needs the read permissions of the subtype the object belongs to. Attribute filters of the parent model automatically apply to all subtypes as well: a subtype is never a way around the rules of the parent model.
In the database
The parent model has a table of its own with the shared fields and the _MODELTYPE column. Each subtype has its own table with its additional fields, linked through the same id.
Table kunde | Table privatkunde | |||
|---|---|---|---|---|
id | _MODELTYPE | name | id | geburtsdatum |
| 5a2b… | crm.privatkunde | Anna Muster | 5a2b… | 1990-04-01 |
| 7c1d… | crm.firmenkunde | Muster GmbH | – | – |
The company is stored with its own fields in the firmenkunde table.