Entity definition
Last modified: 03 March 2025An Entity
in Exposed maps a database table record to a Kotlin object. This ensures type safety and allows you to work with database records just like regular Kotlin objects, taking full advantage of Kotlin's language features.
When you use the Data Access Object (DAO) approach, the IdTable
needs to be associated with an Entity
. This is because every database record in this table needs to be mapped to an Entity
instance, identified by its primary key.
Defining an Entity
You define an entity instance by creating a class. In the following example, StarWarsFilmEntity
is the entity class linked to the table StarWarsFilmsTable
:
package org.example.entities
import org.example.tables.StarWarsFilmsTable
import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.IntEntityClass
import org.jetbrains.exposed.dao.id.EntityID
class StarWarsFilmEntity(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<StarWarsFilmEntity>(StarWarsFilmsTable)
var sequelId by StarWarsFilmsTable.sequelId
var name by StarWarsFilmsTable.name
var director by StarWarsFilmsTable.director
}
import org.jetbrains.exposed.dao.id.IntIdTable
const val MAX_VARCHAR_LENGTH = 50
object StarWarsFilmsTable : IntIdTable() {
val sequelId = integer("sequel_id").uniqueIndex()
val name = varchar("name", MAX_VARCHAR_LENGTH)
val director = varchar("director", MAX_VARCHAR_LENGTH)
}
Entity type
The entity type determines how the Entity
class interacts with the table’s primary key. Since StarWarsFilmsTable
is an IntIdTable
, the StarWarsFilmsEntity
class extends from IntEntity
, which indicates that the ID and primary key of StarWarsFilmsTable
is of type Int
.
The following entity types are supported:
IntEntity
Base class for an entity instance identified by an ID comprised of a wrapped
Int
value.LongEntity
Base class for an entity instance identified by an ID comprised of a wrapped
Long
value.UIntEntity
Base class for an entity instance identified by an ID comprised of a wrapped
UInt
value.ULongEntity
Base class for an entity instance identified by an ID comprised of a wrapped
ULong
value.UUIDEntity
Base class for an entity instance identified by an ID comprised of a wrapped
UUID
value.CompositeEntity
Base class for an entity instance identified by an ID comprised of multiple wrapped values.
Entity class
The EntityClass
in the companion object
block is responsible for managing Entity
instances, such as creating, querying, and deleting records. It also maintains the relationship between the entity class (StarWarsFilmEntity
in this example) and the database table (StarWarsFilmsTable
).
Each entity type is supported by a corresponding EntityClass
, which accepts the following parameters:
- table
The
IdTable
containing rows mapped to entities that are managed by this class.- entityType
(Optional) The expected type of the
Entity
class. This can be omitted if it is the class of the type argument provided to thisEntityClass
instance.- entityCtor
(Optional) The function that instantiates an
Entity
using the providedEntityID
. If not provided, reflection is used to determine the primary constructor on the first access (which may affect performance).
In the example above, IntEntityClass
is specified in the companion object:
companion object : IntEntityClass<StarWarsFilmEntity>(StarWarsFilmsTable)
This setup enables you to use functions provided by IntEntityClass
to manage instances of StarWarsFilmEntity
effectively.
Properties
Each column in the table is represented as a property in the entity class, where the by
keyword ensures the data is fetched or updated from the corresponding column when accessed.
var sequelId by StarWarsFilmsTable.sequelId
var name by StarWarsFilmsTable.name
var director by StarWarsFilmsTable.director
Once the entity class is defined, instances of this class allow you to manipulate individual records from the corresponding table. For example, by creating a new record, retrieving a row based on its primary key, updating values, or deleting records.
Immutable entities
For defining entities that are immutable, Exposed provides the additional ImmutableEntityClass
and ImmutableCachedEntityClass
.
The ImmutableCachedEntityClass
uses an internal cache to store entity loading states by the associated database. This ensures that entity updates are synchronized with this class as the lock object.
To create an immutable entity, use either ImmutableEntityClass
or ImmutableCachedEntityClass
as a companion object in your entity class. For example, here’s how to define a CityEntity
class:
package org.example.entities
import org.example.tables.CitiesTable
import org.jetbrains.exposed.dao.ImmutableEntityClass
import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.id.EntityID
class CityEntity(id: EntityID<Int>) : IntEntity(id) {
val name by CitiesTable.name
companion object : ImmutableEntityClass<Int, CityEntity>(CitiesTable)
}
Properties are defined using
val
instead ofvar
. This enforces immutability, asval
properties cannot be reassigned after initial assignment.The primary function of the entity class in this context is to query data from the associated table, not to modify it. Therefore, inserts can only be performed using the DSL
.insert()
method and updates can be done either through the DSL.update()
or theImmutableEntityClass.forceUpdateEntity()
methods.
Class method overrides
You can use override methods in both the Entity
class and the EntityClass
companion object to extend functionality or manage entity behavior.
For example, here’s how to override the delete()
method in a User
entity:
package org.example.entities
import org.example.tables.UsersTable
import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.IntEntityClass
import org.jetbrains.exposed.dao.id.EntityID
class UserEntityWithOverride(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<UserEntityWithOverride>(UsersTable)
var name by UsersTable.name
var city by CityEntity referencedOn UsersTable.cityId
override fun delete() {
println("Deleting user $name with ID: $id")
super.delete()
}
}
In this example, a custom message is printed before the .delete()
function of the superclass (IntEntity
) completes the deletion.
Field transformations
As databases typically store only basic types, such as integers and strings, it's not always convenient to keep the same simplicity on DAO level.
For example, you might need to parse JSON from a VARCHAR
column, or retrieve values from a cache based on data from the database. In such cases, we recommend to use column transformations.
Example: Defining an Unsigned Integer field
Suppose that you want to define an unsigned integer field on an entity, but Exposed doesn't have such a column type yet. You can achieve this by using the following implementation:
package org.example.entities
import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.IntEntityClass
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.dao.id.IntIdTable
object TableWithUnsignedInteger : IntIdTable() {
val uint = integer("uint")
}
class EntityWithUInt(id: EntityID<Int>) : IntEntity(id) {
var uint: UInt by TableWithUnsignedInteger.uint.transform({ it.toInt() }, { it.toUInt() })
companion object : IntEntityClass<EntityWithUInt>(TableWithUnsignedInteger)
}
The .transform()
function accepts two lambdas that convert values to and from the original column type. In this case, you make sure to store only UInt
instances in the uint
field.
Although it is still possible to insert or update values with negative integers via DAO, this approach assures a cleaner business logic.
Memoized transformations
If your transformation function involves complex operations that impact performance, you can use .memoizedTransform()
to cache the result of the transformation.
package org.example.entities
import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.IntEntityClass
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.dao.id.IntIdTable
import java.util.Base64
object TableWithText : IntIdTable() {
val text = varchar("text", length = 2048)
}
class EntityWithBase64(id: EntityID<Int>) : IntEntity(id) {
var base64: String by TableWithText.text
.memoizedTransform(
wrap = { Base64.getEncoder().encodeToString(it.toByteArray()) },
unwrap = { Base64.getDecoder().decode(it).toString() }
)
companion object :
IntEntityClass<EntityWithBase64>(TableWithText)
}
With memorized transformation, the value is unwrapped only once and then cached for future reads. This cache remains valid for the lifetime of the entity. If the transaction's entity cache is cleared or the entity is reloaded in a new transaction (creating a new cache without the existing value), the value will be wrapped again. However, if the original entity is kept alive outside of the transaction, the cached value persists to avoid re-wrapping.