Structural multi inheritance - fieldenms/tg GitHub Wiki

Introduction

This document describes the feature of structural multi-inheritance in TG.

The object model of the Java language is limited to single inheritance. A class may have at most one superclass.

In the context of TG, this means that an entity type may extend at most one other entity type, in the sense of Java's extends.

Single inheritance in Java presents a limitation, which in practice makes modelling with Synthetic Entities laborious in cases where more than one entity type is used for model construction.

Synthetic entities are underpinned by an EQL model. As with any kind of entities, properties of synthetic entities define their structure, and the EQL model yields data into this structure.

In the most general case, it is required to explicitly define all the necessary properties for a synthetic entity and to make sure that the underlying EQL model yields values for all those properties. Properties can be omitted in yields, but those that are yielded must be present.

If a synthetic entity type extends a persistent one (e.g. ReWorkActivity extends WorkActivity), the structure of the synthetic type inherits all properties from the persistent type. And so, there is no need to declare those properties explicitly, and EQL provides a convenient operation yieldAll(), which tells the EQL engine to yield all inherited properties. This is a simple example of automatic structure definition, where properties are inherited, and thus are present at the level of the Java class that defines the Synthetic Entity.

Structural multi-inheritance

To overcome the limitation of single inheritance in Java, TG provides an annotation processor that supports structural multi-inheritance. This kind of inheritance enables the combination of structures from multiple entity types into one. The resulting entity type, however, is not a subtype of those entity types -- only the structure is inherited.

The following running example is used:

class Building extends AbstractEntity<DynamicEntityKey> {
  @IsProperty
  @CompositeKeyMember(1)
  @Title("Building Number")
  @MapTo
  String number;

  @IsProperty
  @MapTo
  @Title(value = "Status", desc = "Status of the Building.")
  String status;

  @IsProperty
  @Title("Address")
  @MapTo
  String address;

  @IsProperty
  @MapTo
  Date constructionDate;

}

class Equipment extends AbstractEntity<DynamicEntityKey> {
  @IsProperty
  @CompositeKeyMember(1)
  @Title("Equipment Number")
  @MapTo
  String number;

  @IsProperty
  @MapTo
  @Title(value = "Status", desc = "Status of the Equipment.")
  EquipmentStatus status;
}

@EntityTitle(value = ReAsset_Spec.ENTITY_TITLE, desc = ReAsset_Spec.ENTITY_DESC)
@KeyTitle(value = ReAsset_Spec.ENTITY_TITLE, desc = ReAsset_Spec.ENTITY_DESC)
@KeyType(DynamicEntityType.class)
@Extends(name = "ReAsset",
         parentToken = AssetModuleToken.class,
         value = {
             @Entity(value = Equipment.class, exclude = {"status"}),
             @Entity(value = Building.class,  exclude = {"status", "constructionDate"})
         })
abstract class ReAsset_Spec extends ActivatableAbstractEntity<DynamicEntityKey> {

  public static final String ENTITY_TITLE = "Asset";
  public static final String ENTITY_DESC = "Assets belongs to one of the categories: Equipment or Building.";

  @AutoYield
  @IsProperty
  @Title("Asset Number")
  @CompositeKeyMember(1)
  String number;

  @IsProperty
  @MapTo
  @Title(value = "Status", desc = "Status of the Asset.")
  String status;

  private static <E extends AbstractEntity<?>> ISubsequentCompletedAndYielded<E>
  modelFor(final Class<? extends AbstractEntity<?>> type, final IFromAlias<E> source)
  {
    final ISubsequentCompletedAndYielded<E> part0;
    if (type == Building.class) {
      part0 = source.yield().prop(Building_.status()).as(ReAsset_.status());
    }
    else if (type == Equipment.class) {
      part0 = source.yield().prop(Equipment_.status().key()).as(ReAsset_.status());
    }
    else {
      throw new InvalidStateException();
    }
    return part0;
  }

}

@SpecifiedBy(ReAsset_Spec.class)
@CompanionIsGenerated
class ReAsset extends ReAsset_Spec {

  private static List<EntityResultQueryModel> models_;

  @IsProperty
  @Title("Address")
  String address;

}

ReAsset_Spec is a multi-inheritance specification entity type, or spec-entity for short. @Extends specifies the entity types whose structure should be inherited. The result of processing this declaration is a generated synthetic entity type ReAsset, whose name is determined by @Extends.name.

If the resultant synthetic entity is expected to have activatable nature, it is important for the spec entity to extend ActivatableAbstractEntity. If not all extended entities have activatable nature, it is required to declare property active explicitly and implement modelFor for yielding active for all extended entities (active would need to be excluded from inheritance for activatable entities).

As per the established practice, it also recommended to declare ENTITY_TITLE and ENTITY_DESC, and use them in annotations @KeyTitle and @EntityTile.

Property inheritance rules

As a prerequisite, it is important to understand the concept of property presence. Property p is present in entity type T iff it is declared by T or is present in the superclass of T.

For each entity type specified in @Extends, the following properties are inherited:

  • All persistent properties but for the exceptions described below.

  • These properties are never inherited: key, version, AbstractPersistentEntity.created* properties, AbstractPersistentEntity.lastUpdated* properties, refCount.

  • desc is inherited iff the spec-entity type has desc (i.e., @DescTitle is present or desc is declared explicitly).

  • Properties that are excluded with @Extends.Entity.exclude are not inherited.

