Validations, default values, and unmapped fields

This article describes how data entity values are validated, how default values can be provided, and how to use fields that are not mapped to data source values, but instead contain virtual or computed data (unmapped fields).

Validations

Validations can be defined on the tables that back up entities, at both the field level and the record level. Validations can also be defined at the data entity level.

Table (data source) vs. entity validation

Entities are backed by tables (data sources), and validations are defined for these tables at both the field level (Table.validateField()) and the record level (Table.validateWrite()). The validations are respected by data entities that are built by using those tables. Although these validations are intrinsic to the tables that back a data entity, validations can also be defined at the data entity level. Like table-based validations, entity-based validations can be written at the field level (DataEntity.validateField()) or the record level (DataEntity.validateWrite()).

Table-based validation behavior

Table validations are fired automatically as a part of the CUD operations. Table.ValidateField, AllowEdit, AllowEditOnCreate Field-level validations are fired automatically when you perform inserts or updates on the data entity. This is true for all paths (X++, OData, and so on). These validations occur during the mapping process, when fields are mapped from an entity to individual data sources.

redo1.

After the field values from the data entity are copied to mapped data source fields, field validations are run on the set fields. Validations include table-level validateField, which validates AllowEdit and AllowEditOnCreate. If a validation fails because of an error, validation for the remaining fields continues. Finally, validation checks whether any error occurred during the validation process for any of the data sources. If there was an error, the process errors out at this point, and table-level validateWrite() isn't called. To skip validateField for a back-end table, a consumer can call DataEntity.skipDataSourceValidateField(Int _DataEntityFieldId, Boolean _skip). Note that the field ID for this method is the field ID of the data-entity mapped field, not the back-end table field. By using the following API, you can skip validation for a particular field, regardless of the consumer.

Over9.

Table.ValidateWrite Record-level ValidateWrite validations that are defined in back-end tables are fired automatically when you perform data-entity inserts and updates. This is true for all paths (X++, OData, and so on). These validations occur just before the actual insert or update is applied to the data source. If the validation fails, an error is thrown, and the process stops for other data sources.

redo2.

To skip validateWrite for all back-end tables for a data entity, a consumer can call DataEntity.skipDataSourceValidateWrite(Boolean _skip). This method turns validateWrite on or off for all data sources. By using the following API, you can skip validation for a particular data source, regardless of the consumer.

Over10.

Table.ValidateDelete Record-level ValidateDelete validations that are defined in back-end tables are fired automatically when you perform data entity deletes. This is true for all paths (X++, OData, and so on). These validations occur just before the delete is applied to the data source. If the validation fails, an error is thrown, and the process stops for other data sources.

Over11.

To skip validateDelete for all back-end tables for a data entity, a consumer can call DataEntity.skipDataSourceValidateDelete(Boolean _skip). This method turns validateDelete on or off for all data sources. By using the following API, you can skip validation for a particular data source, regardless of the consumer.

Over12.

Entity-based validation behavior

Validation Target Caller
DataEntity.ValidateField
  • Data types
  • Mandatory relationships (both tables and extended data types [EDTs])
  • Any custom validation
  • Doesn't call validateField for underlying mapped table fields
  • Is called automatically from OData
  • Is called by the form engine when a field is modified
  • Isn't called automatically if an insert/update is fired from X++ code
DataEntity.ValidateWrite
  • Mandatory columns
  • Relationships (both tables and EDTs)
  • Any custom validation
  • Doesn't call table-level validateWrite for underlying tables
  • Is called automatically from OData
  • Is called by the form engine when a record is saved.
  • Isn't called automatically if an insert/update is fired from X++ code
DataEntity.ValidateDelete
  • DeleteActions
  • Any custom validation
  • Doesn't call table-level validateDelete for underlying tables
  • Is called automatically from OData.
  • Is called by the form engine when a record is deleted
  • Isn't called automatically if a delete is fired from X++ code

Defaults

Default values can be provided for initializations and rows.

Initializations

