Skip to content

Entities and their validation

01es edited this page Sep 30, 2021 · 11 revisions

Entities and integrity constraints

In TG, the concept of "entity" represents a pair of types – "entity type" and "entity companion type" – where "entity type" defines the structure and the validation rules of an entity, and "entity companion type" defines the CRUD operations for an entity. In day-to-day conversations we use the word "entity" to refer to a specific "entity type" and use the word "companion" to refer to a specific "entity companion type". For example, we may say "entity Person" to refer to type Person (a Java class extending AbstractEntity) and say "Person companion" to refer to type PersonCo (a Java interface extending IEntityDao) or its implementation PersonDao.

The main subject of this article is the "integrity constraints" for entities, which are achieved by employing the mechanism of entity validation. If you're coming from the database background, the notion of "integrity constraints" would be familiar to you (e.g. you may think of foreign key constraints). In the conceptual modelling of information systems, which underpin the modelling approach in TG, integrity constraints refer to any rules that define an entity instance as a "valid state" of an information system. Hence, the terms "validation" and "checking integrity constraints", and terms "validator" and "integrity constraints" are synonymous.

Properties and validation

Entity structure is defined by entity properties (class fields, defined in a special way). Entity instances can be mutated by changing the values for their properties. For example, entity Person may have property name: String and a specific instance of this entity could have that property changed by calling a corresponding setter – person.setName("John Doe").

The central responsibility for each entity is not to permit its mutation that could lead to an invalid state (i.e. violation of its integrity constraints). This is achieved by defining "before change event handlers" (BCE handlers) for properties, which are also known as "property validators". There are 2 kinds of property validators – implicit (cannot be determined by inspecting an entity definition) and explicit (can be determined by inspecting an entity definition). Property validators follow a specific rule to determine the order of their execution. If some validator fails, no other validators with lower priority get executed.

Implicit validation

At the time of writing, there are only 2 implicit integrity constraints – property requiredness, which ensures that a property must have a value (i.e. it cannot be null for an entity to be valid), and value existence. Property requiredness constraint can be applied dynamically during the program execution (i.e. implicitly) or it can be defined explicitly as part of a property definition (more on that further). The requiredness constraint has the highest precedence - if it fails, no other integrity constraint is checked.

A remark. The intuition for precedence of integrity constraints is that more basic, usually simpler, constraints should have higher precedence. Following this intuition, it should be easy to see why requiredness has the highest precedence – it is the most basic, no other constraint makes sense if requiredness is not satisfied. The same intuition should be followed when identifying domain-specific constraints for your entities.

Any value that is not of some entity type is considered to "exist" (e.g. values of types String, Integer, Date, Money). An entity-typed value "exists" only if it represent a persisted and unchanged entity instance. This integrity constraint is represented by validator EntityExistsValidator. This validator is associated with every property that is of an entity type implicitly. For example, entity Person may have property station: Station defined as:

    @IsProperty
    @Title(value = "Station", desc = "A station where the person works.")
    @MapTo
    private Station station;

Property type Station is an entity in its own right. In order to assign values to this property, which are instances of type Station, those values must be persisted and unchanged (i.e. not to have any non-persisted changes). Any attempt to set a non-persisted or mutated/changed instance of Station to property Person.station would not pass the implicit integrity constraint, implemented by EntityExistsValidator.

The "not passing" means that the attempted value will not be assigned to a property and the property's current value would remain unchanged. This notion is relevant for all validators – implicit and explicit. Property validators are the chief mechanism for defining integrity constraints for entities and for ensuring those integrity constraints are upheld.

The implicit EntityExistValidator can be controlled and even completely switched off by means of annotation @SkipEntityExistsValidation. There are valid and powerful uses for this, which would need to be addressed separately.

For entity-typed properties, EntityExistValidator has the 3rd highest precedence – only "requiredness" and "final" (discussed further) integrity constraint are above, if present.

Explicit validation

Explicit integrity constraints can be expressed with specialised annotations for properties (and in some cases for setters, but this approach is being phased out). In the order of their precedence, these annotations are : @Required, @Final, @BeforeChange, and @Unique.

Annotations @Required, @Final, @Unique stand for very specific integrity constraints, which have only one meaning and cannot be changed. Annotation @BeforeChange exists as a way to define domain integrity constraints that implement domain-specific business logic. Let's review these integrity constraints and their uses.

@Required

The simplest standard explicit integrity constraint for a property is the property requiredness. Unlike its implicit counterpart, this constraint needs to be expressed explicitly using annotation @Required. For example, we could redefine property Person.station as:

    @IsProperty
    @Title(value = "Station", desc = "A station where the person works.")
    @Required
    @MapTo
    private Station station;

Explicitly defined requiredness cannot be turned off. If dynamic requiredness, which would be determined as part of some business logic, is needed then implicit requiredness is more suitable. More on that will be discussed further as part of the discussion on meta-properties.

