What this is about
A field hook is logic for a single value. It does not belong to a model but to a kind of value. That is why you can attach the same field hook to many fields in many models. Typical cases:
- store a text encrypted and deliver it decrypted,
- derive a value, such as
total = price × quantity, - normalise a value, such as bringing a phone number into a fixed format.
The model hook sees the whole object and belongs to exactly one model. The field hook receives the value and returns the value to use instead.
Writing a field hook
A field hook implements HookFieldInterface<T>. T is the Java type of the field, so String for a text:
@Component
public class EncryptHook implements HookFieldInterface<String> {
@Override
public String beforeDatabaseChange(String value, FieldHookContext context) {
return value == null ? null : cipher.encrypt(value);
}
@Override
public String afterDatabaseRead(String value, FieldHookContext context) {
return value == null ? null : cipher.decrypt(value);
}
}
Both methods return a value, and CDMS puts it into the field. If you want to change nothing, return value unchanged.
The second parameter, the context (FieldHookContext), tells you where you are:
| Method | returns |
|---|---|
item() | the object with all its other fields |
modelMeta() | the description of the model |
field(), fieldName() | the description and the name of the field |
method() | the operation: CREATE, UPDATE, PATCH or READ |
If the class is a Spring bean, for example with @Component, CDMS uses that bean. Otherwise CDMS creates the class itself through Spring. Either way it gets its dependencies through @Autowired or @Value, the key for example.
Assigning the field hook to a field
The assignment lives in the model, not in the code. In the hub you open the field and set the rule Field hook on the Rules tab. As its value you enter the fully qualified class name:
com.example.shop.hooks.EncryptHook
Separate several hooks with commas. They run in this order:
com.example.shop.hooks.CompressHook, com.example.shop.hooks.EncryptHook
-
1Modellersets the rule Field hook (
@hook) with the class name on the field -
2Generatorchecks the name and writes the class into the description of the fieldA name without a package, such as
EncryptHook, stops the generation. A class that does not exist in the project stops the compilation. -
3CDMScreates every field hook at startup and checks that its type fits the fieldIf it does not, the application does not start. The message names model, field and hook:
field-hook-type-mismatch|Customer.phone -> …EncryptHook. -
4Field hookruns from now on whenever this field is written and read
A field hook exists on simple fields only, that is text, number, date and enum fields. A relation gets no field hook; that is what the model hook is for.
The path of a value
sequenceDiagram
participant C as Client
participant D as CDMS
participant H as Field hook
participant DB as Database
C->>D: POST code = "geheim"
D->>D: model before hooks
D->>H: beforeDatabaseChange("geheim")
H-->>D: "x7Kq…"
D->>DB: stores "x7Kq…"
D->>DB: reads the object for the response
D->>H: afterDatabaseRead("x7Kq…")
H-->>D: "geheim"
D->>D: model read hook
D-->>C: 200, code = "geheim"
After writing, the field hook does not run. The response to a write is produced by reading, and that is where afterDatabaseRead runs.
When the hook runs before writing
Before writing, a field hook runs only for fields the operation writes. That way a stored value is never encrypted a second time.
| Operation | Does beforeDatabaseChange run? |
|---|---|
| Create (POST) | yes, for every field with a hook |
| Replace (PUT) | yes, for every field with a hook |
| Change (PATCH) | only for the fields the PATCH sends |
| PUT or PATCH that a field role refuses, keeping the stored value | no, the stored value stays as it is |
| a model before hook changes the field | yes, this value passes the field hook too |
| Delete | no |
| Roll back to an old state | no, the old state is taken over as it was stored |
The field hook runs after the model’s before hooks and before the rules are checked. A hook that derives a value can therefore also fill a mandatory field. If a field has several hooks, they run in the order from the model. When reading they run in reverse order: what was applied last is undone first.
A value that follows from others
A hook that derives a value must also run when only one of its sources changes. A PATCH with only price does not send total. That is why such a hook signs up for every write:
@Component
public class TotalHook implements HookFieldInterface<Integer> {
@Override
public Integer beforeDatabaseChange(Integer value, FieldHookContext context) {
OrderEntity order = (OrderEntity) context.item();
if (order.getPrice() == null || order.getQuantity() == null)
return null;
return order.getPrice() * order.getQuantity();
}
@Override
public Integer afterDatabaseRead(Integer value, FieldHookContext context) {
return value;
}
@Override
public boolean runsOnEveryWrite() {
return true;
}
}
With runsOnEveryWrite() the hook runs on every create, replace and change of the object. It then also receives the stored value and ignores it. It does not run on delete or roll back.
Field roles and field hooks
A field role requires its role only for a change. If you send a record back as you read it, you need none. For that CDMS compares the sent value with the stored one as a reader sees it, that is after afterDatabaseRead. An encrypted field you send back unchanged therefore does not count as changed.
Decision table
| What you want to do | Hook |
|---|---|
| transform a single value, such as encrypting it | field hook |
| derive a value from other fields of the same object | field hook with runsOnEveryWrite() |
| the same logic on many fields in many models | field hook |
| check a rule that concerns several fields together, and refuse | model hook |
| maintain relations or write other records | model hook |
| do something on a relation | model hook, field hooks exist on simple fields only |
Pitfalls
Where to go next
- The field hook that ships for encryption: Encrypted fields
- The hook for the whole object: Hooks: types and timing
- Where the hooks sit between role check, validation and saving: The order within a write operation
- What the client sees when a hook throws: When a hook fails