DataEntity.initValue: A data entity is initialized with default values and by using any custom logic that is present in entity-level initValue. This method isn't called automatically when an insert or update is performed on a data entity from X++. It must be called explicitly if it's required. The method is called automatically by the form engine when a new record is created. DataEntity.initValue doesn't call the initValue method for back-end tables that are used in the data entity. Table.initValue: Table-level initValue, as defined for back-end tables, is fired when you perform a data entity insert. This is true for all paths (X++, OData, and so on). Table.initValue is run just before the entity is mapped to data source fields.

Over13.

To skip entity-level initValue for all back-end tables for a data entity, a consumer can call DataEntity.skipDataSourceInitValue(Boolean _skip). This method turns initValue on or off for all data sources. By using the following API, you can skip initValue for a particular field, regardless of the consumer.

Capturea.

DefaultRow

DataEntity.DefaultRow: DataEntity.DefaultRow is used in conjunction with defaultField and getDefaultingDependencies to provide defaults. It isn't called automatically by X++ or the form engine. Table.DefaultRow: Table.DefaultRow is called automatically for each data source after mapping is completed, and before the insert and validation on the data source.

Captureb.

Unmapped fields

A data entity can have unmapped fields in addition those fields that are directly mapped to fields of the data sources. There are two mechanisms for generating values for unmapped fields:

  • Custom X++ code
  • SQL that is run by Microsoft SQL Server

The two types of unmapped fields are virtual and computed. Unmapped fields always support read actions, but the feature specification might not require any development effort to support write actions.

Virtual field

  • A non-persisted field.
  • Controlled by custom X++ code.
  • Read and writes occur through custom X++ code.
  • Typically used for intake values that are calculated by using X++ code and can't be replaced by computed columns.

Computed field

  • The value is generated by an SQL view computed column.
  • During reads, data is computed by SQL and fetched directly from the view.
  • For writes, custom X++ code must parse the input value and then write the parsed values to the regular fields of the data entity. The values are stored in the regular fields of the data sources of the entity.
  • Used mostly for reads.
  • It's a good idea to use computed columns instead of virtual fields whenever you can, because computed columns are computed at the SQL Server level, whereas virtual fields are computed row by row in X++.

Properties of unmapped fields

Category Name Type Default value Behavior
Data IsComputedField NoYes Yes
  • Yes: The field is synchronized as a SQL view computed column. An X++ method is required to compute the SQL definition string for the column. The virtual column definition is static and is used when the entity is synchronized. After that, the X++ method isn't called at run time.
  • No: The field is a true virtual field, where inbound and outbound values are fully controlled through custom code.
Data ComputedFieldMethod String A static DataEntity method in X++ is used to build the SQL expression that generates the field definition. This property is disabled and irrelevant if the IsComputedField property is set to No. The method is required if the IsComputedField property is set to Yes.
Data ExtendedDataType String

Unmapped field comparison

Virtual field Computed field
Metadata properties Is computed = No
  • Is Computed = Yes
  • Computed Field Method = static method
Read
  • X++ (override postLoad)
  • Row by row
  • SQL computed column
  • Set-based read possible
Write X++ (override mapEntityToDataSource) X++ (override mapEntityToDataSource)
Advantages
  • Unbound to the schema, keeps the public contract the same, but the implementation can change
  • Call X++ methods
Faster reads, large export can occur directly from the view

Examples

The following table provides a computed example if a UnitOfMeasure relationship exists, and displays that in an unmapped field.

Virtual field Computed field
On postLoad()//Check to see if record exists in UnitOfMeasureInternalCode.UnitOfMeasure//Set hasFixedInternalCode value based on the fieldif(this.UnitOfMeasure)this.HasFixedInternalCodeVirtual = NoYes::Yes; else this.HasFixedInternalCodeVirtual = NoYes::No; On computedFieldMethod()//Desired SQL computed column statement(CASE WHEN T2.RECID IS NULL THEN 0 ELSE 1 END) AS INT)