Data Relationships, Foreign Keys, and Referential Integrity

Key Takeaways

  • One-to-many relationships use a reference attribute (typed identifier) as a foreign key on the many side.
  • Many-to-many requires a join entity with reference attributes to both sides, plus optional relationship metadata attributes.
  • Delete Rule Protect (default) blocks parent deletion when children reference it; Delete cascades; Ignore leaves children orphaned with NullIdentifier.
  • NullIdentifier() is the platform's null sentinel for identifier types — always use it instead of 0 for no-reference comparisons.
  • CreateOrUpdate performs a Create when the identifier is NullIdentifier() and an Update when a valid identifier is provided.
Last updated: July 2026

Quick Answer: OutSystems models data relationships through reference attributes (foreign keys), supports one-to-many directly and many-to-many via join entities, and enforces referential integrity through Delete Rules (Protect, Delete, Ignore). The NullIdentifier function represents "no reference," and the platform provides built-in entity actions (Create, CreateOrUpdate, Update, Delete) for standard CRUD operations.

One-to-Many Relationships

A one-to-many relationship is the most common relationship in OutSystems data modeling. You implement it by adding a reference attribute to the "many" side entity that points to the identifier of the "one" side entity. For example, a Customer entity (one) has many Order entities (many); the Order entity gets a CustomerId reference attribute of type Customer Identifier.

The reference attribute is physically a foreign key column in the database, but OutSystems abstracts it as a typed identifier — Customer Identifier rather than a raw Long Integer. This typing enables the platform to enforce referential integrity automatically and lets you navigate the relationship in aggregates and expressions without writing explicit SQL joins. When you create an Order record, you set Order.CustomerId to a valid Customer identifier; the platform validates that the referenced Customer exists at runtime. When you build an aggregate sourcing from Order, the platform detects the reference attribute and auto-creates the join to Customer, letting you access Customer.Name directly in the output without writing join syntax.

Many-to-Many Relationships

OutSystems does not have a native many-to-many relationship type. You model many-to-many by creating a join entity (also called an intermediary or junction entity) that has reference attributes to both sides. For example, a Student can enroll in many Course entities, and a Course can have many Student entities — you create an Enrollment join entity with StudentId (Student Identifier) and CourseId (Course Identifier) reference attributes.

The join entity can also carry attributes that describe the relationship itself, such as EnrollmentDate, Grade, or Status. This is a key advantage of the join-entity pattern: the relationship metadata lives on the join entity and is fully queryable in aggregates like any other entity data.

Delete Rules and Referential Integrity

When you define a reference attribute, you must choose a Delete Rule that governs what happens to the referencing records when the referenced record is deleted. OutSystems offers three Delete Rules:

Delete RuleBehaviorUse When
ProtectPrevents deletion of the parent if any child references it; raises a runtime exceptionYou want to block destructive deletes that would orphan records (default and safest)
DeleteCascades the delete — removing the parent also removes all referencing child recordsThe children are meaningless without the parent (e.g., Order Lines when an Order is deleted)
IgnoreAllows the parent delete and sets the child's reference to NullIdentifierRarely used; the child can meaningfully exist without the parent

The Protect rule is the default and the safest choice. It raises an exception at runtime if you try to delete a Customer that has Orders. You must explicitly delete or reassign the child records first. The exam frequently tests Protect as the default behavior.

The Delete rule cascades: deleting the parent entity record automatically deletes all referencing child records. Use this when children are owned by the parent — the classic example is deleting an Order should also delete its Order Lines because they have no independent meaning.

The Ignore rule is the least common. It allows the parent to be deleted while leaving child records in place, with their reference attribute set to NullIdentifier(). Use this only when the child can legitimately exist without the parent, which is rare in well-designed schemas.

NullIdentifier

NullIdentifier() is the OutSystems function that represents "no reference" — the null sentinel for identifier-type values. When an entity record has no parent, you set its CustomerId to NullIdentifier() rather than 0 or empty string. It resolves to the correct type at compile time, returning the platform's null sentinel for that identifier type.

You test for "no reference" by comparing against NullIdentifier(): if Order.CustomerId = NullIdentifier() then "Unassigned" else "Assigned". A common exam trap: comparing against 0 is not safe because 0 can be a valid identifier in some configurations. Always use NullIdentifier() for identifier comparisons, never numeric literals.

Entity Actions

OutSystems generates built-in CRUD actions for every entity automatically. You do not write SQL for basic operations — the platform provides these entity actions:

  • Create<Entity>: inserts a new record and returns the new identifier. You pass a record of the entity's record type with attribute values set.
  • CreateOrUpdate<Entity>: inserts if the identifier is NullIdentifier(), updates if a valid identifier is provided. Combines create and update in one action, ideal for upsert and import/sync patterns.
  • Update<Entity>: updates an existing record by identifier. Raises an exception if the record does not exist.
  • Delete<Entity>: deletes a record by identifier. Triggers Delete Rules on all referencing records, cascading or blocking as configured.

These actions are available in the logic canvas under the entity's name in the toolbox. They run server-side and participate in the implicit transaction that wraps each server action — if any logic in the same server action raises an exception, all database changes roll back automatically. A common exam point: CreateOrUpdate inspects the identifier — if it equals NullIdentifier(), the action performs a Create; otherwise, it performs an Update. This makes it the standard choice for upsert patterns. Another exam point: the Delete action triggers Delete Rules on all referencing records — if any referencing record has Protect as its Delete Rule, the deletion fails with an exception, rolling back the transaction.

Test Your Knowledge

What happens when you try to delete a Customer record that has referencing Order records, if the Delete Rule on the Order.CustomerId reference is set to Protect?

A
B
C
D
Test Your Knowledge

How should you represent and test for 'no reference' on a CustomerId attribute of type Customer Identifier in OutSystems?

A
B
C
D
Test Your Knowledge

What does the CreateOrUpdate entity action do when the record's identifier is NullIdentifier()?

A
B
C
D