All inherited properties are declared in the generated entity type, except for:

  • Properties present in the spec-entity type that are annotated with @AutoYield.

    This avoids declaring an equivalent property in the generated entity type, when it can be inherited from the spec-entity type via Java extends.

  • Property desc, which is conditionally inherited from the spec-entity type.

    Be aware of modelling scenarios where the spec-entity type has desc, but none of the extended types do (or desc is excluded for all of them). The resulting generated type will contain desc, but the generated EQL model will contain no yields for desc. In such a case, the developer should adjust the definition.

EQL model generation

Entity types generated by the processor are synthetic and thus require an EQL model. As can be seen from the definition of ReAsset, its EQL model is not statically assigned. Instead, it is generated at runtime, using the information from ReAsset_Spec. The resulting EQL model is a union of sub-models for entity types in @Extends. Therefore, the resulting EQL model has a rectangular shape.

If a property is inherited, it will be yielded in the generated EQL model. E.g., ReAsset.address is inherited from Building.address, but not from Equipment.address. The generated EQL model will contain the following:

select(Building.class)
  ...
  yield().prop("address").as().prop("address")
  ...

select(Equipment.class)
  ...
  yield().val(null).as().prop("address")
  ...

Since address is absent in Equipment, it has to be filled with a default value, which is null for all property types except for boolean, for which false is used.

Customisation of the EQL model

To provide control over the resulting EQL model, a spec-entity type may define the following static method:

static <E extends AbstractEntity<?>> ISubsequentCompletedAndYielded<E> modelFor(final Class<E> type, final IFromAlias<E> source)

This method will then be called during EQL model generation for each entity type in @Extends. E.g., for ReAsset_Spec this method will be called as modelFor(Building.class, select(Building.class)) and similary for Equipment.

A spec-entity type may declare its own properties, which should then be yielded in the modelFor method. E.g., ReAsset_Spec declares status : String to reconcile Building.status and Equipment.status that have different types, with corresponding yields in the modelFor method.

After modelFor has returned, the generation logic will proceed to yield inherited properties.

@AutoYield

Properties present in the spec-entity type and annotated with @AutoYield will be automatically yielded during EQL model generation.

E.g., ReAsset_Spec.number hides Building.number and Equipment.number, and the generated EQL model will contain the following:

select(Building.class)
  ...
  yield().prop("number").as().prop("number")
  ...

select(Equipment.class)
  ...
  yield().prop("number").as().prop("number")
  ...

If there was another extended entity, such as Vehicle, and it did not have number, then null would be yielded for its sub-model:

select(Vehicle.class)
  ...
  yield().val(null).as().prop("number")
  ...

It is an error if a property annotated with @AutoYield cannot be yielded from any of the types in @Extends (either due to exclusion or non-existence).

Companion and token generation

Companion object types for generated entity types, such as ReAsset, are generated at runtime, which is indicated by @CompanionIsGenerated. Therefore, ReAssetCo need not exist in source code.

Security tokens are also generated at runtime. Specifically, tokens READ and READ_MODEL are generated for generated entity types, such as ReAsset. The parent token type of generated tokens can be specified with @Extends.parentToken.

@EntityTypeCarrier property

A generated entity type will always declare an additional property annotated with @EntityTypeCarrier. The name of this property can be controlled with @Extends.entityTypeCarrierProperty.

The purpose of this property is to support navigation between Compound Masters of different entity types. During EQL model generation, a yield for this property will be inserted, which will use the type of a corresponding type in @Extends for the yielded value.

Recommended practices

  • Specification entity types should be declared abstract, since their sole purpose is to provide input to the generator -- they will not be considered "domain types", hence will not have corresponding companion objects.

    This will also have the desired effect of excluding specification entity types from being registered in the ApplicationDomain. If a specification entity type cannot be declared abstract, it should be annotated with @SkipEntityRegistration.

  • Specification entity types may have an arbitrary key type, depending on the nature and structure of their supertypes.

    The recommended practice is to declare an optional composite key member for each supertype, and specify corresponding yields for EQL sub-models. This will improve the user experience of using search criteria -- each key member could be used for autocompletion.

    However, if there is a natural candidate for a key that is shared by all extended entities, it is best to use that key for the synthetic entity. For example, if Building and Equipment are uniquely identified by their property number: String, then simply declare number as a composite key member in the spec with @AutoYield and use DynamicEntityKey as the key type parameter.

    In situations where the key has no meaning, the key type should be declared as NoKey.

    If there is a clear set of common properties in the extended entity types that could be used as a key, a composite key should be declared with the corresponding members defined explicitly, with the EQL submodels for each extended entity type yielding their values. For example, entity Equipment might have single composite key member name: String, while entity Vehicle has key member number: String. The spec-entity type could then declare a single composite key member number: String, and yield name as number in the EQL submodel for Equipment and number as number in the EQL submodel for Vehicle.

Configuration

The processor is packaged alongside other processors in platform-annotation-processors. To enable the processor, it must be specified in the pom.xml of the *-pojo-bl project:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.11.0</version>
    <configuration>
        <showWarnings>true</showWarnings>
        <!-- source output directory -->
        <generatedSourcesDirectory>${project.build.directory}/generated-sources/</generatedSourcesDirectory>
        <annotationProcessorPaths>
            <path>
                <groupId>fielden</groupId>
                <artifactId>platform-annotation-processors</artifactId>
                <version>${platform.version}</version>
                <classifier>uber</classifier>
            </path>
        </annotationProcessorPaths>
        <annotationProcessors>
            <annotationProcessor>ua.com.fielden.platform.processors.minheritance.MultiInheritanceProcessor</annotationProcessor>
            <!-- Other processors follow -->
        </annotationProcessors>
    </configuration>
</plugin>
⚠️ **GitHub.com Fallback** ⚠️