Skip to content

@Relation

Marks a foreign-key property and triggers generation of findBy<Parent>, observeBy<Parent>, and deleteBy<Parent> convenience methods on the child repository.

Signature

@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.SOURCE)
annotation class Relation(
    val cascade: Boolean = false
)

Parameters

Parameter Type Default Description
cascade Boolean false Documents that this FK participates in a cascade delete. Has no runtime effect — the cascade is performed manually by calling deleteBy<Parent> before delete(parentId).

Naming convention

Kiln infers the parent entity name from the property name by stripping a trailing Id suffix and capitalising the result:

Property name Inferred parent Generated methods
projectId Project findByProject, observeByProject, deleteByProject
userId User findByUser, observeByUser, deleteByUser
categoryId Category findByCategory, observeByCategory, deleteByCategory
ownerId (no entity named Owner) Owner (used verbatim) findByOwner, observeByOwner, deleteByOwner

If the property name does not end in Id, the whole name is capitalised:

Property name Inferred parent
parent Parent
author Author

Example

@DbEntity(tableName = "projects")
data class Project(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    val name: String
)

@DbEntity(tableName = "tasks")
data class Task(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    val title: String,
    @Relation(cascade = true) val projectId: Long = 0
)

Kiln generates these extra methods on TaskRepository:

suspend fun findByProject(projectId: Long): List<Task>
fun observeByProject(projectId: Long): Flow<List<Task>>
suspend fun deleteByProject(projectId: Long)

Cascade delete pattern

cascade = true is documentation only. Perform the cascade explicitly before deleting the parent:

// Wrap in a transaction so both tables notify their observers atomically
driver.withTransaction {
    taskRepo.deleteByProject(projectId)   // removes all child rows
    projectRepo.delete(projectId)         // removes the parent row
}

See driver.withTransaction for the transaction API.


Nullable relations

A @Relation property can be nullable to model optional parent references — for example, an order that may or may not belong to a registered customer:

@DbEntity(tableName = "orders")
data class Order(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    @Column(name = "customer_id") @Relation val customerId: Long? = null,
    val total: Double
)

The generated findByCustomer(id) and observeByCustomer(id) still work — they match rows where customer_id equals the given id. Rows where customer_id is NULL (walk-in orders, in this case) are excluded from those results. Use isNull() / isNotNull() in a DSL query to find them:

val walkIns = orderRepo.findWhere { OrderColumns.customerId.isNull() }
val registered = orderRepo.findWhere { OrderColumns.customerId.isNotNull() }

Reactive join

Combine observeBy<Parent> with observeAll() on the parent to build a live Pair<Project?, List<Task>> stream:

combine(
    projectRepo.observeAll(),
    taskRepo.observeByProject(projectId)
) { projects, tasks ->
    projects.find { it.id == projectId } to tasks
}.collect { (project, tasks) ->
    // re-emits whenever either table changes
}