Skip to content

Synthetic entities

homedirectory edited this page Sep 13, 2024 · 13 revisions

Synthetic entities provide a way to implement the "informative function" of Information Systems and can aggregate data from other synthetic entities or persistent entities.

For simple data aggregration purposes, one can simply define a calculated property for a persistent entity. But, in more complex scenarios, a need for a synthetic entity arises.

A Synthetic Entity is based on a model

An essential part of a synthetic entity is its underlying model, i.e., an EQL query. It can be defined in 2 ways:

  1. As a single EQL query assigned to a static field model_.

    class TgAverageFuelUsage extends AbstractEntity<TgVehicle> {
    
      static final EntityResultQueryModel<TgAverageFuelUsage> model_ =
              select(TgVehicleFuelUsage.class).where().
                      prop("date").gt().iParam("datePeriod.from").and().
                      prop("date").lt().iParam("datePeriod.to").
                      groupBy().prop("vehicle").
                      yield().prop("vehicle").as("id").
                      yield().prop("vehicle").as("key").
                      yield().sumOf().prop("qty").as("qty").
                      yield().sumOf().prop("cost.amount").as("cost.amount").
                      modelAsEntity(TgAverageFuelUsage.class);
    
      @IsProperty
      private BigDecimal qty;
      
      @IsProperty
      @MapTo
      private Money cost;
    
      @IsProperty
      @CritOnly
      private Date datePeriod;
    }
  2. As a non-empty list of EQL queries assigned to a static field models_. The resulting queries form an implicit union. This way is provided as a convenient alternative to specifying a union by hand.

    class SynConditionAssessment extends AbstractEntity<DynamicEntityKey> {
      static final List<EntityResultQueryModel<SynConditionAssessment>> models_ = 
        List.of(modelForExternalAssessment(), modelForInternalAssessment());
    
      static EntityResultQueryModel<SynConditionAssessment> modelForExternalAssessment() {
         return select(WorkActivity.class).
                 where().prop(WorkActivity_.conditionAssessmentExternal()).isNotNull().
                 yield().prop(WorkActivity_.id()).as(SynConditionAssessment_.workActivity()).
                 yield().prop(WorkActivity_.conditionAssessmentExternal()).as(SynConditionAssessment_.conditionAssessmentExternal()).
                 yield().prop(WorkActivity_.conditionAssessmentExternal()).as(SynConditionAssessment_.conditionAssessment()).
                 yield().val(null).as(SynConditionAssessment_.conditionAssessmentInternal()).
                 yield().prop(WorkActivity_.conditionAssessmentExternalEntryDate()).as(SynConditionAssessment_.entryDate()).
                 modelAsEntity(SynConditionAssessment.class);
      }
    
      static EntityResultQueryModel<SynConditionAssessment> modelForInternalAssessment() {
         return select(WorkActivity.class).
                 where().prop(WorkActivity_.conditionAssessmentInternal()).isNotNull().
                 yield().prop(WorkActivity_.id()).as(SynConditionAssessment_.workActivity()).
                 yield().prop(WorkActivity_.conditionAssessmentInternal()).as(SynConditionAssessment_.conditionAssessmentInternal()).
                 yield().prop(WorkActivity_.conditionAssessmentInternal()).as(SynConditionAssessment_.conditionAssessment()).
                 yield().val(null).as(SynConditionAssessment_.conditionAssessmentExternal()).
                 yield().prop(WorkActivity_.conditionAssessmentInternalEntryDate()).as(SynConditionAssessment_.entryDate()).
                 modelAsEntity(SynConditionAssessment.class);
      }
    }

A synthetic entity's model is not meant to be used directly by the developer. Rather, it gets processed by the EQL engine when a synthetic entity is used as a query source.

Kinds of synthetic entities

A synthetic entity can be one of the following kinds:

  1. Based on a persistent entity, meaning that it extends a persistent entity type. Such a synthetic entity enriches the persistent one with properties that get calculated in the underlying model. It also inherits all properties of the persistent entity. Use cases include:

    • Enhanced searching capabilities. E.g., by an association with another entity.
    • Reporting. E.g., aggregating data from mutiple associatied entities and presenting results in the form of additional properties.

    Such entities will greatly benefit from a convenient yielding syntax in EQL - yieldAll(), which is equivalent to yielding all properties (both declared and inherited) of the query source (which happens to be a persistent entity that the synthetic one is based on) explicitly.

  2. One-to-one association (i.e., having a key typed with a persistent entity). A synthetic entity of this kind is associated with a particular persistent entity, similar to those that are based on a persistent type. They differ, however, in that one-to-one synthetic entities don't inherit properties of the persistent entity (because there is no subtyping relationship).

  3. Otherwise, it serves a basic purpose of data aggregation. A synthetic entity of this kind is useful when data needs to be pulled from several sources, and there is no single persistent entity type that is associated with the synthetic entity.

