Creating Table Mappings
Arda’s system uses Exposed as its relational mapping layer. Using this layer, Arda’s system
defines Bitemporal Records that to map payload to records in the Database. Furthermore, it defines the concept of Universe as a bitemporal repository
with additional capabilities for scoping and validation of CRUD and Query operations.
Basic Entity Mappings¶
Basic Entity Mappings are those that only use natively supported Column Types in Exposed
This document describes how to create the data classes, components, tables, and record classes needed to set up a table that reflects a data type with nested data classes at multiple levels. It also explains how to build a DataAuthorityUniverse that persists a pair of data types representing Payload and Metadata.
Defining a Payload for Entity Persistence¶
To map a Kotlin class to such these capabilities, a class must:
- Extend
cards.arda.common.lib.persistence.universe.EntityPayload - Be a Kotlin
data class
The pattern to define a Class as a payload for persistence mapping is:
@Serializable
sealed interface PayloadClass : EntityPayload {
@Serializable(with = UUIDSerializer::class)
override val eId: EntityId
<<other properties>>
data class Entity(
@Serializable(with = UUIDSerializer::class)
override val eId: EntityId
<<other properties>>
) : PayloadClass {
override fun validate(ctx: ApplicationContext, mutation: Mutation): Result<Unit> {
// Return Success if validation is O.K. Failure with exception for the reason otherwise
}
}
}
Properties can be any of the supported column types from Exposed including JSON values.
Entities in Arda’s system also require defining associated Metadata that will control non-domain aspects of their behavior. Typically
these could be things like the Scope in which an entity needs to be interpreted, lifecycle information (e.g. Draft, Published, etc…) or
other similar characteristics. An EntityPayload metadata must extend from cards.arda.common.lib.persistence.types.PayloadMetadata or one of its
subtypes. For example
@Serializable
data class SampleEntityMetadata(
@Serializable(with = UUIDSerializer::class)
override val tenantId: UUID
) : ScopedMetadata
Defining the Persistence Mapping for and EntityPayload¶
Persistence is defined by:
- A Table Configuration that defines the Table Name (and in the future other potential mapping characteristics)
- A Table definition that declares the column mappings for each defined property
- A Record Type that must extends
BitemporalRecordwith properties provided by the table mapped columns and the logic to fill
in the payload and metadata associated with the entity. - One or multiple
UniversalConditionto configure the set of entities that a particular Universe manages. - A Universe, extending from
AbstractUniverseor one of its subtypes.
Table Configuration and Table Pattern¶
object SampleTableConfiguration: TableConfiguration {
override val name = "SAMPLE"
}
object SAMPLE_TABLE : ScopedTable(ItemTableConfiguration) {
val sampleProperty = varchar("SAMPLE_PROPERTY", 255)
<<...>>
}
Record Type Pattern¶
class SampleRecord(rId: EntityID<UUID>): ScopedRecord<SamplePayload, SampleMetadata, SAMPLE_TABLE, SampleRecord>(rId,
ITEM_TABLE) {
var sampleProperty by SAMPLE_TABLE.sampleProperty
<<...>>
companion object : Persistence<SamplePayload, SampleMetadata, SAMPLE_TABLE, SampleRecord>(
SAMPLE_TABLE,
SampleRecord::class.java,
{ SampleRecord(it) },
{
sampleProperty = payload.sampleProperty
<<...>>
}
)
override fun fillPayload() {
payload = Item.Entity(
eId = eId,
sampleProperty = sampleProperty
)
}
override fun fillMetadata() {
metadata = SampleMetadata(tenantId)
}
}
Sample Universal Condition and Universe¶
fun validatorFor(u: SampleUniverse): ScopingValidator<SamplePayload, SampleMetadata> {
return object : ScopingValidator<SamplePayload, SampleMetadata>() {
}
}
object SampleUniversalCondition: ScopedUniversalCondition()
class SampleUniverse :
AbstractScopedUniverse<SamplePayload, SampleMetadata, SAMPLE_TABLE, SampleRecord>(),
LogEnabled by LogProvider(SampleUniverse::class) {
override val persistence = SampleRecord.Companion
override val universalCondition = SampleUniversalCondition
override val validator = validatorFor(this)
}
Extensions for Composite Mappings¶
Composite Mappings are those whose properties can be defined as other Data Classes that are treated as “pure values” (i.e. they don’t have an identity of their own)
Arda’s common libraries support Composite mappings following a pattern to define the data classes to be used as value properties:
Defining and Mapping Property Types¶
@Schema(
description = "Classification for an Item with two levels, type and subtype. They will be " +
"migrated to represent actual category entities in the future"
)
@Serializable
sealed interface PropertyType {
<<properties>>
@Serializable
data class Value(
<<override properties>>
) : PropertyType
}
The mapping of the property type is defined as:
inline fun <TBL : EntityTable, R: RowRecord<TBL, R>, reified DOMAIN : PropertyType?>
TBL.propertyTypeComponent(name: String, parent: Component<TBL, R, *>): PropertyTypeComponent<TBL, R, DOMAIN> {
return object : PropertyTypeComponent<TBL, R, DOMAIN>(name, parent, this@propertyTypeComponent) {
override val isNullable = (null is DOMAIN)
}
}
abstract class PropertyTypeComponent<TBL: EntityTable, R: RowRecord<TBL, R>, DOMAIN: PropertyType?>(
override val cmpName: String,
override val parent: Component<TBL, R, *>,
override val tbl: TBL
) : Component.Sub<TBL, R, DOMAIN> {
override val prefix = prefixGetter()
val childProperty = <<column or sub-component definition>>
<<other property definitions>>
@ExperimentalContracts
@Suppress("UNCHECKED_CAST")
override fun getComponentValue(row: R): Result<DOMAIN?> =
with(row) {
val childProperty = childProperty.getValue(row, ItemClassification::type)
when (build.requiring(childProperty, <<other required properties >>)) {
true -> Result.success(
ItemClassification.Value(
childProperty = childProperty,
<<initialize other properties from row>>
) as DOMAIN
)
false -> Result.success(null)
null -> Result.failure(
AppError.IncompatibleState("ItemClassification: {\n type: ${type}\n} must be all null or none null")
)
}
}
override fun setComponentValue(r: R, valueObject: DOMAIN) = with(r) {
<< set mapped properties from valueObject >>
}
}
Notes¶
- All property definitions should be defined as
nullable()unless:- The usage of the PropertyType will always be not-nullable itself, which is not reasonable to guarantee for non-private types.
- The atomic property itself is non-nullable
- Local Nullability when the PropertyType is non-null is defined byt the
build.requiring(type)condition which can take up to 22 arguments.
Using Property Type Definitions in Entity Mappings¶
With these definitions, the EntityPayload is defined as:
@Serializable
sealed interface PayloadClass : EntityPayload {
@Serializable(with = UUIDSerializer::class)
override val eId: EntityId
val compositeProperty: PropertyType.Value?
data class Entity(
@Serializable(with = UUIDSerializer::class)
override val eId: EntityId,
override val compositeProperty: Property.Value? = null
<<other properties>>
) : PayloadClass {
override fun validate(ctx: ApplicationContext, mutation: Mutation): Result<Unit> {
// Return Success if validation is O.K. Failure with exception for the reason otherwise
}
}
}
And only the Table Definition changes:
Table Definition¶
object SAMPLE_TABLE : ScopedTable(ItemTableConfiguration) {
private val root = Component.Root<SAMPLE_TABLE, ItemRecord>(this)
val compositeProperty = compositePropertyComponent<SAMPLE_TABLE, SampleRecord, CompositePropertyType.Value?>("composite_property", root)
<<...>>
}
Note that if the component itself is nullable or not is indicated by the generic parameters:
For nullable component:
compositePropertyComponent<SAMPLE_TABLE, SampleRecord, CompositePropertyType.Value?>("composite_property", root)
For Non-nullable component:
compositePropertyComponent<SAMPLE_TABLE, SampleRecord, CompositePropertyType.Value>("composite_property", root)
Building a DataAuthorityUniverse¶
Finally, build a DataAuthorityUniverse to manage the persistence of your data. The DataAuthorityUniverse ties together the data classes, components, tables, and record classes.
class ComponentsTestUniverse : AbstractUniverse<TestEntity, TestMetadata, TEST_TABLE, TestRow>()
, LogEnabled by LogProvider(ComponentsTestUniverse::class) {
override val persistence = TestRow.Companion
override val universalCondition = ScopedUniversalCondition()
override val validator = object : Validator<TestEntity, TestMetadata> {
override suspend fun validateForCreate(
ctx: ApplicationContext,
payload: TestEntity,
metadata: TestMetadata,
asOf: TimeCoordinates,
author: String
): DBIO<Unit> = {
Result.success(Unit)
}
override suspend fun validateForUpdate(
ctx: ApplicationContext,
payload: TestEntity,
metadata: TestMetadata,
asOf: TimeCoordinates,
author: String,
idempotency: Idempotency,
previous: BitemporalEntity<TestEntity, TestMetadata>?
): DBIO<Unit> = {
Result.success(Unit)
}
override suspend fun validateForDelete(
ctx: ApplicationContext,
candidate: BitemporalEntity<TestEntity, TestMetadata>,
metadata: TestMetadata,
asOf: TimeCoordinates,
author: String
): DBIO<Unit> = {
Result.success(Unit)
}
}
}
This ComponentsTestUniverse can now be used to perform CRUD operations on the collection of entities that the universe manages.
Copyright: © Arda Systems 2025, All rights reserved