DJ Adams

Modelling contained-in relationships with compositions in CDS

A short study of the features of CDS for modelling classic contained-in relationships, with a focus on the details, and a lean towards anonymous aspects.

Background

There's a classic structure often found representing business data in enterprise software, and that's the "document". Purchase requisition, sales order, goods receipt, and so on. Header and items. Most of the time the items don't make sense existing on their own, independent of their header parent. Such documents are typically modelled using contained-in relationships.

There's a great section of the Capire documentation, specifically on compositions, on which I want to expand here.

Explicit modelling

Given a hammer, all problems look like nails. Given the "entity" concept, all business data structures look like they should be modelled using entities.

Which does work. To wit:

service S {
  entity Orders {
    key ID    : Integer;
        Items : Composition of many OrderItems
                  on Items.parent = $self;
  }

  entity OrderItems {
    key parent : Association to Orders;
    key pos    : Integer;
        qty    : Integer;
  }
}

When compiled (with cds compile --to yaml service.cds1), this results in the following schema notation (in a YAML representation as I find it cleaner and easier to read)2:

definitions:
  S:
    kind: service
  S.Orders:
    kind: entity
    elements:
      ID:
        key: true
        type: cds.Integer
      Items:
        type: cds.Composition
        cardinality:
          max: '*'
        target: S.OrderItems
        'on':
          - ref:
              - Items
              - parent
          - '='
          - ref:
              - $self
  S.OrderItems:
    kind: entity
    elements:
      parent:
        key: true
        type: cds.Association
        target: S.Orders
        keys:
          - ref:
              - ID
      pos:
        key: true
        type: cds.Integer
      qty:
        type: cds.Integer

The resulting CSN encompasses the modelling intent - zero or more item children, and a relationship constraint that links back to the parent.

But the definitions at the CDS model level are quite explicit and arguably a little "noisy":

Remember, domain modelling is the common language for business domain experts and developers, and the CDS model is where they meet. So machinery is to be avoided where possible.

Implicit modelling

Such a classic parent-child structure can be expressed in the CDS model a lot more succinctly, using aspects, specifically anonymous aspects.

The exact same model can be expressed thus:

service S {
  entity Orders {
    key ID    : Integer;
        Items : Composition of many {
                  key pos : Integer;
                      qty : Integer;
                }
  }

}

That's it - that's the entire equivalent declaration. In using an anonymous aspect (that's the part expressed as the { ... } block) to describe the composition target, we get to NOT have to come up with and define:

I would go so far as to say that staring at this structure expression conjures up far more readily what a document (such as those examples mentioned above) "looks like" mentally.

And guess what? The equivalent schema notation is logically the same. Here's what compiling this "quieter" and more compact version to the YAML flavour of CSN gives us2:

definitions:
  S:
    kind: service
  S.Orders:
    kind: entity
    elements:
      ID:
        key: true
        type: cds.Integer
      Items:
        type: cds.Composition
        cardinality:
          max: '*'
        targetAspect:
          elements:
            pos:
              key: true
              type: cds.Integer
            qty:
              type: cds.Integer
        target: S.Orders.Items
        'on':
          - ref:
              - Items
              - up_
          - '='
          - ref:
              - $self
  S.Orders.Items:
    kind: entity
    elements:
      up_:
        key: true
        type: cds.Association
        cardinality:
          min: 1
          max: 1
        target: S.Orders
        keys:
          - ref:
              - ID
        notNull: true
      pos:
        key: true
        type: cds.Integer
      qty:
        type: cds.Integer

There are some technical differences, which are worth pointing out (there's always an opportunity to learn):

From a naming convention perspective, the name of the generated target entity is created using the parent entity as a prefix (S.Orders) to the composition element's name (Items), forming a scoped name S.Orders.Items. And I would wager that it's unlikely that we as humans would use an underscore-suffixed name for an element, so up_ is a good choice for the automatically generated pointer-to-parent element name.

Hybrid

There is a middle ground, where named, rather than anonymous aspects can be used. You can read more about this in the named targets section of the Domain Modelling topic of Capire.

Wrapping up

The CDS modelling language (and CDL specifically) is for humans. CSN is for machines. The distinction is not always top of mind, but if we try to keep it there, we'll end up with cleaner, simpler model definitions that are easier to reason about, easier to extend and reuse, and easier to maintain.

Footnotes

  1. I piped the resulting YAML through yq -y . to pretty-print it, as I find the long(er) lines of the raw YAML output a little tough to digest.

  2. I removed the metadata from the compiled YAML output:

    meta:
      creator: CDS Compiler v6.4.2
      flavor: inferred
    $version: 2

    just to keep things a little more compact.