-
Notifications
You must be signed in to change notification settings - Fork 7
Entities and their validation
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.
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.
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 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.
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.
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.
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.
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
.
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:
- Requiredness - if an attempt to set
null
is made, this constraint fails and the next constraint is not checked. - Entity existence - if an attempted station is not persisted or modified, this constraint fails and the next constraint is not checked.
- 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.
- 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.
Per aspera ad astra
- Web UI Design and Web API
- Safe Communication and User Authentication
- Gitworkflow
- JavaScript: Testing with Maven
- Java Application Profiling
-
TG Development Guidelines
- TLS and HAProxy for development
- TG Development Checklist
- Entities and their validation
- Entity Properties
- Entity Type Enhancement
- EQL
- Tooltip How To
- All about Matchers
- Streaming data
- Synthetic entities
- Activatable entities
- Jasper Reports
- Opening Compound Master from another Compound Master
- Window management test plan
- Multi Time Zone Environment
- GraphQL Web API
- Guice
- Maven
- Full Text Search
- Deployment recipes
- Application Configuration
- JRebel Installation and Integration
- Compile-time mechanisms