The requiredness integrity constraint always has the highest precedence over all other constraints.

@Final

The next in precedence is the "final" integrity constraint, which can be defined for a property with annotation @Final. The purpose of this constraint is not to permit property mutation after it was assigned a non null value and successfully persisted.

Annotation @Final has parameter persistentOnly, defaulted to true. Defining a property as @Final(persistentOnly = false) would ensure that such property can ever have its value assigned just once, making it immutable. In practice, having persistentOnly = true is a more practical equivalent, where immutability is attained immediately after the value is persisted into the database.

@BeforeChange

Domain-specific integrity constraints are expressed as BCE handlers, which need to be specified as part of a property definition using annotation @BeforeChange. This annotation can accept one or more BCE handlers using annotation @Handler. For example, consider the following definition for property Person.name:

    @IsProperty(length = 32)
    @Title(value = "Initials", desc = "Person's initials, must represent the person uniquely.")
    @BeforeChange(@Handler(MaxLengthValidator.class))
    @MapTo
    private String name;

In this example, there is only one BCE handler MaxLengthValidator, which is one of the standard validators to ensure that the length of values assigned to Person.name is not longer than 32 characters defined by @IsProperty(length = 32).

The precedence of BCE handlers is determined by their order in @BeforeChange. Here is another example, which demonstrates the use of several explicit validators and which also has a subtle deficiency that should be improved.

    @IsProperty(length = 32)
    @Title(value = "Initials", desc = "Person's initials, must represent the person uniquely.")
    @BeforeChange({@Handler(NameExclusionValidator.class),
                   @Handler(MaxLengthValidator.class)})
    @Required
    @MapTo
    private String name;

Let's analyse this definition. It has 3 explicit integrity constraints. In the order of their precedence, they are the "requiredness" (expressed with @Required), "special excluded names are not permitted" (expressed with @Handler(NameExclusionValidator.class) as the first BCE handler), and "max length is not greater than 32" (expressed with @Handler(MaxLengthValidator.class) as the second BCE handler).

Two observations worth noting. First, the order in which @Required and @BeforeChange are defined for a property have no impact on the precedence of the integrity constraints they represent. Although @Required is defined after @BeforeChange, requiredness will be validated first.

Another observation has to do with the order of BCE handlers. Should NameExclusionValidator be checked before MaxLengthValidator? Following the intuition, we established earlier, it would appear that MaxLengthValidator should take precedence over NameExclusionValidator as a more basic integrity constraint. If value exceeds the max length then it surely cannot be among the excluded names, and checking a value against excluded could be a relatively expensive operation that requires a database request, etc. By this reasoning, it would make sense to redefine property Person.name by specifying @Handler(MaxLengthValidator.class) first:

    @IsProperty(length = 32)
    @Title(value = "Initials", desc = "Person's initials, must represent the person uniquely.")
    @Required
    @BeforeChange({@Handler(MaxLengthValidator.class),
                   @Handler(NameExclusionValidator.class)})
    @MapTo
    private String name;

Please note that @Required was also moved above @BeforeChange. The only reason for this is to improve code comprehension – to reflect in the structure that requiredness is checked before BCE handlers.

@Unique

The uniqueness constraint is only applicable to persistent entities and has the lowest precedence – it is checked only after all other property constraints are satisfied. The same constraint could be expressed as a custom domain-specific validator, but having a standard annotation like @Unique is more generic and more pronounced (i.e. it is easy to spot the presence of @Unique).

Another thing worth noting about @Unique is that null values never violate this constraint. In other words, there can be multiple entity instances persisted with null values for properties defined as @Unique.

Combining implicit and explicit constraints (example)

Let's now consider an entity-typed property Person.station defined as:

    @IsProperty
    @Title(value = "Station", desc = "A station where the person works.")
    @Required
    @BeforeChange({@Handler(NoLessThan3PersonsPerStation.class), 
                   @Handler(NoMoreThan10PersonsPerStation.class)})
    @MapTo
    private Station station;

It has 3 explicit integrity constraints expressed with @Required and @BeforeChange({@Handler(NoLessThan3PersonsPerStation.class), @Handler(NoMoreThan10PersonsPerStation.class)}). But due to the fact that this is an entity-typed property, it also has an implicit constraint "entity exists" -- 4 integrity constraints all up. Based on what we should know by now, the precedence of these constraints are:

  1. Requiredness - if an attempt to set null is made, this constraint fails and the next constraint is not checked.
  2. Entity existence - if an attempted station is not persisted or modified, this constraint fails and the next constraint is not checked.
  3. Restriction on the min number of people per station - if property has some station assigned already and assigning another station reduces the number of people assigned to the current station below 3, this constraint fails and the next constraint is not checked.
  4. Restriction on the max number of people per station - if an attempted station already has more than 10 people, this constraint fails and values is not assigned to the property; otherwise, the value is assigned.
Clone this wiki locally