06 · Defining Entities & Types

Declare entities with key fields, built-in scalar types, virtual fields, and computed elements.

Lesson 6 — Defining Entities & Types

What you'll learn

Every way to declare fields in CDS — scalars, virtual elements, calculated fields, custom types, and how to define reusable types that keep your schema consistent.


Entities in depth

An entity in CDS maps to a database table. Every entity must have at least one key field.

Compound keys (multi-field primary key)

entity OrderItems {
  key order  : Association to Orders;
  key lineNo : Integer;
      product: String(50);
      qty    : Integer;
}

Avoid compound keys where possible. UUID keys are simpler, and CAP handles them automatically.

Elements with defaults

entity Products {
  key ID     : UUID;
      status : String(20) default 'active';
      stock  : Integer    default 0;
      taxRate: Decimal    default 0.20;
}

Nullable vs not null

entity Person {
  key ID       : UUID;
      name     : String not null;  // required
      nickname : String;           // nullable (default)
      age      : Integer;          // nullable
}

Virtual elements

Virtual elements exist in the service layer but are not persisted to the database. They are computed or populated by handlers at runtime.

entity Books {
  key ID      : UUID;
      title   : String(100);
      price   : Decimal(10,2);
      stock   : Integer;

      // Virtual — computed in a handler, not stored in DB
      virtual discount     : Decimal(10,2);
      virtual isAvailable  : Boolean;
}

In your handler, populate them:

this.after('READ', 'Books', books => {
  for (const book of books) {
    book.discount    = book.price > 30 ? book.price * 0.1 : 0;
    book.isAvailable = book.stock > 0;
  }
});

Calculated elements (stored)

Calculated elements are persisted — the value is computed from other fields and stored in the DB:

entity OrderItems {
  key ID       : UUID;
      quantity : Integer;
      unitPrice: Decimal(10,2);

      // Calculated: stored in DB, recomputed on INSERT/UPDATE
      totalPrice = quantity * unitPrice : Decimal(10,2);
}

Calculated vs Virtual: Calculated = stored in DB, computed from other fields. Virtual = not in DB, computed in handler at read time.


Custom types

Define reusable types to keep your schema consistent:

// db/types.cds
namespace com.bookshop.types;

type BookTitle  : String(100);
type Price      : Decimal(10, 2);
type ISBN       : String(13);
type Status     : String(20) enum {
  active    = 'active';
  inactive  = 'inactive';
  archived  = 'archived';
};

Use them in entities:

using { com.bookshop.types } from './types';

entity Books {
  key ID    : UUID;
      title : types.BookTitle;
      price : types.Price;
      isbn  : types.ISBN;
      status: types.Status default 'active';
}

Enum types

Enums enforce that a field can only have specific values:

type Priority : Integer enum {
  low    = 1;
  medium = 2;
  high   = 3;
  urgent = 4;
}

entity Tasks {
  key ID      : UUID;
      title   : String(200);
      priority: Priority default 2;
}

CAP validates enum values on INSERT and UPDATE automatically. Trying to set priority = 99 will get rejected.


Structured types

Structured types let you define a record shape and reuse it:

type Address {
  street  : String(100);
  city    : String(50);
  country : String(50);
  zip     : String(10);
}

entity Customers {
  key ID              : UUID;
      name            : String(100);
      billingAddress  : Address;
      shippingAddress : Address;
}

In HANA: Structured types are stored as a JSON column. In SQLite, CAP flattens them: billingAddress_street, billingAddress_city, etc.


Array types

entity Products {
  key ID   : UUID;
      tags : array of String(50);
}

Arrays are stored as JSON in both SQLite and HANA. They are useful for lightweight tagging, but not for data you need to filter or join on. For those cases, use a separate entity with an association.


Annotating for validation

entity Users {
  key ID    : UUID;

      @mandatory
      email : String(254);

      @assert.range: [18, 120]
      age   : Integer;

      @assert.format: '^[A-Z]{2}[0-9]{6}$'
      passport: String(8);
}
Annotation What it does
@mandatory Field must be provided on CREATE
@assert.range Number must be in [min, max]
@assert.format String must match regex
@assert.unique Value must be unique across all rows

Best practices

Use UUID keys everywhere — they work offline, don't require a sequence, and are safe for distributed systems.

Define shared types in a types.cds file — then use them across all entities for consistency.

Avoid over-normalizing — if a piece of data is always read with its parent and never queried independently, consider structured types instead of a separate entity.

Use enums instead of magic strings — Status.active is clearer than 'active' and CAP validates it.


Checkpoint ✓

In Lesson 7 we build on this to define proper service definitions — controlling what operations are allowed and how entities are projected.

Sign in for progress tracking & Q&A →