Skip to content

Domain Keys — the Business Identity of a Root Entity

Overview and Motivation

Every PDO has a technical identity: the object id (plus its serial version counter). But no user ever says "invoice 4711002"; the business identifies its objects by natural keys — a user by their username, a country by its ISO code, a stored resource bundle by (name, locale). Tentackle makes this business identity a first-class concept: the unique domain key (UDK).

A domain key is declared in the model, woven into the generated interfaces, and then used pervasively by the framework: it is the toString() of a root entity, the duplicate check before saving, the value a user types into a text field bound to a PDO, a cache index, and the natural lookup for things like logins.

Domain keys belong to root entities only: identity is a property of the aggregate, and components are identified through their root (see rootId / rootClassId).

Declaring a Domain Key: the key Option

An attribute becomes part of the domain key via the key attribute option in the model. If the key consists of more than one attribute, each one gets the option — here the real model of StoredBundle, whose identity is (name, locale):

## attributes
[root, remote, tracked, tableserial]

String(128)    name            bname              the resource bundle name [key]
String(8)      locale          blocale            the locale, null if default [key, mapnull]

## indexes
unique index udk := bname, blocale

Things to know:

  • key does not create a database constraint. It declares the semantic identity; the unique index that enforces it must be declared explicitly in the ## indexes section, as above. (The framework offers a friendly duplicate check on top, but the database index is the hard guarantee.)
  • The key attributes are collected including inherited attributes and — less obviously — the attributes of the aggregate's components (recursively, stopping at sub-roots and embedded entities). A root's identity may thus include a component attribute.
  • Embedded attributes cannot be domain keys — the model rejects the combination.
  • A subclass that simply inherits its super entity's domain key gets nothing regenerated — the inherited key is its key.

What Gets Generated

Three wurblets plus one on the persistence side turn the key option into API:

Methods annotates the getter of each key attribute with @DomainKey — the runtime-retained marker a reader encounters in generated code — and declares the type-safe unique select (suppress with --noudk):

@Persistent(ordinal=1, comment="the resource bundle name")
@NotNull
@DomainKey
String getName();

@Persistent(ordinal=2, comment="the locale, null if default")
@DomainKey
String getLocale();

StoredBundle selectByUniqueDomainKey(String name, String locale);

UniqueDomainKey generates the key type — but only for multi-member keys. A single-member key needs no wrapper: the attribute's type (e.g. String) is the key type. For two or more members, a nested record is woven into the PDO interface:

record StoredBundleUDK(String name, String locale)
       implements Serializable, Comparable<StoredBundleUDK> {

  public StoredBundleUDK(StoredBundle pdo) {
    this(pdo.getName(), pdo.getLocale());
  }

  @Override
  public int compareTo(StoredBundleUDK other) {
    int rv = Compare.compare(name, other.name);
    if (rv == 0) {
      rv = Compare.compare(locale, other.locale);
    }
    return rv;
  }
}

A record is the natural fit: value equality, an ordering that compares member by member (null-safe via Compare.compare), and Serializable — so a domain key travels over TRIP like any other value.

DomainMethods implements the runtime API in the domain interface: isUniqueDomainKeyProvided() returning true, getUniqueDomainKeyType(), getUniqueDomainKey() / setUniqueDomainKey() mapping to the attribute getters/setters, and findByUniqueDomainKey() delegating to selectByUniqueDomainKey(...).

PdoSelectUnique generates the implementation of selectByUniqueDomainKey(...) into the persistence layer — a remote-aware unique select like any other generated query method:

// @wurblet selectByUniqueDomainKey PdoSelectUnique name locale

The Runtime API

The generic access to the domain key lives on DomainObject<T>, i.e. on every PDO:

Method Meaning
isUniqueDomainKeyProvided() Does this entity define a domain key? The guard for all methods below.
getUniqueDomainKeyType() The key's type: the attribute type, or the generated …UDK record.
getUniqueDomainKey() The key value — the attribute value, or a new record snapshot of the members.
setUniqueDomainKey(Object) Applies a key value to the PDO through its setters (used to preset new objects).
findByUniqueDomainKey(Object) Loads the PDO with that business identity, null if none.

The defaults in AbstractDomainObject assert that the PDO is a root entity and throw a DomainException — so generic code must check isUniqueDomainKeyProvided() (which defaults to false) before touching the rest.

Because the methods are typed Object, they are the reflective entry — framework and generic code use them; application code normally calls the type-safe selectByUniqueDomainKey(...) directly.

Alongside the API, PdoUtilities.getMembers(...) exposes the @DomainKey annotation as PdoMember.isDomainKey() — the per-attribute reflection view UI frameworks build on.

findDuplicate(): the duplicate check

PersistentObject.findDuplicate() asks: would saving this object violate the business identity? The default implementation runs findByUniqueDomainKey(getUniqueDomainKey()) and returns the found object if it is a different one. The Rich Desktop Client invokes it before saving and can tell the user which object already claims the key — a far better message than the constraint-violation exception the unique index would raise.

How the Framework Uses Domain Keys

  • toString(). The PDO proxy routes toString() to the domain delegate, and there a root entity's string representation is its domain key (components fall back to their singular name) — which is why log output and debugger views show business identities, not ids. The persistence delegate's toString(), in contrast, returns the technical toGenericString() (class plus id/serial).

Binding a text field to a PDO

In the Rich Desktop Client, a text component can be bound directly to a PDO-typed attribute — say, a User field on a form. The PdoStringTranslator makes that work via the domain key:

  • to view: the PDO is rendered as its domain key (using the value translator registered for the key type, so formatting rules apply).
  • to model: the typed string is converted to a key and resolved via findByUniqueDomainKey. If no such PDO exists, the translator opens the entity's interactive finder, presetting the search criteria from the (possibly incomplete) input — via setUniqueDomainKey, or a static lenientValueOf(String) factory the key type may optionally provide.

So "type the key, get the object — or a search dialog" is default behavior, not something the application has to build. Related conveniences: the default PDO editor gives initial focus to the first changeable domain-key field, and creating a new PDO from the finder presets it with the domain key the user searched for.

Other consumers

  • Caching. The --udk option of the PdoCache wurblet adds a cache index keyed by the domain key, so selectByUniqueDomainKey-style lookups are served from the cache without touching the database.
  • Login. The archetype's server looks up the User by its unique domain key (the username) to authenticate a session — see the walkthrough.
  • I18n. StoredBundle resolves remote resource bundles by their (name, locale) domain key.