Table Mappings
Arda uses Exposed as its relational mapping layer. This document describes how to create the data classes, components, tables, and record classes needed to map a domain payload to a bitemporal table.
Basic Entity Mappings
Section titled “Basic Entity Mappings”Basic entity mappings use only natively supported Exposed Column Types.
Defining a Payload for Entity Persistence
Section titled “Defining a Payload for Entity Persistence”A class must:
- Extend
cards.arda.common.lib.persistence.universe.EntityPayload - Be a Kotlin
data class
Pattern:
@Serializablesealed 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 or failure with AppError } }}Defining Metadata
Section titled “Defining Metadata”Entity metadata must extend from PayloadMetadata or one of its subtypes:
@Serializabledata class SampleEntityMetadata( @Serializable(with = UUIDSerializer::class) override val tenantId: UUID) : ScopedMetadataTable Configuration and Table Pattern
Section titled “Table Configuration and Table Pattern”object SampleTableConfiguration: TableConfiguration { override val name = "SAMPLE"}
object SAMPLE_TABLE : ScopedTable(SampleTableConfiguration) { val sampleProperty = varchar("sample_property", 255) // other columns...}Column names must be lowercase snake_case. While Exposed allows uppercase, the project standard is lowercase to prevent surprises in SQL queries and external tools.
Record Type Pattern
Section titled “Record Type Pattern”class SampleRecord(rId: EntityID<UUID>): ScopedRecord<SamplePayload, SampleMetadata, SAMPLE_TABLE, SampleRecord>( rId, SAMPLE_TABLE) { var sampleProperty by SAMPLE_TABLE.sampleProperty // other delegates...
companion object : Persistence<SamplePayload, SampleMetadata, SAMPLE_TABLE, SampleRecord>( SAMPLE_TABLE, SampleRecord::class.java, { SampleRecord(it) }, { sampleProperty = payload.sampleProperty // other fill operations... } )
override fun fillPayload() { payload = SamplePayload.Entity( eId = eId, sampleProperty = sampleProperty ) }
override fun fillMetadata() { metadata = SampleMetadata(tenantId) }}Universe
Section titled “Universe”fun validatorFor(u: SampleUniverse): ScopingValidator<SamplePayload, SampleMetadata> { return object : ScopingValidator<SamplePayload, SampleMetadata>() { }}
object SampleUniversalCondition: ScopedUniversalCondition()
val sampleQueryConfig = EntityServiceConfiguration.create(SamplePayload::class).also { it.freeze() }
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) override val translator by lazy { sampleQueryConfig.bindToTable(persistence.bt) }}The EntityServiceConfiguration introspects the @Serializable payload class and builds a structured locator translator. This enables query locators to use JSON field names (camelCase) in addition to raw column names. See Query DSL: EntityServiceConfiguration.
Composite Mappings
Section titled “Composite Mappings”Composite mappings are those whose properties are defined as other Data Classes treated as “pure values” (without their own identity).
Defining and Mapping Property Types
Section titled “Defining and Mapping Property Types”@Serializablesealed interface PropertyType { // properties...
@Serializable data class Value( // override properties... ) : PropertyType}The mapping is defined using a Component abstraction:
abstract class PropertyTypeComponent<TBL, R, DOMAIN: PropertyType?>( override val cmpName: String, override val parent: Component<TBL, R, *>, override val tbl: TBL) : Component.Sub<TBL, R, DOMAIN> { // ... override fun getComponentValue(row: R): Result<DOMAIN?> = with(row) { when (build.requiring(childProperty, /* other required properties */)) { true -> Result.success(PropertyType.Value(...) as DOMAIN) false -> Result.success(null) null -> Result.failure(AppError.IncompatibleState("must be all null or none null")) } }
override fun setComponentValue(r: R, valueObject: DOMAIN) = with(r) { // set mapped properties from valueObject }}All column definitions within a composite should be
nullable()unless:
- The PropertyType usage will always be non-nullable (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 by
build.requiring(...), which takes up to 22 arguments.
Using Property Types in Entity Mappings
Section titled “Using Property Types in Entity Mappings”Payload definition:
data class Entity( override val eId: EntityId, override val compositeProperty: PropertyType.Value? = null, // other properties...) : PayloadClass { override fun validate(...): Result<Unit> { ... }}Table definition:
object SAMPLE_TABLE : ScopedTable(SampleTableConfiguration) { private val root = Component.Root<SAMPLE_TABLE, SampleRecord>(this) val compositeProperty = compositePropertyComponent<SAMPLE_TABLE, SampleRecord, CompositePropertyType.Value?>( "composite_property", root )}For nullable composite: CompositePropertyType.Value?
For non-nullable composite: CompositePropertyType.Value
Building a DataAuthorityUniverse
Section titled “Building a DataAuthorityUniverse”Final step — build the universe:
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(...): DBIO<Unit> = { Result.success(Unit) } override suspend fun validateForUpdate(...): DBIO<Unit> = { Result.success(Unit) } override suspend fun validateForDelete(...): DBIO<Unit> = { Result.success(Unit) } }}This universe can now be used to perform CRUD operations on the collection of entities it manages.
Copyright: © Arda Systems 2025-2026, All rights reserved