Entity IDs

In the most general case, synthetic entities do not have an id associated with them. This is only natural as aggregated data simply does not have a corresponding record in the database, which would have an id. However, there is one case where id can be derived and be used to provide additional functionality. Specifically, where a synthetic entity does not aggregate the data, but rather provides additional information on top of one or more persistent entity types. Depending on the kind of a synthetic entity:

  1. Based on a persistent entity - id is interpreted as a persistent property.
  2. One-to-one - id is interpreted as a calculated property with an expression that makes it equal to property key (the associated persistent entity).
  3. Otherwise - id is interpreted as a persistent property (this is likely to be changed in the future).

For persistent entities property id is fetched by default every time they are read from a database. Due to the intrinsic nature of synthetic entities fetching id by default is not possible. So, the platform tries to be smart where it can to dynamically identify cases where implicit fetching of id is possible. In other cases, the developer needs to be more explicit to ensure that id is fetched.

Before discussing these other cases, it is important to emphasise that yielding a property in an EQL model associated with a synthetic entity does not mean that property will be fetched. The fetch model controls what is actually retrieved, while yields in a query only define what is available for fetching. The following is a simplified model of property retrieval:

isRetrieved :: Property -> Boolean
isRetrieved = isYielded AND isFetched

Implicit fetching of id

There are two situations where property id is implicitly fetched for synthetic entity E:

  1. E is based on a persistent entity.
  2. E represents a one-to-one assocation.

In all other cases, id is not implicitly fetched, hence it needs to be added to a fetch model explicitly.

Manual transformations for id fetching

There may be many different situations where id values can be derived manually. This subsection describes two situations where the base model for a synthetic entity effectively represents a sum type (either one or another (or another...) type). Although, on the surface these situations are different, they boil down to the same transformation technique.

A good example for the first situation is from the TG Air domain - synthetic entity Manpower:

@KeyType(DynamicEntityKey.class)
class Manpower extends AbstractEntity<DynamicEntityKey> {
    @IsProperty
    @CompositeKeyMember(1)
    @Optional
    private Person person;

    @IsProperty
    @CompositeKeyMember(2)
    @Optional
    private Team team;

    static final EntityResultQueryModel<Manpower> model_ = 
      select(modelForPersonManpower(), modelForTeamManpower())
        .modelAsEntity(Manpower.class);

    static EntityResultQueryModel<Manpower> modelForPersonManpower() {
        final var nullTeamModel = select(Team.class).where().val(1).eq().val(0).model();

        return select(PersonManpower.class).
                yield().prop(PersonManpower_.id()).as(Manpower_.id()).
                yield().prop(PersonManpower_.person()).as(Manpower_.person()).
                yield().model(nullTeamModel).as(Manpower_.team()).
                modelAsEntity(Manpower.class);
    }

    static EntityResultQueryModel<Manpower> produceModelForTeamManpower() {
        final var nullPersonModel = select(Person.class).where().val(1).eq().val(0).model();
        
        return select(TeamManpower.class).
                yield().prop(TeamManpower_.id()).as(Manpower_.id()).
                yield().model(nullPersonModel).as(Manpower_.person()).
                yield().prop(TeamManpower_.team()).as(Manpower_.team()).
                modelAsEntity(Manpower.class);
    }
}

In this case, Manpower has a composite key with 2 members: Team and Person, and it is essential that both of them are optional. Each instance of Manpower could be based on either Team or Person, which are persistent and thus have id, making it possible to derive id for Manpower. However, this is a very specific case and the platform does not handle it at the moment. Hence, id has to be fetched explicitly.

This is where a transformation trick comes in handy. Here are the steps:

  1. Define property computedId: Long as part of a synthetic entity.
  2. Override getter getId() to return computedId.
  3. Yield both id and computedId as part of the underlying EQL model.

The above recipe will ensure that such synthetic entities can be correctly represented in the Entity Centre, selected entries can be exported to Excel and even the type-specific Entity Masters can be invoked for them.

Another situation, which boils down to the one above, is where several persistent entity types with a common base type are used for defining an EQL model that underpins a synthetic entity. A good example of this from the TG Air domain is entity EscCoc with an EQL model, which is based on four (4) sub-models. Each of those sub-models is based on a separate persistent entity type. And because of that, the values for id can be derived for each sub-model and by extension for each instance of EscCoc. This can be achieved by following the steps described above.

N.B. In order for id to be included as part of synthetic entities that are displayed on their corresponding Entity Centres, the centres need to be configured to include this. However, if adding this is inappropriate, then simply extending the default fetch model with id is the best way. For example:

// as part of Entity Centre configuration
.setFetchProvider(fetchNone(MyEntity.class).with("id"))
Clone this wiki locally